package main import ( // Internal "notes2/authentication" "notes2/html_template" "os" // Standard "bufio" "context" "embed" "encoding/json" "flag" "fmt" "io" "log/slog" "net/http" "path" "regexp" "strconv" "strings" "text/template" ) const VERSION = "v1" const CONTEXT_USER = 1 const SYNC_PAGINATION = 250 var ( FlagGenerate bool FlagDev bool FlagConfig string FlagCreateUser string FlagChangePassword string Webengine HTMLTemplate.Engine config Config Log *slog.Logger AuthManager authentication.Manager RxpBearerToken *regexp.Regexp //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.BoolVar(&FlagGenerate, "generate", false, "Generate test data") 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() RxpBearerToken = regexp.MustCompile("(?i)^\\s*Bearer\\s+(.*?)\\s*$") } // }}} 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) // Generate test data? if FlagGenerate { err := TestData() if err != nil { fmt.Printf("%s\n", err) return } return } // 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("/notes2", pageNotes2) http.HandleFunc("/login", pageLogin) http.HandleFunc("/sync", pageSync) http.HandleFunc("/user/authenticate", AuthManager.AuthenticationHandler) http.HandleFunc("/sync/node/{sequence}/{offset}", authenticated(actionSyncNode)) http.HandleFunc("/node/retrieve/{uuid}", authenticated(actionNodeRetrieve)) http.HandleFunc("/service_worker.js", pageServiceWorker) listen := fmt.Sprintf("%s:%d", config.Network.Address, config.Network.Port) Log.Info("webserver", "listen_address", listen) http.ListenAndServe(listen, nil) } // }}} func authenticated(fn func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { // {{{ return func(w http.ResponseWriter, r *http.Request) { failed := func(err error) { j, _ := json.Marshal(struct { OK bool Error string AuthFailed bool }{false, err.Error(), true}) w.Write(j) } // The Bearer token is extracted. authHeader := r.Header.Get("Authorization") authParts := RxpBearerToken.FindStringSubmatch(authHeader) if len(authParts) != 2 { failed(fmt.Errorf("Authorization missing or invalid")) return } token := authParts[1] // Token signature is verified with the application secret key. claims, err := AuthManager.VerifyToken(token) if err != nil { failed(err) return } // User object is added to the context for the next handler. user := NewUser(claims) r = r.WithContext(context.WithValue(r.Context(), CONTEXT_USER, user)) Log.Info("webserver", "op", "request", "method", r.Method, "url", r.URL.String(), "username", user.Username) fn(w, r) } } // }}} 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 == "/" { http.Redirect(w, r, "/notes2", http.StatusSeeOther) return } Webengine.StaticResource(w, r) } // }}} func httpError(w http.ResponseWriter, err error) { // {{{ j, _ := json.Marshal(struct { OK bool Error string }{false, err.Error()}) w.Write(j) } // }}} 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 DevMode bool }{VERSION, FlagDev}) if err != nil { w.Write([]byte(err.Error())) return } } // }}} func pageLogin(w http.ResponseWriter, r *http.Request) { // {{{ page := NewPage("login") err := Webengine.Render(page, w, r) if err != nil { w.Write([]byte(err.Error())) return } } // }}} func pageNotes2(w http.ResponseWriter, r *http.Request) { // {{{ page := NewPage("notes2") err := Webengine.Render(page, w, r) if err != nil { w.Write([]byte(err.Error())) return } } // }}} func pageSync(w http.ResponseWriter, r *http.Request) { // {{{ page := NewPage("sync") err := Webengine.Render(page, w, r) if err != nil { w.Write([]byte(err.Error())) return } } // }}} func actionSyncNode(w http.ResponseWriter, r *http.Request) { // {{{ // The purpose of the Client UUID is to avoid // sending nodes back once again to a client that // just created or modified it. request := struct { ClientUUID string }{} body, _ := io.ReadAll(r.Body) err := json.Unmarshal(body, &request) if err != nil { Log.Error("/node/tree", "error", err) httpError(w, err) return } user := getUser(r) changedFrom, _ := strconv.Atoi(r.PathValue("sequence")) offset, _ := strconv.Atoi(r.PathValue("offset")) nodes, maxSeq, moreRowsExist, err := Nodes(user.ID, offset, uint64(changedFrom), request.ClientUUID) if err != nil { Log.Error("/node/tree", "error", err) httpError(w, err) return } Log.Debug("/node/tree", "num_nodes", len(nodes), "maxSeq", maxSeq) foo, _ := json.Marshal(nodes) os.WriteFile(fmt.Sprintf("/tmp/nodes-%d.json", offset), foo, 0644) j, _ := json.Marshal(struct { OK bool Nodes []Node MaxSeq uint64 Continue bool }{true, nodes, maxSeq, moreRowsExist}) w.Write(j) } // }}} func actionNodeRetrieve(w http.ResponseWriter, r *http.Request) { // {{{ user := getUser(r) var err error uuid := r.PathValue("uuid") node, err := RetrieveNode(user.ID, uuid) if err != nil { responseError(w, err) return } responseData(w, map[string]interface{}{ "OK": true, "Node": node, }) } // }}} 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") } // }}} func getUser(r *http.Request) User { // {{{ user, _ := r.Context().Value(CONTEXT_USER).(User) return user } // }}}