parent
bb11b870d6
commit
922112e181
7 changed files with 181 additions and 3 deletions
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
87
pkg/webui/accountsIdResetPassword.go
Normal file
87
pkg/webui/accountsIdResetPassword.go
Normal file
|
@ -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,
|
||||
})
|
||||
})
|
||||
}
|
57
pkg/webui/html/accountsIdResetPassword.html
Normal file
57
pkg/webui/html/accountsIdResetPassword.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
{{ define "main" }}
|
||||
{{ if .PasswordChanged }}
|
||||
<h2>Password Reset Successful</h2>
|
||||
<p>
|
||||
Your password has been set successfully. You can now try to <a href="/login">log in</a>!
|
||||
</p>
|
||||
{{ else }}
|
||||
<h1>User Account</h1>
|
||||
<h2>Password Reset</h2>
|
||||
<form action="/accounts/{{ .Account.Id }}/reset/{{ .Token }}" enctype="multipart/form-data" method="post">
|
||||
<fieldset>
|
||||
<legend>Set Password</legend>
|
||||
<p>
|
||||
You have been invited to set a password for this account. Please choose a
|
||||
strong password or passphrase that you haven't used before. Think about a
|
||||
combination of words that would be memorable yet complex
|
||||
like <a href="https://xkcd.com/936/">Correct-Horse-Battery-Staple</a>.
|
||||
</p>
|
||||
<div class="flex-row">
|
||||
<label for="password">Password</label>
|
||||
<input autofocus
|
||||
class="flex-stretch{{ if .PasswordInvalid }} error{{ end }}"
|
||||
id="password"
|
||||
minlength="8"
|
||||
name="password"
|
||||
type="password"
|
||||
required>
|
||||
<button type="submit" value="edit">Set Password</button>
|
||||
</div>
|
||||
{{ if .PasswordInvalid }}
|
||||
<span class="error">
|
||||
<span class="tooltip">
|
||||
Invalid password.
|
||||
<span class="tooltip-text">
|
||||
Passwords must be at least 8 characters long.
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
{{ end }}
|
||||
</fieldset>
|
||||
</form>
|
||||
<h2>Status</h2>
|
||||
<p>
|
||||
The account
|
||||
<strong>{{ .Account.Username }}</strong>
|
||||
was created on
|
||||
<strong>{{ .Account.Created }}</strong>
|
||||
and
|
||||
{{ if eq .Account.Created .Account.LastLogin }}
|
||||
<strong>never logged in</strong>.
|
||||
{{ else }}
|
||||
last logged in on
|
||||
<strong>{{ .Account.LastLogin }}</strong>.
|
||||
{{ end }}
|
||||
</p>
|
||||
{{ end }}
|
||||
{{ end }}
|
|
@ -19,6 +19,11 @@
|
|||
<i class="material-symbols-outlined">login</i>
|
||||
<span>Login</span>
|
||||
</a>
|
||||
{{ else if eq .Page.Section "reset" }}
|
||||
<a href="/login">
|
||||
<i class="material-symbols-outlined">login</i>
|
||||
<span>Login</span>
|
||||
</a>
|
||||
{{ else if eq .Page.Section "error" }}
|
||||
<a href="/">
|
||||
<i class="material-symbols-outlined">mountain_flag</i>
|
||||
|
@ -50,5 +55,6 @@
|
|||
</div>
|
||||
<footer>
|
||||
</footer>
|
||||
<script type="module" src="/static/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -3,7 +3,6 @@ package webui
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
@ -55,8 +54,8 @@ func handleLoginPOST(db *database.DB) http.Handler {
|
|||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
|
||||
if username == "" || password == "" { // the webui cannot issue this
|
||||
errorResponse(w, r, http.StatusBadRequest, fmt.Errorf("Forbidden"))
|
||||
if username == "" || password == "" {
|
||||
errorResponse(w, r, http.StatusBadRequest, nil)
|
||||
return
|
||||
}
|
||||
if ok := validUsername.MatchString(username); !ok {
|
||||
|
|
|
@ -15,6 +15,8 @@ func addRoutes(
|
|||
requireAdmin := adminMiddleware(db, requireLogin)
|
||||
mux.Handle("GET /accounts", requireLogin(handleAccountsGET(db)))
|
||||
mux.Handle("GET /accounts/{id}", requireLogin(handleAccountsIdGET(db)))
|
||||
mux.Handle("GET /accounts/{id}/reset/{token}", handleAccountsIdResetPasswordGET(db))
|
||||
mux.Handle("POST /accounts/{id}/reset/{token}", handleAccountsIdResetPasswordPOST(db))
|
||||
mux.Handle("POST /accounts", requireAdmin(handleAccountsPOST(db)))
|
||||
mux.Handle("GET /healthz", handleHealthz())
|
||||
mux.Handle("GET /login", requireSession(handleLoginGET()))
|
||||
|
|
Loading…
Add table
Reference in a new issue