diff --git a/pkg/database/accounts.go b/pkg/database/accounts.go index 8eb8d16..7af95a0 100644 --- a/pkg/database/accounts.go +++ b/pkg/database/accounts.go @@ -28,8 +28,9 @@ func (db *DB) CreateAccount(username string, isAdmin bool) (*model.Account, erro if err := passwordReset.Generate(uuid.V4); err != nil { return nil, fmt.Errorf("failed to generate password reset uuid: %w", err) } - _, err := db.Exec(`INSERT INTO accounts(id, username, is_Admin, settings, password_reset) - VALUES (?, ?, ?, jsonb(?), ?);`, + _, err := db.Exec( + `INSERT INTO accounts(id, username, is_Admin, settings, password_reset) + VALUES (?, ?, ?, jsonb(?), ?);`, accountId, username, isAdmin, diff --git a/pkg/database/sessions.go b/pkg/database/sessions.go index 58a69d1..8a49938 100644 --- a/pkg/database/sessions.go +++ b/pkg/database/sessions.go @@ -54,7 +54,17 @@ func (db *DB) DeleteExpiredSessions() error { func (db *DB) DeleteSession(session *model.Session) error { _, err := db.Exec(`DELETE FROM sessions WHERE id = ?`, session.Id) if err != nil { - return fmt.Errorf("failed to delete session %s: %w", session.Id, err) + return fmt.Errorf("failed to delete session: %w", err) + } + return nil +} + +func (db *DB) DeleteSessions(account *model.Account) error { + _, err := db.Exec( + `DELETE FROM sessions WHERE data->'account'->>'id' = ?`, + account.Id) + if err != nil { + return fmt.Errorf("failed to delete sessions: %w", err) } return nil } diff --git a/pkg/model/account.go b/pkg/model/account.go index 02d1276..5c57769 100644 --- a/pkg/model/account.go +++ b/pkg/model/account.go @@ -2,6 +2,7 @@ package model import ( "crypto/subtle" + "fmt" "time" "git.adyxax.org/adyxax/tfstated/pkg/helpers" @@ -27,6 +28,17 @@ func (account *Account) CheckPassword(password string) bool { return subtle.ConstantTimeCompare(hash, account.PasswordHash) == 1 } +func (account *Account) ResetPassword() error { + var passwordReset uuid.UUID + if err := passwordReset.Generate(uuid.V4); err != nil { + return fmt.Errorf("failed to generate password reset uuid: %w", err) + } + account.Salt = nil + account.PasswordHash = nil + account.PasswordReset = &passwordReset + return nil +} + func (account *Account) SetPassword(password string) { account.Salt = helpers.GenerateSalt() account.PasswordHash = helpers.HashPassword(password, account.Salt) diff --git a/pkg/webui/accountsId.go b/pkg/webui/accountsId.go index 90c2774..452d53b 100644 --- a/pkg/webui/accountsId.go +++ b/pkg/webui/accountsId.go @@ -23,45 +23,98 @@ type AccountsIdPage struct { var accountsIdTemplates = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/accountsId.html")) +func prepareAccountsIdPage(db *database.DB, w http.ResponseWriter, r *http.Request) *AccountsIdPage { + var accountId uuid.UUID + if err := accountId.Parse(r.PathValue("id")); err != nil { + errorResponse(w, r, http.StatusBadRequest, err) + return nil + } + account, err := db.LoadAccountById(&accountId) + if err != nil { + errorResponse(w, r, http.StatusInternalServerError, err) + return nil + } + if account == nil { + errorResponse(w, r, http.StatusNotFound, fmt.Errorf("The account Id could not be found.")) + return nil + } + statePaths, err := db.LoadStatePaths() + if err != nil { + errorResponse(w, r, http.StatusInternalServerError, err) + return nil + } + versions, err := db.LoadVersionsByAccount(account) + if err != nil { + errorResponse(w, r, http.StatusInternalServerError, err) + return nil + } + isAdmin := "" + if account.IsAdmin { + isAdmin = "1" + } + return &AccountsIdPage{ + Account: account, + IsAdmin: isAdmin, + Page: makePage(r, &Page{ + Section: "accounts", + Title: account.Username, + }), + StatePaths: statePaths, + Versions: versions, + } + +} + func handleAccountsIdGET(db *database.DB) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var accountId uuid.UUID - if err := accountId.Parse(r.PathValue("id")); err != nil { + page := prepareAccountsIdPage(db, w, r) + if page != nil { + render(w, accountsIdTemplates, http.StatusOK, page) + } + }) +} + +func handleAccountsIdPOST(db *database.DB) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { errorResponse(w, r, http.StatusBadRequest, err) return } - account, err := db.LoadAccountById(&accountId) - if err != nil { - errorResponse(w, r, http.StatusInternalServerError, err) + if !verifyCSRFToken(w, r) { return } - if account == nil { - errorResponse(w, r, http.StatusNotFound, fmt.Errorf("The account Id could not be found.")) + page := prepareAccountsIdPage(db, w, r) + if page == nil { return } - statePaths, err := db.LoadStatePaths() - if err != nil { - errorResponse(w, r, http.StatusInternalServerError, err) + action := r.FormValue("action") + switch action { + case "delete": + errorResponse(w, r, http.StatusNotImplemented, nil) + return + case "edit": + errorResponse(w, r, http.StatusNotImplemented, nil) + 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 { + errorResponse(w, r, http.StatusInternalServerError, + fmt.Errorf("failed to save account: %w", err)) + return + } + if err := db.DeleteSessions(page.Account); err != nil { + errorResponse(w, r, http.StatusInternalServerError, + fmt.Errorf("failed to save account: %w", err)) + return + } + default: + errorResponse(w, r, http.StatusBadRequest, nil) return } - versions, err := db.LoadVersionsByAccount(account) - if err != nil { - errorResponse(w, r, http.StatusInternalServerError, err) - return - } - isAdmin := "" - if account.IsAdmin { - isAdmin = "1" - } - render(w, accountsIdTemplates, http.StatusOK, AccountsIdPage{ - Account: account, - IsAdmin: isAdmin, - Page: makePage(r, &Page{ - Section: "accounts", - Title: account.Username, - }), - StatePaths: statePaths, - Versions: versions, - }) + render(w, accountsIdTemplates, http.StatusOK, page) }) } diff --git a/pkg/webui/accountsIdResetPassword.go b/pkg/webui/accountsIdResetPassword.go index 9c5b6f5..55b6249 100644 --- a/pkg/webui/accountsIdResetPassword.go +++ b/pkg/webui/accountsIdResetPassword.go @@ -82,7 +82,8 @@ func handleAccountsIdResetPasswordPOST(db *database.DB) http.Handler { } account.SetPassword(password) if err := db.SaveAccount(account); err != nil { - errorResponse(w, r, http.StatusInternalServerError, err) + errorResponse(w, r, http.StatusInternalServerError, + fmt.Errorf("failed to save account: %w", err)) return } render(w, accountsIdResetPasswordTemplates, http.StatusOK, diff --git a/pkg/webui/html/accountsId.html b/pkg/webui/html/accountsId.html index fe05cbf..6a9f8d3 100644 --- a/pkg/webui/html/accountsId.html +++ b/pkg/webui/html/accountsId.html @@ -26,9 +26,9 @@ {{ end }} {{ if .Page.Session.Data.Account.IsAdmin }}

Operations

-
- -
+
+ +
Edit User Account
@@ -59,25 +59,30 @@ {{ end }}
- +
+ +
+
Danger Zone
-
- + +
{{ end }}

Activity

{{ if gt (len .Versions) 0 }} diff --git a/pkg/webui/html/accountsIdResetPassword.html b/pkg/webui/html/accountsIdResetPassword.html index 87ced7a..c690313 100644 --- a/pkg/webui/html/accountsIdResetPassword.html +++ b/pkg/webui/html/accountsIdResetPassword.html @@ -7,7 +7,7 @@ {{ else }}

User Account

Password Reset

-
+
Set Password diff --git a/pkg/webui/routes.go b/pkg/webui/routes.go index f21fd83..77dcea9 100644 --- a/pkg/webui/routes.go +++ b/pkg/webui/routes.go @@ -15,6 +15,7 @@ func addRoutes( requireAdmin := adminMiddleware(requireLogin) mux.Handle("GET /accounts", requireLogin(handleAccountsGET(db))) mux.Handle("GET /accounts/{id}", requireLogin(handleAccountsIdGET(db))) + mux.Handle("POST /accounts/{id}", requireAdmin(handleAccountsIdPOST(db))) mux.Handle("GET /accounts/{id}/reset/{token}", requireSession(handleAccountsIdResetPasswordGET(db))) mux.Handle("POST /accounts/{id}/reset/{token}", requireSession(handleAccountsIdResetPasswordPOST(db))) mux.Handle("POST /accounts", requireAdmin(handleAccountsPOST(db))) diff --git a/pkg/webui/statesId.go b/pkg/webui/statesId.go index f37162f..eb6d48d 100644 --- a/pkg/webui/statesId.go +++ b/pkg/webui/statesId.go @@ -89,7 +89,8 @@ func handleStatesIdPOST(db *database.DB) http.Handler { action := r.FormValue("action") switch action { case "delete": - errorResponse(w, r, http.StatusNotImplemented, err) + errorResponse(w, r, http.StatusNotImplemented, nil) + return case "edit": statePath := r.FormValue("path") parsedStatePath, err := url.Parse(statePath) @@ -128,7 +129,7 @@ func handleStatesIdPOST(db *database.DB) http.Handler { } state.Lock = nil default: - errorResponse(w, r, http.StatusBadRequest, err) + errorResponse(w, r, http.StatusBadRequest, nil) return } render(w, statesIdTemplate, http.StatusOK, StatesIdPage{