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