package web import ( "errors" "net/http" "time" "git.josie-c.com/josie/simplegit/internal/auth" "git.josie-c.com/josie/simplegit/internal/db" ) // tokenView is one row of the token list, with a formatted date. type tokenView struct { ID int64 Name string Hint string Created string } type settingsData struct { Username string Tokens []tokenView NewToken string // plaintext, shown exactly once PasswordError string PasswordOK bool } func tokenViews(tokens []db.Token) []tokenView { views := make([]tokenView, 0, len(tokens)) for _, tok := range tokens { views = append(views, tokenView{ ID: tok.ID, Name: tok.Name, Hint: tok.Hint, Created: time.Unix(tok.CreatedAt, 0).Format("2006-01-02"), }) } return views } // settingsView loads the page model for a user; a missing token list is // non-fatal (the page still renders the password form). func (s *Server) settingsView(user *db.User) settingsData { data := settingsData{Username: user.Username} if tokens, err := db.ListTokens(s.database, user.ID); err == nil { data.Tokens = tokenViews(tokens) } return data } // requireUser redirects anonymous callers to the login page and reports // whether the request may continue. func requireUser(w http.ResponseWriter, r *http.Request) *db.User { user := currentUser(r) if user == nil { http.Redirect(w, r, "/login", http.StatusSeeOther) return nil } return user } // handleSettings shows the account page: token list and password change. func (s *Server) handleSettings(w http.ResponseWriter, r *http.Request) { user := requireUser(w, r) if user == nil { return } s.render(w, "settings.html", http.StatusOK, s.settingsView(user)) } // handleCreateToken mints a token and re-renders the page so the plaintext // is visible once. func (s *Server) handleCreateToken(w http.ResponseWriter, r *http.Request) { user := requireUser(w, r) if user == nil { return } r.Body = http.MaxBytesReader(w, r.Body, loginFormMax) if err := r.ParseForm(); err != nil { http.Error(w, "bad form", http.StatusBadRequest) return } name := formText(r, "name", tokenLabelMax) if name == "" { name = "token" } plain, err := auth.NewAPIToken() if err != nil { s.internalError(w, r, err) return } if _, err := db.CreateToken(s.database, user.ID, name, auth.TokenHint(plain), auth.HashToken(plain)); err != nil { s.internalError(w, r, err) return } data := s.settingsView(user) data.NewToken = plain s.render(w, "settings.html", http.StatusOK, data) } // handleDeleteToken revokes one of the caller's tokens. func (s *Server) handleDeleteToken(w http.ResponseWriter, r *http.Request) { user := requireUser(w, r) if user == nil { return } id, ok := pathID(r) if !ok { http.NotFound(w, r) return } if err := db.DeleteToken(s.database, user.ID, id); err != nil && !errors.Is(err, db.ErrNotFound) { s.internalError(w, r, err) return } http.Redirect(w, r, "/settings", http.StatusSeeOther) } // handleChangePassword verifies the current password, stores a new hash, // and signs out every other browser session. func (s *Server) handleChangePassword(w http.ResponseWriter, r *http.Request) { user := requireUser(w, r) if user == nil { return } r.Body = http.MaxBytesReader(w, r.Body, loginFormMax) if err := r.ParseForm(); err != nil { http.Error(w, "bad form", http.StatusBadRequest) return } current := r.FormValue("current_password") next := r.FormValue("new_password") confirm := r.FormValue("confirm_password") fail := func(message string) { data := s.settingsView(user) data.PasswordError = message s.render(w, "settings.html", http.StatusUnprocessableEntity, data) } switch { case !auth.CheckPassword(user.PasswordHash, current): fail("current password is incorrect") return case next == "": fail("new password must not be empty") return case next != confirm: fail("new passwords do not match") return } hash, err := auth.HashPassword(next) if err != nil { s.internalError(w, r, err) return } if err := db.UpdateUserPassword(s.database, user.ID, hash); err != nil { s.internalError(w, r, err) return } if cookie, err := r.Cookie(sessionCookieName); err == nil { _ = db.DeleteOtherSessions(s.database, user.ID, cookie.Value) } data := s.settingsView(user) data.PasswordOK = true s.render(w, "settings.html", http.StatusOK, data) }