From 373f567773b483c4c70d09c8eb211cc51da6e28d Mon Sep 17 00:00:00 2001 From: Julien Dessaux Date: Sat, 3 May 2025 09:48:53 +0200 Subject: [PATCH] feat(webui): add accounts username and isAdmin flag edition for admins Closes #43 --- pkg/database/accounts.go | 104 +++++++++++++++++---------- pkg/webui/accounts.go | 2 +- pkg/webui/accountsId.go | 35 ++++++++- pkg/webui/accountsIdResetPassword.go | 8 ++- pkg/webui/html/accounts.html | 6 +- pkg/webui/html/accountsId.html | 6 +- 6 files changed, 111 insertions(+), 50 deletions(-) diff --git a/pkg/database/accounts.go b/pkg/database/accounts.go index 7af95a0..123471b 100644 --- a/pkg/database/accounts.go +++ b/pkg/database/accounts.go @@ -234,48 +234,74 @@ func (db *DB) LoadAccountByUsername(username string) (*model.Account, error) { return &account, nil } -func (db *DB) SaveAccount(account *model.Account) error { - _, err := db.Exec( - `UPDATE accounts - SET username = ?, - salt = ?, - password_hash = ?, - is_admin = ?, - password_reset = ? - WHERE id = ?`, - account.Username, - account.Salt, - account.PasswordHash, - account.IsAdmin, - account.PasswordReset, - account.Id) - if err != nil { - return fmt.Errorf("failed to update account id %s: %w", account.Id, err) - } - return nil +func (db *DB) SaveAccount(account *model.Account) (bool, error) { + ret := false + return ret, db.WithTransaction(func(tx *sql.Tx) error { + _, err := tx.ExecContext(db.ctx, + `UPDATE accounts + SET username = ?, + salt = ?, + password_hash = ?, + is_admin = ?, + password_reset = ? + WHERE id = ?`, + account.Username, + account.Salt, + account.PasswordHash, + account.IsAdmin, + account.PasswordReset, + account.Id) + if err != nil { + var sqliteErr sqlite3.Error + if errors.As(err, &sqliteErr) { + if sqliteErr.Code == sqlite3.ErrNo(sqlite3.ErrConstraint) { + return nil + } + } + return fmt.Errorf("failed to update account id %s: %w", account.Id, err) + } + data, err := json.Marshal(account) + if err != nil { + return fmt.Errorf("failed to marshal account %s: %w", account.Username, err) + } + _, err = tx.ExecContext(db.ctx, + `UPDATE sessions + SET data = jsonb_replace(data, + '$.account', jsonb(:data)) + WHERE data->'account'->>'id' = :id`, + sql.Named("data", data), + sql.Named("id", account.Id)) + if err != nil { + return fmt.Errorf("failed to update account settings for user account %s: %w", account.Username, err) + } + ret = true + return nil + }) } func (db *DB) SaveAccountSettings(account *model.Account, settings *model.Settings) error { - data, err := json.Marshal(settings) - if err != nil { - return fmt.Errorf("failed to marshal settings for user accont %s: %w", account.Username, err) - } - _, err = db.Exec(`UPDATE accounts SET settings = ? WHERE id = ?`, data, account.Id) - if err != nil { - return fmt.Errorf("failed to update account settings for user account %s: %w", account.Username, err) - } - _, err = db.Exec( - `UPDATE sessions - SET data = jsonb_replace(data, - '$.settings', jsonb(:data), - '$.account.settings', jsonb(:data)) - WHERE data->'account'->>'id' = :id`, - sql.Named("data", data), - sql.Named("id", account.Id)) - if err != nil { - return fmt.Errorf("failed to update account settings for user account %s: %w", account.Username, err) - } - return nil + return db.WithTransaction(func(tx *sql.Tx) error { + data, err := json.Marshal(settings) + if err != nil { + return fmt.Errorf("failed to marshal settings for user account %s: %w", account.Username, err) + } + _, err = tx.ExecContext(db.ctx, `UPDATE accounts SET settings = ? WHERE id = ?`, data, account.Id) + if err != nil { + return fmt.Errorf("failed to update account settings for user account %s: %w", account.Username, err) + } + _, err = tx.ExecContext(db.ctx, + `UPDATE sessions + SET data = jsonb_replace(data, + '$.settings', jsonb(:data), + '$.account.settings', jsonb(:data)) + WHERE data->'account'->>'id' = :id`, + sql.Named("data", data), + sql.Named("id", account.Id)) + if err != nil { + return fmt.Errorf("failed to update account settings for user account %s: %w", account.Username, err) + } + return nil + }) } func (db *DB) TouchAccount(account *model.Account) error { diff --git a/pkg/webui/accounts.go b/pkg/webui/accounts.go index ad0dbca..5237b70 100644 --- a/pkg/webui/accounts.go +++ b/pkg/webui/accounts.go @@ -51,7 +51,7 @@ func handleAccountsPOST(db *database.DB) http.Handler { return } accountUsername := r.FormValue("username") - isAdmin := r.FormValue("isAdmin") + isAdmin := r.FormValue("is-admin") page := AccountsPage{ Page: makePage(r, &Page{Title: "New Account", Section: "accounts"}), Accounts: accounts, diff --git a/pkg/webui/accountsId.go b/pkg/webui/accountsId.go index 452d53b..e3a52d7 100644 --- a/pkg/webui/accountsId.go +++ b/pkg/webui/accountsId.go @@ -87,25 +87,54 @@ func handleAccountsIdPOST(db *database.DB) http.Handler { if page == nil { return } + session := r.Context().Value(model.SessionContextKey{}).(*model.Session) action := r.FormValue("action") switch action { case "delete": errorResponse(w, r, http.StatusNotImplemented, nil) return case "edit": - errorResponse(w, r, http.StatusNotImplemented, nil) - return + page.Username = r.FormValue("username") + isAdmin := r.FormValue("is-admin") + if ok := validUsername.MatchString(page.Username); !ok { + page.UsernameInvalid = true + render(w, accountsIdTemplates, http.StatusBadRequest, page) + return + } + if page.Account.Id != session.Data.Account.Id { + page.Account.IsAdmin = isAdmin == "1" + } + prev := page.Account.Username + page.Account.Username = page.Username + success, err := db.SaveAccount(page.Account) + if err != nil { + errorResponse(w, r, http.StatusInternalServerError, + fmt.Errorf("failed to save account: %w", err)) + return + } + if !success { + page.Account.Username = prev + page.UsernameDuplicate = true + render(w, accountsIdTemplates, http.StatusBadRequest, page) + return + } case "reset-password": if err := page.Account.ResetPassword(); err != nil { errorResponse(w, r, http.StatusNotImplemented, fmt.Errorf("failed to reset password: %w", err)) return } - if err := db.SaveAccount(page.Account); err != nil { + success, err := db.SaveAccount(page.Account) + if err != nil { errorResponse(w, r, http.StatusInternalServerError, fmt.Errorf("failed to save account: %w", err)) return } + if !success { + errorResponse(w, r, http.StatusInternalServerError, + fmt.Errorf("failed to save account: table constraint error")) + return + } if err := db.DeleteSessions(page.Account); err != nil { errorResponse(w, r, http.StatusInternalServerError, fmt.Errorf("failed to save account: %w", err)) diff --git a/pkg/webui/accountsIdResetPassword.go b/pkg/webui/accountsIdResetPassword.go index 55b6249..826345a 100644 --- a/pkg/webui/accountsIdResetPassword.go +++ b/pkg/webui/accountsIdResetPassword.go @@ -81,11 +81,17 @@ func handleAccountsIdResetPasswordPOST(db *database.DB) http.Handler { return } account.SetPassword(password) - if err := db.SaveAccount(account); err != nil { + success, err := db.SaveAccount(account) + if err != nil { errorResponse(w, r, http.StatusInternalServerError, fmt.Errorf("failed to save account: %w", err)) return } + if !success { + errorResponse(w, r, http.StatusInternalServerError, + fmt.Errorf("failed to save account: table constraint error")) + return + } render(w, accountsIdResetPasswordTemplates, http.StatusOK, AccountsIdResetPasswordPage{ Account: account, diff --git a/pkg/webui/html/accounts.html b/pkg/webui/html/accounts.html index 227585e..6b501ff 100644 --- a/pkg/webui/html/accounts.html +++ b/pkg/webui/html/accounts.html @@ -8,7 +8,7 @@

{{ if .Page.Session.Data.Account.IsAdmin }} -
+
New User Account @@ -21,11 +21,11 @@ type="text" value="{{ .Username }}"> - + value="1" /> {{ if .UsernameDuplicate }} This username already exist. diff --git a/pkg/webui/html/accountsId.html b/pkg/webui/html/accountsId.html index 6a9f8d3..3897692 100644 --- a/pkg/webui/html/accountsId.html +++ b/pkg/webui/html/accountsId.html @@ -1,5 +1,5 @@ {{ define "main" }} -

User Account

+

{{ .Account.Username }}

{{ if ne .Account.PasswordReset nil }}

Password Reset

@@ -37,14 +37,14 @@ id="username" name="username" type="text" - value="{{ .Username }}"> + value="{{ if eq .Username "" }}{{ .Account.Username }}{{ else }}{{ .Username }}{{ end }}"> + value="1" /> {{ if .UsernameDuplicate }} This username already exist.