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 }}
-