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
|
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 {
|
func (db *DB) SaveAccountSettings(account *model.Account, settings *model.Settings) error {
|
||||||
data, err := json.Marshal(settings)
|
data, err := json.Marshal(settings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -27,3 +27,9 @@ func (account *Account) CheckPassword(password string) bool {
|
||||||
hash := helpers.HashPassword(password, account.Salt)
|
hash := helpers.HashPassword(password, account.Salt)
|
||||||
return subtle.ConstantTimeCompare(hash, account.PasswordHash) == 1
|
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>
|
<i class="material-symbols-outlined">login</i>
|
||||||
<span>Login</span>
|
<span>Login</span>
|
||||||
</a>
|
</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" }}
|
{{ else if eq .Page.Section "error" }}
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<i class="material-symbols-outlined">mountain_flag</i>
|
<i class="material-symbols-outlined">mountain_flag</i>
|
||||||
|
@ -50,5 +55,6 @@
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
</footer>
|
</footer>
|
||||||
|
<script type="module" src="/static/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -3,7 +3,6 @@ package webui
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -55,8 +54,8 @@ func handleLoginPOST(db *database.DB) http.Handler {
|
||||||
username := r.FormValue("username")
|
username := r.FormValue("username")
|
||||||
password := r.FormValue("password")
|
password := r.FormValue("password")
|
||||||
|
|
||||||
if username == "" || password == "" { // the webui cannot issue this
|
if username == "" || password == "" {
|
||||||
errorResponse(w, r, http.StatusBadRequest, fmt.Errorf("Forbidden"))
|
errorResponse(w, r, http.StatusBadRequest, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ok := validUsername.MatchString(username); !ok {
|
if ok := validUsername.MatchString(username); !ok {
|
||||||
|
|
|
@ -15,6 +15,8 @@ func addRoutes(
|
||||||
requireAdmin := adminMiddleware(db, requireLogin)
|
requireAdmin := adminMiddleware(db, requireLogin)
|
||||||
mux.Handle("GET /accounts", requireLogin(handleAccountsGET(db)))
|
mux.Handle("GET /accounts", requireLogin(handleAccountsGET(db)))
|
||||||
mux.Handle("GET /accounts/{id}", requireLogin(handleAccountsIdGET(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("POST /accounts", requireAdmin(handleAccountsPOST(db)))
|
||||||
mux.Handle("GET /healthz", handleHealthz())
|
mux.Handle("GET /healthz", handleHealthz())
|
||||||
mux.Handle("GET /login", requireSession(handleLoginGET()))
|
mux.Handle("GET /login", requireSession(handleLoginGET()))
|
||||||
|
|
Loading…
Add table
Reference in a new issue