From 8d75b75af79f6cb308e6b9c18ea431a0d01e5551 Mon Sep 17 00:00:00 2001
From: Julien Dessaux
Date: Mon, 5 May 2025 00:34:08 +0200
Subject: [PATCH] feat(webui): add user account delete
Closes #19
---
pkg/database/accounts.go | 27 +++++++++++++++------------
pkg/database/sql/000_init.sql | 4 +++-
pkg/model/account.go | 8 ++++++++
pkg/webui/accountsId.go | 30 ++++++++++++++++++++++++++----
pkg/webui/html/accountsId.html | 6 ++++--
pkg/webui/login.go | 2 +-
6 files changed, 57 insertions(+), 20 deletions(-)
diff --git a/pkg/database/accounts.go b/pkg/database/accounts.go
index 123471b..0f4c2a5 100644
--- a/pkg/database/accounts.go
+++ b/pkg/database/accounts.go
@@ -30,11 +30,10 @@ func (db *DB) CreateAccount(username string, isAdmin bool) (*model.Account, erro
}
_, err := db.Exec(
`INSERT INTO accounts(id, username, is_Admin, settings, password_reset)
- VALUES (?, ?, ?, jsonb(?), ?);`,
+ VALUES (?, ?, ?, jsonb('{}'), ?);`,
accountId,
username,
isAdmin,
- []byte("{}"),
passwordReset,
)
if err != nil {
@@ -73,13 +72,12 @@ func (db *DB) InitAdminAccount() error {
hash := helpers.HashPassword(password.String(), salt)
if _, err := tx.ExecContext(db.ctx,
`INSERT INTO accounts(id, username, salt, password_hash, is_admin, settings)
- VALUES (:id, "admin", :salt, :hash, TRUE, jsonb(:settings))
- ON CONFLICT DO UPDATE SET password_hash = :hash
+ VALUES (:id, "admin", :salt, :hash, TRUE, jsonb('{}'))
+ ON CONFLICT DO UPDATE SET password_hash = :hash, is_admin = TRUE
WHERE username = "admin";`,
sql.Named("id", accountId),
sql.Named("hash", hash),
sql.Named("salt", salt),
- sql.Named("settings", []byte("{}")),
); err == nil {
AdvertiseAdminPassword(password.String())
} else {
@@ -93,7 +91,7 @@ func (db *DB) InitAdminAccount() error {
func (db *DB) LoadAccounts() ([]model.Account, error) {
rows, err := db.Query(
`SELECT id, username, salt, password_hash, is_admin, created, last_login,
- json_extract(settings, '$'), password_reset FROM accounts;`)
+ json_extract(settings, '$'), password_reset, deleted FROM accounts;`)
if err != nil {
return nil, fmt.Errorf("failed to load accounts from database: %w", err)
}
@@ -115,7 +113,8 @@ func (db *DB) LoadAccounts() ([]model.Account, error) {
&created,
&lastLogin,
&settings,
- &account.PasswordReset)
+ &account.PasswordReset,
+ &account.Deleted)
if err != nil {
return nil, fmt.Errorf("failed to load account from row: %w", err)
}
@@ -171,7 +170,7 @@ func (db *DB) LoadAccountById(id *uuid.UUID) (*model.Account, error) {
)
err := db.QueryRow(
`SELECT username, salt, password_hash, is_admin, created, last_login,
- json_extract(settings, '$'), password_reset
+ json_extract(settings, '$'), password_reset, deleted
FROM accounts
WHERE id = ?;`,
id,
@@ -182,7 +181,8 @@ func (db *DB) LoadAccountById(id *uuid.UUID) (*model.Account, error) {
&created,
&lastLogin,
&settings,
- &account.PasswordReset)
+ &account.PasswordReset,
+ &account.Deleted)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
@@ -208,7 +208,7 @@ func (db *DB) LoadAccountByUsername(username string) (*model.Account, error) {
)
err := db.QueryRow(
`SELECT id, salt, password_hash, is_admin, created, last_login,
- json_extract(settings, '$'), password_reset
+ json_extract(settings, '$'), password_reset, deleted
FROM accounts
WHERE username = ?;`,
username,
@@ -219,7 +219,8 @@ func (db *DB) LoadAccountByUsername(username string) (*model.Account, error) {
&created,
&lastLogin,
&settings,
- &account.PasswordReset)
+ &account.PasswordReset,
+ &account.Deleted)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
@@ -243,13 +244,15 @@ func (db *DB) SaveAccount(account *model.Account) (bool, error) {
salt = ?,
password_hash = ?,
is_admin = ?,
- password_reset = ?
+ password_reset = ?,
+ deleted = ?
WHERE id = ?`,
account.Username,
account.Salt,
account.PasswordHash,
account.IsAdmin,
account.PasswordReset,
+ account.Deleted,
account.Id)
if err != nil {
var sqliteErr sqlite3.Error
diff --git a/pkg/database/sql/000_init.sql b/pkg/database/sql/000_init.sql
index 30fff33..089bc51 100644
--- a/pkg/database/sql/000_init.sql
+++ b/pkg/database/sql/000_init.sql
@@ -11,9 +11,11 @@ CREATE TABLE accounts (
created INTEGER NOT NULL DEFAULT (unixepoch()),
last_login INTEGER NOT NULL DEFAULT (unixepoch()),
settings BLOB NOT NULL,
- password_reset TEXT
+ password_reset TEXT,
+ deleted INTEGER NOT NULL DEFAULT 0
) STRICT;
CREATE UNIQUE INDEX accounts_username ON accounts(username);
+CREATE INDEX accounts_deleted ON accounts(deleted);
CREATE TABLE sessions (
id BLOB PRIMARY KEY,
diff --git a/pkg/model/account.go b/pkg/model/account.go
index 5c57769..f2b4bd2 100644
--- a/pkg/model/account.go
+++ b/pkg/model/account.go
@@ -21,6 +21,7 @@ type Account struct {
LastLogin time.Time `json:"last_login"`
Settings *Settings `json:"settings"`
PasswordReset *uuid.UUID `json:"password_reset"`
+ Deleted bool `json:"deleted"`
}
func (account *Account) CheckPassword(password string) bool {
@@ -28,6 +29,13 @@ func (account *Account) CheckPassword(password string) bool {
return subtle.ConstantTimeCompare(hash, account.PasswordHash) == 1
}
+func (account *Account) MarkForDeletion() {
+ account.Salt = nil
+ account.PasswordHash = nil
+ account.PasswordReset = nil
+ account.Deleted = true
+}
+
func (account *Account) ResetPassword() error {
var passwordReset uuid.UUID
if err := passwordReset.Generate(uuid.V4); err != nil {
diff --git a/pkg/webui/accountsId.go b/pkg/webui/accountsId.go
index e3a52d7..ef4e06e 100644
--- a/pkg/webui/accountsId.go
+++ b/pkg/webui/accountsId.go
@@ -91,8 +91,25 @@ func handleAccountsIdPOST(db *database.DB) http.Handler {
action := r.FormValue("action")
switch action {
case "delete":
- errorResponse(w, r, http.StatusNotImplemented, nil)
- return
+ if !page.Account.Deleted {
+ page.Account.MarkForDeletion()
+ 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: this cannot happen"))
+ return
+ }
+ if err := db.DeleteSessions(page.Account); err != nil {
+ errorResponse(w, r, http.StatusInternalServerError,
+ fmt.Errorf("failed to delete sessions: %w", err))
+ return
+ }
+ }
case "edit":
page.Username = r.FormValue("username")
isAdmin := r.FormValue("is-admin")
@@ -119,8 +136,13 @@ func handleAccountsIdPOST(db *database.DB) http.Handler {
return
}
case "reset-password":
+ if page.Account.Deleted {
+ errorResponse(w, r, http.StatusBadRequest,
+ fmt.Errorf("You cannot reset the password for this account because it is marked for deletion."))
+ return
+ }
if err := page.Account.ResetPassword(); err != nil {
- errorResponse(w, r, http.StatusNotImplemented,
+ errorResponse(w, r, http.StatusInternalServerError,
fmt.Errorf("failed to reset password: %w", err))
return
}
@@ -137,7 +159,7 @@ func handleAccountsIdPOST(db *database.DB) http.Handler {
}
if err := db.DeleteSessions(page.Account); err != nil {
errorResponse(w, r, http.StatusInternalServerError,
- fmt.Errorf("failed to save account: %w", err))
+ fmt.Errorf("failed to delete sessions: %w", err))
return
}
default:
diff --git a/pkg/webui/html/accountsId.html b/pkg/webui/html/accountsId.html
index 3897692..a4953d2 100644
--- a/pkg/webui/html/accountsId.html
+++ b/pkg/webui/html/accountsId.html
@@ -21,10 +21,12 @@
{{ .Account.LastLogin }}.
{{ end }}
-{{ if .Account.IsAdmin }}
+{{ if .Account.Deleted }}
+This accounts is marked for deletion!
+{{ else if .Account.IsAdmin }}
This accounts has admin privileges on TfStated.
{{ end }}
-{{ if .Page.Session.Data.Account.IsAdmin }}
+{{ if and (not .Account.Deleted) .Page.Session.Data.Account.IsAdmin }}
Operations