97f81af03f521ebc51c64ec84a8649e28d7fa8a0 / internal/web/settings.go · 4401 bytes · raw
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)
}