diff --git a/pkg/database/accounts.go b/pkg/database/accounts.go index bb99cc4..66e23ad 100644 --- a/pkg/database/accounts.go +++ b/pkg/database/accounts.go @@ -215,6 +215,27 @@ 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 user id %s: %w", account.Id, err) + } + return nil +} + func (db *DB) SaveAccountSettings(account *model.Account, settings *model.Settings) error { data, err := json.Marshal(settings) if err != nil { diff --git a/pkg/model/account.go b/pkg/model/account.go index c1ea958..df7ea8a 100644 --- a/pkg/model/account.go +++ b/pkg/model/account.go @@ -27,3 +27,9 @@ func (account *Account) CheckPassword(password string) bool { hash := helpers.HashPassword(password, account.Salt) return subtle.ConstantTimeCompare(hash, account.PasswordHash) == 1 } + +func (account *Account) SetPassword(password string) { + account.Salt = helpers.GenerateSalt() + account.PasswordHash = helpers.HashPassword(password, account.Salt) + account.PasswordReset = nil +} diff --git a/pkg/webui/accountsIdResetPassword.go b/pkg/webui/accountsIdResetPassword.go new file mode 100644 index 0000000..f8e341e --- /dev/null +++ b/pkg/webui/accountsIdResetPassword.go @@ -0,0 +1,87 @@ +package webui + +import ( + "html/template" + "net/http" + + "git.adyxax.org/adyxax/tfstated/pkg/database" + "git.adyxax.org/adyxax/tfstated/pkg/model" + "go.n16f.net/uuid" +) + +type AccountsIdResetPasswordPage struct { + Account *model.Account + Page *Page + PasswordInvalid bool + PasswordChanged bool + Token string +} + +var accountsIdResetPasswordTemplates = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/accountsIdResetPassword.html")) + +func processAccountsIdResetPasswordPathValues(db *database.DB, w http.ResponseWriter, r *http.Request) (*model.Account, bool) { + var accountId uuid.UUID + if err := accountId.Parse(r.PathValue("id")); err != nil { + errorResponse(w, r, http.StatusBadRequest, err) + return nil, false + } + var token uuid.UUID + if err := token.Parse(r.PathValue("token")); err != nil { + errorResponse(w, r, http.StatusBadRequest, err) + return nil, false + } + account, err := db.LoadAccountById(accountId) + if err != nil { + errorResponse(w, r, http.StatusInternalServerError, err) + return nil, false + } + if account == nil || account.PasswordReset == nil { + errorResponse(w, r, http.StatusBadRequest, err) + return nil, false + } + if !account.PasswordReset.Equal(token) { + errorResponse(w, r, http.StatusBadRequest, err) + return nil, false + } + return account, true +} + +func handleAccountsIdResetPasswordGET(db *database.DB) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + account, valid := processAccountsIdResetPasswordPathValues(db, w, r) + if !valid { + return + } + render(w, accountsIdResetPasswordTemplates, http.StatusOK, + AccountsIdResetPasswordPage{ + Account: account, + Page: &Page{Title: "Password Reset", Section: "reset"}, + Token: r.PathValue("token"), + }) + }) +} + +func handleAccountsIdResetPasswordPOST(db *database.DB) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + account, valid := processAccountsIdResetPasswordPathValues(db, w, r) + if !valid { + return + } + password := r.FormValue("password") + if len(password) < 8 { + errorResponse(w, r, http.StatusBadRequest, nil) + return + } + account.SetPassword(password) + if err := db.SaveAccount(account); err != nil { + errorResponse(w, r, http.StatusInternalServerError, err) + return + } + render(w, accountsIdResetPasswordTemplates, http.StatusOK, + AccountsIdResetPasswordPage{ + Account: account, + Page: &Page{Title: "Password Reset", Section: "reset"}, + PasswordChanged: true, + }) + }) +} diff --git a/pkg/webui/html/accountsIdResetPassword.html b/pkg/webui/html/accountsIdResetPassword.html new file mode 100644 index 0000000..c1f348f --- /dev/null +++ b/pkg/webui/html/accountsIdResetPassword.html @@ -0,0 +1,57 @@ +{{ define "main" }} +{{ if .PasswordChanged }} +
+ Your password has been set successfully. You can now try to log in! +
+{{ else }} ++ The account + {{ .Account.Username }} + was created on + {{ .Account.Created }} + and + {{ if eq .Account.Created .Account.LastLogin }} + never logged in. + {{ else }} + last logged in on + {{ .Account.LastLogin }}. + {{ end }} +
+{{ end }} +{{ end }} diff --git a/pkg/webui/html/base.html b/pkg/webui/html/base.html index 81e39a6..2a5b5a9 100644 --- a/pkg/webui/html/base.html +++ b/pkg/webui/html/base.html @@ -19,6 +19,11 @@ login Login + {{ else if eq .Page.Section "reset" }} + + login + Login + {{ else if eq .Page.Section "error" }} mountain_flag @@ -50,5 +55,6 @@ +