parent
373f567773
commit
8d75b75af7
6 changed files with 57 additions and 20 deletions
|
@ -30,11 +30,10 @@ func (db *DB) CreateAccount(username string, isAdmin bool) (*model.Account, erro
|
||||||
}
|
}
|
||||||
_, err := db.Exec(
|
_, err := db.Exec(
|
||||||
`INSERT INTO accounts(id, username, is_Admin, settings, password_reset)
|
`INSERT INTO accounts(id, username, is_Admin, settings, password_reset)
|
||||||
VALUES (?, ?, ?, jsonb(?), ?);`,
|
VALUES (?, ?, ?, jsonb('{}'), ?);`,
|
||||||
accountId,
|
accountId,
|
||||||
username,
|
username,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
[]byte("{}"),
|
|
||||||
passwordReset,
|
passwordReset,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -73,13 +72,12 @@ func (db *DB) InitAdminAccount() error {
|
||||||
hash := helpers.HashPassword(password.String(), salt)
|
hash := helpers.HashPassword(password.String(), salt)
|
||||||
if _, err := tx.ExecContext(db.ctx,
|
if _, err := tx.ExecContext(db.ctx,
|
||||||
`INSERT INTO accounts(id, username, salt, password_hash, is_admin, settings)
|
`INSERT INTO accounts(id, username, salt, password_hash, is_admin, settings)
|
||||||
VALUES (:id, "admin", :salt, :hash, TRUE, jsonb(:settings))
|
VALUES (:id, "admin", :salt, :hash, TRUE, jsonb('{}'))
|
||||||
ON CONFLICT DO UPDATE SET password_hash = :hash
|
ON CONFLICT DO UPDATE SET password_hash = :hash, is_admin = TRUE
|
||||||
WHERE username = "admin";`,
|
WHERE username = "admin";`,
|
||||||
sql.Named("id", accountId),
|
sql.Named("id", accountId),
|
||||||
sql.Named("hash", hash),
|
sql.Named("hash", hash),
|
||||||
sql.Named("salt", salt),
|
sql.Named("salt", salt),
|
||||||
sql.Named("settings", []byte("{}")),
|
|
||||||
); err == nil {
|
); err == nil {
|
||||||
AdvertiseAdminPassword(password.String())
|
AdvertiseAdminPassword(password.String())
|
||||||
} else {
|
} else {
|
||||||
|
@ -93,7 +91,7 @@ func (db *DB) InitAdminAccount() error {
|
||||||
func (db *DB) LoadAccounts() ([]model.Account, error) {
|
func (db *DB) LoadAccounts() ([]model.Account, error) {
|
||||||
rows, err := db.Query(
|
rows, err := db.Query(
|
||||||
`SELECT id, username, salt, password_hash, is_admin, created, last_login,
|
`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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load accounts from database: %w", err)
|
return nil, fmt.Errorf("failed to load accounts from database: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -115,7 +113,8 @@ func (db *DB) LoadAccounts() ([]model.Account, error) {
|
||||||
&created,
|
&created,
|
||||||
&lastLogin,
|
&lastLogin,
|
||||||
&settings,
|
&settings,
|
||||||
&account.PasswordReset)
|
&account.PasswordReset,
|
||||||
|
&account.Deleted)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load account from row: %w", err)
|
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(
|
err := db.QueryRow(
|
||||||
`SELECT username, salt, password_hash, is_admin, created, last_login,
|
`SELECT username, salt, password_hash, is_admin, created, last_login,
|
||||||
json_extract(settings, '$'), password_reset
|
json_extract(settings, '$'), password_reset, deleted
|
||||||
FROM accounts
|
FROM accounts
|
||||||
WHERE id = ?;`,
|
WHERE id = ?;`,
|
||||||
id,
|
id,
|
||||||
|
@ -182,7 +181,8 @@ func (db *DB) LoadAccountById(id *uuid.UUID) (*model.Account, error) {
|
||||||
&created,
|
&created,
|
||||||
&lastLogin,
|
&lastLogin,
|
||||||
&settings,
|
&settings,
|
||||||
&account.PasswordReset)
|
&account.PasswordReset,
|
||||||
|
&account.Deleted)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -208,7 +208,7 @@ func (db *DB) LoadAccountByUsername(username string) (*model.Account, error) {
|
||||||
)
|
)
|
||||||
err := db.QueryRow(
|
err := db.QueryRow(
|
||||||
`SELECT id, salt, password_hash, is_admin, created, last_login,
|
`SELECT id, salt, password_hash, is_admin, created, last_login,
|
||||||
json_extract(settings, '$'), password_reset
|
json_extract(settings, '$'), password_reset, deleted
|
||||||
FROM accounts
|
FROM accounts
|
||||||
WHERE username = ?;`,
|
WHERE username = ?;`,
|
||||||
username,
|
username,
|
||||||
|
@ -219,7 +219,8 @@ func (db *DB) LoadAccountByUsername(username string) (*model.Account, error) {
|
||||||
&created,
|
&created,
|
||||||
&lastLogin,
|
&lastLogin,
|
||||||
&settings,
|
&settings,
|
||||||
&account.PasswordReset)
|
&account.PasswordReset,
|
||||||
|
&account.Deleted)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -243,13 +244,15 @@ func (db *DB) SaveAccount(account *model.Account) (bool, error) {
|
||||||
salt = ?,
|
salt = ?,
|
||||||
password_hash = ?,
|
password_hash = ?,
|
||||||
is_admin = ?,
|
is_admin = ?,
|
||||||
password_reset = ?
|
password_reset = ?,
|
||||||
|
deleted = ?
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
account.Username,
|
account.Username,
|
||||||
account.Salt,
|
account.Salt,
|
||||||
account.PasswordHash,
|
account.PasswordHash,
|
||||||
account.IsAdmin,
|
account.IsAdmin,
|
||||||
account.PasswordReset,
|
account.PasswordReset,
|
||||||
|
account.Deleted,
|
||||||
account.Id)
|
account.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var sqliteErr sqlite3.Error
|
var sqliteErr sqlite3.Error
|
||||||
|
|
|
@ -11,9 +11,11 @@ CREATE TABLE accounts (
|
||||||
created INTEGER NOT NULL DEFAULT (unixepoch()),
|
created INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||||
last_login INTEGER NOT NULL DEFAULT (unixepoch()),
|
last_login INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||||
settings BLOB NOT NULL,
|
settings BLOB NOT NULL,
|
||||||
password_reset TEXT
|
password_reset TEXT,
|
||||||
|
deleted INTEGER NOT NULL DEFAULT 0
|
||||||
) STRICT;
|
) STRICT;
|
||||||
CREATE UNIQUE INDEX accounts_username ON accounts(username);
|
CREATE UNIQUE INDEX accounts_username ON accounts(username);
|
||||||
|
CREATE INDEX accounts_deleted ON accounts(deleted);
|
||||||
|
|
||||||
CREATE TABLE sessions (
|
CREATE TABLE sessions (
|
||||||
id BLOB PRIMARY KEY,
|
id BLOB PRIMARY KEY,
|
||||||
|
|
|
@ -21,6 +21,7 @@ type Account struct {
|
||||||
LastLogin time.Time `json:"last_login"`
|
LastLogin time.Time `json:"last_login"`
|
||||||
Settings *Settings `json:"settings"`
|
Settings *Settings `json:"settings"`
|
||||||
PasswordReset *uuid.UUID `json:"password_reset"`
|
PasswordReset *uuid.UUID `json:"password_reset"`
|
||||||
|
Deleted bool `json:"deleted"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (account *Account) CheckPassword(password string) bool {
|
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
|
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 {
|
func (account *Account) ResetPassword() error {
|
||||||
var passwordReset uuid.UUID
|
var passwordReset uuid.UUID
|
||||||
if err := passwordReset.Generate(uuid.V4); err != nil {
|
if err := passwordReset.Generate(uuid.V4); err != nil {
|
||||||
|
|
|
@ -91,8 +91,25 @@ func handleAccountsIdPOST(db *database.DB) http.Handler {
|
||||||
action := r.FormValue("action")
|
action := r.FormValue("action")
|
||||||
switch action {
|
switch action {
|
||||||
case "delete":
|
case "delete":
|
||||||
errorResponse(w, r, http.StatusNotImplemented, nil)
|
if !page.Account.Deleted {
|
||||||
return
|
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":
|
case "edit":
|
||||||
page.Username = r.FormValue("username")
|
page.Username = r.FormValue("username")
|
||||||
isAdmin := r.FormValue("is-admin")
|
isAdmin := r.FormValue("is-admin")
|
||||||
|
@ -119,8 +136,13 @@ func handleAccountsIdPOST(db *database.DB) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "reset-password":
|
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 {
|
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))
|
fmt.Errorf("failed to reset password: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -137,7 +159,7 @@ func handleAccountsIdPOST(db *database.DB) http.Handler {
|
||||||
}
|
}
|
||||||
if err := db.DeleteSessions(page.Account); err != nil {
|
if err := db.DeleteSessions(page.Account); err != nil {
|
||||||
errorResponse(w, r, http.StatusInternalServerError,
|
errorResponse(w, r, http.StatusInternalServerError,
|
||||||
fmt.Errorf("failed to save account: %w", err))
|
fmt.Errorf("failed to delete sessions: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -21,10 +21,12 @@
|
||||||
<strong>{{ .Account.LastLogin }}</strong>.
|
<strong>{{ .Account.LastLogin }}</strong>.
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</p>
|
</p>
|
||||||
{{ if .Account.IsAdmin }}
|
{{ if .Account.Deleted }}
|
||||||
|
<p>This accounts is <strong>marked for deletion</strong>!</p>
|
||||||
|
{{ else if .Account.IsAdmin }}
|
||||||
<p>This accounts has <strong>admin</strong> privileges on TfStated.</p>
|
<p>This accounts has <strong>admin</strong> privileges on TfStated.</p>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if .Page.Session.Data.Account.IsAdmin }}
|
{{ if and (not .Account.Deleted) .Page.Session.Data.Account.IsAdmin }}
|
||||||
<h2>Operations</h2>
|
<h2>Operations</h2>
|
||||||
<div class="flex-row">
|
<div class="flex-row">
|
||||||
<form action="/accounts/{{ .Account.Id }}" method="post">
|
<form action="/accounts/{{ .Account.Id }}" method="post">
|
||||||
|
|
|
@ -72,7 +72,7 @@ func handleLoginPOST(db *database.DB) http.Handler {
|
||||||
fmt.Errorf("failed to load account by username %s: %w", username, err))
|
fmt.Errorf("failed to load account by username %s: %w", username, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if account == nil || !account.CheckPassword(password) {
|
if account == nil || account.Deleted || !account.CheckPassword(password) {
|
||||||
renderForbidden(w, r, username)
|
renderForbidden(w, r, username)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue