package main import ( // External // Internal "notes2/authentication" "notes2/html_template" "os" // Standard "bufio" "embed" "flag" "fmt" "log/slog" "net/http" "path" "strings" "text/template" ) const VERSION = "v1" var ( FlagDev bool FlagConfig string FlagCreateUser string FlagChangePassword string Webengine HTMLTemplate.Engine config Config Log *slog.Logger AuthManager authentication.Manager //go:embed views ViewFS embed.FS //go:embed static StaticFS embed.FS //go:embed sql SqlFS embed.FS ) func init() { // {{{ // Configuration filename to use with a somewhat sane default. cfgDir, err := os.UserConfigDir() if err != nil { panic(err) } cfgFilename := path.Join(cfgDir, "notes2.json") flag.StringVar(&FlagConfig, "config", cfgFilename, "Configuration file") flag.BoolVar(&FlagDev, "dev", false, "Use local files instead of embedded files") flag.StringVar(&FlagCreateUser, "create-user", "", "Username for creating a new user") flag.StringVar(&FlagChangePassword, "change-password", "", "Change the password for the given username") flag.Parse() } // }}} func initLog() { // {{{ opts := slog.HandlerOptions{} opts.Level = slog.LevelDebug Log = slog.New(slog.NewJSONHandler(os.Stdout, &opts)) } // }}} func main() { // {{{ initLog() err := readConfig() if err != nil { Log.Error("config", "error", err) os.Exit(1) } // dbschema uses the embedded SQL files to keep the database schema up to date. err = initDB() if err != nil { Log.Error("database", "error", err) os.Exit(1) } // The session manager contains authentication, authorization and session settings. AuthManager, err = authentication.NewManager(db, Log, config.JWT.Secret, config.JWT.ExpireDays) // A new user? if FlagCreateUser != "" { createNewUser(FlagCreateUser) return } // Forgotten the password? if FlagChangePassword != "" { changePassword(FlagChangePassword) return } // The webengine takes layouts, pages and components and renders them into HTML. Webengine, err = HTMLTemplate.NewEngine(ViewFS, StaticFS, FlagDev) if err != nil { panic(err) } http.HandleFunc("/", rootHandler) http.HandleFunc("/authenticate", AuthManager.AuthenticationHandler) http.HandleFunc("/service_worker.js", pageServiceWorker) listen := fmt.Sprintf("%s:%d", config.Network.Address, config.Network.Port) http.ListenAndServe(listen, nil) } // }}} func rootHandler(w http.ResponseWriter, r *http.Request) { // {{{ // All URLs not specifically handled are routed to this function. // Everything going here should be a static resource. if r.URL.Path != "/" { Webengine.StaticResource(w, r) return } pageIndex(w, r) } // }}} func pageServiceWorker(w http.ResponseWriter, r *http.Request) { // {{{ w.Header().Add("Content-Type", "text/javascript; charset=utf-8") var tmpl *template.Template var err error if FlagDev { tmpl, err = template.ParseFiles("static/service_worker.js") } else { tmpl, err = template.ParseFS(StaticFS, "static/service_worker.js") } if err != nil { w.Write([]byte(err.Error())) return } err = tmpl.Execute(w, struct{ VERSION string }{VERSION}) if err != nil { w.Write([]byte(err.Error())) return } } // }}} func pageIndex(w http.ResponseWriter, r *http.Request) { // {{{ // Index page is rendered since everything // else are specifically handled. page := HTMLTemplate.SimplePage{ Layout: "main", Page: "index", Version: VERSION, } err := Webengine.Render(page, w, r) if err != nil { w.Write([]byte(err.Error())) return } } // }}} func sessionFilter(fn func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { // {{{ return func(w http.ResponseWriter, r *http.Request) { fmt.Printf("Filtered ") fn(w, r) } } // }}} func createNewUser(username string) { // {{{ reader := bufio.NewReader(os.Stdin) fmt.Print("\nPassword: ") pwd, _ := reader.ReadString('\n') pwd = strings.Trim(pwd, "\r\n") fmt.Print("Name: ") name, _ := reader.ReadString('\n') name = strings.Trim(name, "\r\n") alreadyExists, err := AuthManager.CreateUser(username, pwd, name) if alreadyExists { fmt.Printf("\nUser '%s' already exists\n", username) return } if err != nil { Log.Error("create_user", "error", err) return } fmt.Printf("\nUser '%s' with username '%s' is created.\n", name, username) } // }}} func changePassword(username string) { // {{{ reader := bufio.NewReader(os.Stdin) fmt.Print("\nPassword: ") newPwd, _ := reader.ReadString('\n') newPwd = strings.Trim(newPwd, "\r\n") hasChanged, err := AuthManager.ChangePassword(username, "", newPwd, true) if !hasChanged { fmt.Printf("Invalid user\n") return } if err != nil { Log.Error("change_password", "error", err) return } fmt.Printf("\nPassword changed\n") } // }}}