chore(webui): rewrite the web session code again while preparing for csrf tokens
#60
This commit is contained in:
parent
3bb5e735c6
commit
895615ad6e
20 changed files with 162 additions and 149 deletions
|
@ -29,7 +29,7 @@ func (db *DB) CreateAccount(username string, isAdmin bool) (*model.Account, erro
|
||||||
return nil, fmt.Errorf("failed to generate password reset uuid: %w", err)
|
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)
|
_, err := db.Exec(`INSERT INTO accounts(id, username, is_Admin, settings, password_reset)
|
||||||
VALUES (?, ?, ?, ?, ?);`,
|
VALUES (?, ?, ?, jsonb(?), ?);`,
|
||||||
accountId,
|
accountId,
|
||||||
username,
|
username,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
|
@ -72,7 +72,7 @@ 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, :settings)
|
VALUES (:id, "admin", :salt, :hash, TRUE, jsonb(:settings))
|
||||||
ON CONFLICT DO UPDATE SET password_hash = :hash
|
ON CONFLICT DO UPDATE SET password_hash = :hash
|
||||||
WHERE username = "admin";`,
|
WHERE username = "admin";`,
|
||||||
sql.Named("id", accountId),
|
sql.Named("id", accountId),
|
||||||
|
@ -91,7 +91,8 @@ 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, settings, password_reset FROM accounts;`)
|
`SELECT id, username, salt, password_hash, is_admin, created, last_login,
|
||||||
|
json_extract(settings, '$'), password_reset 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)
|
||||||
}
|
}
|
||||||
|
@ -102,6 +103,7 @@ func (db *DB) LoadAccounts() ([]model.Account, error) {
|
||||||
account model.Account
|
account model.Account
|
||||||
created int64
|
created int64
|
||||||
lastLogin int64
|
lastLogin int64
|
||||||
|
settings []byte
|
||||||
)
|
)
|
||||||
err = rows.Scan(
|
err = rows.Scan(
|
||||||
&account.Id,
|
&account.Id,
|
||||||
|
@ -111,11 +113,14 @@ func (db *DB) LoadAccounts() ([]model.Account, error) {
|
||||||
&account.IsAdmin,
|
&account.IsAdmin,
|
||||||
&created,
|
&created,
|
||||||
&lastLogin,
|
&lastLogin,
|
||||||
&account.Settings,
|
&settings,
|
||||||
&account.PasswordReset)
|
&account.PasswordReset)
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
if err := json.Unmarshal(settings, &account.Settings); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal account settings: %w", err)
|
||||||
|
}
|
||||||
account.Created = time.Unix(created, 0)
|
account.Created = time.Unix(created, 0)
|
||||||
account.LastLogin = time.Unix(lastLogin, 0)
|
account.LastLogin = time.Unix(lastLogin, 0)
|
||||||
accounts = append(accounts, account)
|
accounts = append(accounts, account)
|
||||||
|
@ -161,9 +166,11 @@ func (db *DB) LoadAccountById(id *uuid.UUID) (*model.Account, error) {
|
||||||
var (
|
var (
|
||||||
created int64
|
created int64
|
||||||
lastLogin int64
|
lastLogin int64
|
||||||
|
settings []byte
|
||||||
)
|
)
|
||||||
err := db.QueryRow(
|
err := db.QueryRow(
|
||||||
`SELECT username, salt, password_hash, is_admin, created, last_login, settings, password_reset
|
`SELECT username, salt, password_hash, is_admin, created, last_login,
|
||||||
|
json_extract(settings, '$'), password_reset
|
||||||
FROM accounts
|
FROM accounts
|
||||||
WHERE id = ?;`,
|
WHERE id = ?;`,
|
||||||
id,
|
id,
|
||||||
|
@ -173,7 +180,7 @@ func (db *DB) LoadAccountById(id *uuid.UUID) (*model.Account, error) {
|
||||||
&account.IsAdmin,
|
&account.IsAdmin,
|
||||||
&created,
|
&created,
|
||||||
&lastLogin,
|
&lastLogin,
|
||||||
&account.Settings,
|
&settings,
|
||||||
&account.PasswordReset)
|
&account.PasswordReset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
@ -181,6 +188,9 @@ func (db *DB) LoadAccountById(id *uuid.UUID) (*model.Account, error) {
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("failed to load account by id %s: %w", id, err)
|
return nil, fmt.Errorf("failed to load account by id %s: %w", id, err)
|
||||||
}
|
}
|
||||||
|
if err := json.Unmarshal(settings, &account.Settings); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal account settings: %w", err)
|
||||||
|
}
|
||||||
account.Created = time.Unix(created, 0)
|
account.Created = time.Unix(created, 0)
|
||||||
account.LastLogin = time.Unix(lastLogin, 0)
|
account.LastLogin = time.Unix(lastLogin, 0)
|
||||||
return &account, nil
|
return &account, nil
|
||||||
|
@ -193,9 +203,11 @@ func (db *DB) LoadAccountByUsername(username string) (*model.Account, error) {
|
||||||
var (
|
var (
|
||||||
created int64
|
created int64
|
||||||
lastLogin int64
|
lastLogin int64
|
||||||
|
settings []byte
|
||||||
)
|
)
|
||||||
err := db.QueryRow(
|
err := db.QueryRow(
|
||||||
`SELECT id, salt, password_hash, is_admin, created, last_login, settings, password_reset
|
`SELECT id, salt, password_hash, is_admin, created, last_login,
|
||||||
|
json_extract(settings, '$'), password_reset
|
||||||
FROM accounts
|
FROM accounts
|
||||||
WHERE username = ?;`,
|
WHERE username = ?;`,
|
||||||
username,
|
username,
|
||||||
|
@ -205,7 +217,7 @@ func (db *DB) LoadAccountByUsername(username string) (*model.Account, error) {
|
||||||
&account.IsAdmin,
|
&account.IsAdmin,
|
||||||
&created,
|
&created,
|
||||||
&lastLogin,
|
&lastLogin,
|
||||||
&account.Settings,
|
&settings,
|
||||||
&account.PasswordReset)
|
&account.PasswordReset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
@ -213,6 +225,9 @@ func (db *DB) LoadAccountByUsername(username string) (*model.Account, error) {
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("failed to load account by username %s: %w", username, err)
|
return nil, fmt.Errorf("failed to load account by username %s: %w", username, err)
|
||||||
}
|
}
|
||||||
|
if err := json.Unmarshal(settings, &account.Settings); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal account settings: %w", err)
|
||||||
|
}
|
||||||
account.Created = time.Unix(created, 0)
|
account.Created = time.Unix(created, 0)
|
||||||
account.LastLogin = time.Unix(lastLogin, 0)
|
account.LastLogin = time.Unix(lastLogin, 0)
|
||||||
return &account, nil
|
return &account, nil
|
||||||
|
@ -242,13 +257,20 @@ func (db *DB) SaveAccount(account *model.Account) error {
|
||||||
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 {
|
||||||
return fmt.Errorf("failed to marshal settings for user account %s: %w", account.Username, err)
|
return fmt.Errorf("failed to marshal settings for user accont %s: %w", account.Username, err)
|
||||||
}
|
}
|
||||||
_, err = db.Exec(`UPDATE accounts SET settings = ? WHERE id = ?`, data, account.Id)
|
_, err = db.Exec(`UPDATE accounts SET settings = ? WHERE id = ?`, data, account.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to update account settings for user account %s: %w", account.Username, err)
|
return fmt.Errorf("failed to update account settings for user account %s: %w", account.Username, err)
|
||||||
}
|
}
|
||||||
_, err = db.Exec(`UPDATE sessions SET settings = ? WHERE account_id = ?`, data, account.Id)
|
_, err = db.Exec(
|
||||||
|
`UPDATE sessions
|
||||||
|
SET data = jsonb_replace(data,
|
||||||
|
'$.settings', jsonb(:data),
|
||||||
|
'$.account.settings', jsonb(:data))
|
||||||
|
WHERE data->'account'->>'id' = :id`,
|
||||||
|
sql.Named("data", data),
|
||||||
|
sql.Named("id", account.Id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to update account settings for user account %s: %w", account.Username, err)
|
return fmt.Errorf("failed to update account settings for user account %s: %w", account.Username, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package database
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
@ -10,31 +11,35 @@ import (
|
||||||
"git.adyxax.org/adyxax/tfstated/pkg/helpers"
|
"git.adyxax.org/adyxax/tfstated/pkg/helpers"
|
||||||
"git.adyxax.org/adyxax/tfstated/pkg/model"
|
"git.adyxax.org/adyxax/tfstated/pkg/model"
|
||||||
"git.adyxax.org/adyxax/tfstated/pkg/scrypto"
|
"git.adyxax.org/adyxax/tfstated/pkg/scrypto"
|
||||||
"go.n16f.net/uuid"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (db *DB) CreateSession(account *model.Account, settingsData []byte) (string, error) {
|
func (db *DB) CreateSession(sessionData *model.SessionData) (string, *model.Session, error) {
|
||||||
sessionBytes := scrypto.RandomBytes(32)
|
sessionBytes := scrypto.RandomBytes(32)
|
||||||
sessionId := base64.RawURLEncoding.EncodeToString(sessionBytes[:])
|
sessionId := base64.RawURLEncoding.EncodeToString(sessionBytes[:])
|
||||||
sessionHash := helpers.HashSessionId(sessionBytes, db.sessionsSalt.Bytes())
|
sessionHash := helpers.HashSessionId(sessionBytes, db.sessionsSalt.Bytes())
|
||||||
var accountId *uuid.UUID = nil
|
if sessionData == nil {
|
||||||
var settings = []byte("{}")
|
var err error
|
||||||
if account != nil {
|
sessionData, err = model.NewSessionData(nil, nil)
|
||||||
accountId = &account.Id
|
if err != nil {
|
||||||
settings = account.Settings
|
return "", nil, fmt.Errorf("failed to generate new session data: %w", err)
|
||||||
} else if settingsData != nil {
|
}
|
||||||
settings = settingsData
|
}
|
||||||
|
data, err := json.Marshal(sessionData)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, fmt.Errorf("failed to marshal session data: %w", err)
|
||||||
}
|
}
|
||||||
if _, err := db.Exec(
|
if _, err := db.Exec(
|
||||||
`INSERT INTO sessions(id, account_id, settings)
|
`INSERT INTO sessions(id, data)
|
||||||
VALUES (?, ?, ?);`,
|
VALUES (?, jsonb(?));`,
|
||||||
sessionHash,
|
sessionHash,
|
||||||
accountId,
|
data,
|
||||||
settings,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return "", fmt.Errorf("failed insert new session in database: %w", err)
|
return "", nil, fmt.Errorf("failed insert new session in database: %w", err)
|
||||||
}
|
}
|
||||||
return sessionId, nil
|
return sessionId, &model.Session{
|
||||||
|
Id: sessionHash,
|
||||||
|
Data: sessionData,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) DeleteExpiredSessions() error {
|
func (db *DB) DeleteExpiredSessions() error {
|
||||||
|
@ -66,38 +71,44 @@ func (db *DB) LoadSessionById(id string) (*model.Session, error) {
|
||||||
var (
|
var (
|
||||||
created int64
|
created int64
|
||||||
updated int64
|
updated int64
|
||||||
|
data []byte
|
||||||
)
|
)
|
||||||
err = db.QueryRow(
|
err = db.QueryRow(
|
||||||
`SELECT account_id,
|
`SELECT created,
|
||||||
created,
|
|
||||||
updated,
|
updated,
|
||||||
settings
|
json_extract(data, '$')
|
||||||
FROM sessions
|
FROM sessions
|
||||||
WHERE id = ?;`,
|
WHERE id = ?;`,
|
||||||
sessionHash,
|
sessionHash,
|
||||||
).Scan(&session.AccountId,
|
).Scan(
|
||||||
&created,
|
&created,
|
||||||
&updated,
|
&updated,
|
||||||
&session.Settings,
|
&data)
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("failed to load session by id %s: %w", id, err)
|
return nil, fmt.Errorf("failed to load session by id %s: %w", id, err)
|
||||||
}
|
}
|
||||||
|
if err := json.Unmarshal(data, &session.Data); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal session data: %w", err)
|
||||||
|
}
|
||||||
session.Created = time.Unix(created, 0)
|
session.Created = time.Unix(created, 0)
|
||||||
session.Updated = time.Unix(updated, 0)
|
session.Updated = time.Unix(updated, 0)
|
||||||
return &session, nil
|
return &session, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) MigrateSession(session *model.Session, account *model.Account) (string, error) {
|
func (db *DB) MigrateSession(session *model.Session, account *model.Account) (string, *model.Session, error) {
|
||||||
if err := db.DeleteSession(session); err != nil {
|
if err := db.DeleteSession(session); err != nil {
|
||||||
return "", fmt.Errorf("failed to delete session: %w", err)
|
return "", nil, fmt.Errorf("failed to delete session: %w", err)
|
||||||
}
|
}
|
||||||
sessionId, err := db.CreateSession(account, session.Settings)
|
sessionData, err := model.NewSessionData(account, session.Data.Settings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to create session: %w", err)
|
return "", nil, fmt.Errorf("failed to generate new session data: %w", err)
|
||||||
}
|
}
|
||||||
return sessionId, nil
|
sessionId, session, err := db.CreateSession(sessionData)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, fmt.Errorf("failed to create session: %w", err)
|
||||||
|
}
|
||||||
|
return sessionId, session, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,15 +13,15 @@ CREATE TABLE accounts (
|
||||||
settings BLOB NOT NULL,
|
settings BLOB NOT NULL,
|
||||||
password_reset TEXT
|
password_reset TEXT
|
||||||
) STRICT;
|
) STRICT;
|
||||||
CREATE UNIQUE INDEX accounts_username on accounts(username);
|
CREATE UNIQUE INDEX accounts_username ON accounts(username);
|
||||||
|
|
||||||
CREATE TABLE sessions (
|
CREATE TABLE sessions (
|
||||||
id BLOB PRIMARY KEY,
|
id BLOB PRIMARY KEY,
|
||||||
account_id TEXT,
|
|
||||||
created INTEGER NOT NULL DEFAULT (unixepoch()),
|
created INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||||
updated INTEGER NOT NULL DEFAULT (unixepoch()),
|
updated INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||||
settings BLOB NOT NULL
|
data BLOB NOT NULL
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
CREATE INDEX sessions_data_account_id ON sessions(data->'account'->>'id');
|
||||||
|
|
||||||
CREATE TABLE states (
|
CREATE TABLE states (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
|
@ -30,7 +30,7 @@ CREATE TABLE states (
|
||||||
created INTEGER DEFAULT (unixepoch()),
|
created INTEGER DEFAULT (unixepoch()),
|
||||||
updated INTEGER DEFAULT (unixepoch())
|
updated INTEGER DEFAULT (unixepoch())
|
||||||
) STRICT;
|
) STRICT;
|
||||||
CREATE UNIQUE INDEX states_path on states(path);
|
CREATE UNIQUE INDEX states_path ON states(path);
|
||||||
|
|
||||||
CREATE TABLE versions (
|
CREATE TABLE versions (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
|
|
|
@ -2,7 +2,6 @@ package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/json"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.adyxax.org/adyxax/tfstated/pkg/helpers"
|
"git.adyxax.org/adyxax/tfstated/pkg/helpers"
|
||||||
|
@ -12,15 +11,15 @@ import (
|
||||||
type AccountContextKey struct{}
|
type AccountContextKey struct{}
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
Id uuid.UUID
|
Id uuid.UUID `json:"id"`
|
||||||
Username string
|
Username string `json:"username"`
|
||||||
Salt []byte
|
Salt []byte `json:"salt"`
|
||||||
PasswordHash []byte
|
PasswordHash []byte `json:"password_hash"`
|
||||||
IsAdmin bool
|
IsAdmin bool `json:"is_admin"`
|
||||||
Created time.Time
|
Created time.Time `json:"created"`
|
||||||
LastLogin time.Time
|
LastLogin time.Time `json:"last_login"`
|
||||||
Settings json.RawMessage
|
Settings *Settings `json:"settings"`
|
||||||
PasswordReset *uuid.UUID
|
PasswordReset *uuid.UUID `json:"password_reset"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (account *Account) CheckPassword(password string) bool {
|
func (account *Account) CheckPassword(password string) bool {
|
||||||
|
|
|
@ -1,20 +1,40 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.n16f.net/uuid"
|
"go.n16f.net/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SessionData struct {
|
||||||
|
Account *Account `json:"account"`
|
||||||
|
CsrfToken uuid.UUID `json:"csrf_token"`
|
||||||
|
Settings *Settings `json:"settings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSessionData(account *Account, previousSessionSettings *Settings) (*SessionData, error) {
|
||||||
|
data := SessionData{Account: account}
|
||||||
|
if err := data.CsrfToken.Generate(uuid.V4); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate csrf token uuid: %w", err)
|
||||||
|
}
|
||||||
|
if account != nil {
|
||||||
|
data.Settings = account.Settings
|
||||||
|
} else if previousSessionSettings != nil {
|
||||||
|
data.Settings = previousSessionSettings
|
||||||
|
} else {
|
||||||
|
data.Settings = &Settings{}
|
||||||
|
}
|
||||||
|
return &data, nil
|
||||||
|
}
|
||||||
|
|
||||||
type SessionContextKey struct{}
|
type SessionContextKey struct{}
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
Id []byte
|
Id []byte
|
||||||
AccountId *uuid.UUID
|
Created time.Time
|
||||||
Created time.Time
|
Updated time.Time
|
||||||
Updated time.Time
|
Data *SessionData
|
||||||
Settings json.RawMessage
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (session *Session) IsExpired() bool {
|
func (session *Session) IsExpired() bool {
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
type SettingsContextKey struct{}
|
|
||||||
|
|
||||||
type Settings struct {
|
type Settings struct {
|
||||||
LightMode bool `json:"light_mode"`
|
LightMode bool `json:"light_mode"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import (
|
||||||
|
|
||||||
type AccountsPage struct {
|
type AccountsPage struct {
|
||||||
Accounts []model.Account
|
Accounts []model.Account
|
||||||
ActiveTab int
|
|
||||||
IsAdmin string
|
IsAdmin string
|
||||||
Page *Page
|
Page *Page
|
||||||
Username string
|
Username string
|
||||||
|
@ -45,11 +44,10 @@ func handleAccountsPOST(db *database.DB) http.Handler {
|
||||||
accountUsername := r.FormValue("username")
|
accountUsername := r.FormValue("username")
|
||||||
isAdmin := r.FormValue("isAdmin")
|
isAdmin := r.FormValue("isAdmin")
|
||||||
page := AccountsPage{
|
page := AccountsPage{
|
||||||
ActiveTab: 1,
|
Page: makePage(r, &Page{Title: "New Account", Section: "accounts"}),
|
||||||
Page: makePage(r, &Page{Title: "New Account", Section: "accounts"}),
|
Accounts: accounts,
|
||||||
Accounts: accounts,
|
IsAdmin: isAdmin,
|
||||||
IsAdmin: isAdmin,
|
Username: accountUsername,
|
||||||
Username: accountUsername,
|
|
||||||
}
|
}
|
||||||
if ok := validUsername.MatchString(accountUsername); !ok {
|
if ok := validUsername.MatchString(accountUsername); !ok {
|
||||||
page.UsernameInvalid = true
|
page.UsernameInvalid = true
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package webui
|
package webui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
@ -35,7 +36,7 @@ func handleAccountsIdGET(db *database.DB) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if account == nil {
|
if account == nil {
|
||||||
errorResponse(w, r, http.StatusNotFound, err)
|
errorResponse(w, r, http.StatusNotFound, fmt.Errorf("The account Id could not be found."))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
statePaths, err := db.LoadStatePaths()
|
statePaths, err := db.LoadStatePaths()
|
||||||
|
|
|
@ -19,37 +19,36 @@ type AccountsIdResetPasswordPage struct {
|
||||||
|
|
||||||
var accountsIdResetPasswordTemplates = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/accountsIdResetPassword.html"))
|
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) {
|
func processAccountsIdResetPasswordPathValues(db *database.DB, w http.ResponseWriter, r *http.Request) *model.Account {
|
||||||
var accountId uuid.UUID
|
var accountId uuid.UUID
|
||||||
if err := accountId.Parse(r.PathValue("id")); err != nil {
|
if err := accountId.Parse(r.PathValue("id")); err != nil {
|
||||||
errorResponse(w, r, http.StatusBadRequest, err)
|
return nil
|
||||||
return nil, false
|
|
||||||
}
|
}
|
||||||
var token uuid.UUID
|
var token uuid.UUID
|
||||||
if err := token.Parse(r.PathValue("token")); err != nil {
|
if err := token.Parse(r.PathValue("token")); err != nil {
|
||||||
errorResponse(w, r, http.StatusBadRequest, err)
|
errorResponse(w, r, http.StatusBadRequest, err)
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
account, err := db.LoadAccountById(&accountId)
|
account, err := db.LoadAccountById(&accountId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorResponse(w, r, http.StatusInternalServerError, err)
|
errorResponse(w, r, http.StatusInternalServerError, err)
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
if account == nil || account.PasswordReset == nil {
|
if account == nil || account.PasswordReset == nil {
|
||||||
errorResponse(w, r, http.StatusBadRequest, err)
|
errorResponse(w, r, http.StatusBadRequest, err)
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
if !account.PasswordReset.Equal(token) {
|
if !account.PasswordReset.Equal(token) {
|
||||||
errorResponse(w, r, http.StatusBadRequest, err)
|
errorResponse(w, r, http.StatusBadRequest, err)
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
return account, true
|
return account
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleAccountsIdResetPasswordGET(db *database.DB) http.Handler {
|
func handleAccountsIdResetPasswordGET(db *database.DB) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
account, valid := processAccountsIdResetPasswordPathValues(db, w, r)
|
account := processAccountsIdResetPasswordPathValues(db, w, r)
|
||||||
if !valid {
|
if account == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
render(w, accountsIdResetPasswordTemplates, http.StatusOK,
|
render(w, accountsIdResetPasswordTemplates, http.StatusOK,
|
||||||
|
@ -63,8 +62,8 @@ func handleAccountsIdResetPasswordGET(db *database.DB) http.Handler {
|
||||||
|
|
||||||
func handleAccountsIdResetPasswordPOST(db *database.DB) http.Handler {
|
func handleAccountsIdResetPasswordPOST(db *database.DB) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
account, valid := processAccountsIdResetPasswordPathValues(db, w, r)
|
account := processAccountsIdResetPasswordPathValues(db, w, r)
|
||||||
if !valid {
|
if account == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
password := r.FormValue("password")
|
password := r.FormValue("password")
|
||||||
|
|
|
@ -4,21 +4,14 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.adyxax.org/adyxax/tfstated/pkg/database"
|
|
||||||
"git.adyxax.org/adyxax/tfstated/pkg/model"
|
"git.adyxax.org/adyxax/tfstated/pkg/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func adminMiddleware(db *database.DB, requireLogin func(http.Handler) http.Handler) func(http.Handler) http.Handler {
|
func adminMiddleware(requireLogin func(http.Handler) http.Handler) func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return requireLogin(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return requireLogin(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
account := r.Context().Value(model.AccountContextKey{})
|
session := r.Context().Value(model.SessionContextKey{}).(*model.Session)
|
||||||
if account == nil {
|
if !session.Data.Account.IsAdmin {
|
||||||
// this could happen if the account was deleted in the short
|
|
||||||
// time between retrieving the session and here
|
|
||||||
http.Redirect(w, r, "/login", http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !account.(*model.Account).IsAdmin {
|
|
||||||
errorResponse(w, r, http.StatusForbidden, fmt.Errorf("Only administrators can perform this request."))
|
errorResponse(w, r, http.StatusForbidden, fmt.Errorf("Only administrators can perform this request."))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
Use this page to inspect user accounts.
|
Use this page to inspect user accounts.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{{ if .Page.IsAdmin }}
|
{{ if .Page.Session.Data.Account.IsAdmin }}
|
||||||
<form action="/accounts" enctype="multipart/form-data" method="post">
|
<form action="/accounts" enctype="multipart/form-data" method="post">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>New User Account</legend>
|
<legend>New User Account</legend>
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
{{ if .Account.IsAdmin }}
|
{{ 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.IsAdmin }}
|
{{ if .Page.Session.Data.Account.IsAdmin }}
|
||||||
<h2>Operations</h2>
|
<h2>Operations</h2>
|
||||||
<form action="/accounts/{{ .Account.Id }}" enctype="multipart/form-data" method="post">
|
<form action="/accounts/{{ .Account.Id }}" enctype="multipart/form-data" method="post">
|
||||||
<div class="flex-row">
|
<div class="flex-row">
|
||||||
|
@ -39,7 +39,7 @@
|
||||||
value="{{ .Username }}">
|
value="{{ .Username }}">
|
||||||
<label for="is-admin">Is Admin</label>
|
<label for="is-admin">Is Admin</label>
|
||||||
<input {{ if .Account.IsAdmin }}checked{{ end }}
|
<input {{ if .Account.IsAdmin }}checked{{ end }}
|
||||||
{{ if eq .Page.AccountId.String .Account.Id.String }}disabled{{ end }}
|
{{ if eq .Page.Session.Data.Account.Id.String .Account.Id.String }}disabled{{ end }}
|
||||||
id="is-admin"
|
id="is-admin"
|
||||||
name="is-admin"
|
name="is-admin"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
@ -63,13 +63,13 @@
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Danger Zone</legend>
|
<legend>Danger Zone</legend>
|
||||||
<button {{ if eq .Page.AccountId.String .Account.Id.String }}disabled{{ end }}
|
<button {{ if eq .Page.Session.Data.Account.Id.String .Account.Id.String }}disabled{{ end }}
|
||||||
type="submit"
|
type="submit"
|
||||||
value="delete">
|
value="delete">
|
||||||
Delete User Account
|
Delete User Account
|
||||||
</button>
|
</button>
|
||||||
<!--<button type="submit" value="lock">Lock User Account</button>-->
|
<!--<button type="submit" value="lock">Lock User Account</button>-->
|
||||||
<button {{ if or (ne .Account.PasswordReset nil) (eq .Page.AccountId.String .Account.Id.String) }}disabled{{ end }}
|
<button {{ if or (ne .Account.PasswordReset nil) (eq .Page.Session.Data.Account.Id.String .Account.Id.String) }}disabled{{ end }}
|
||||||
type="submit"
|
type="submit"
|
||||||
value="reset-password">
|
value="reset-password">
|
||||||
Reset Password
|
Reset Password
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" />
|
||||||
<title>TFSTATED - {{ .Page.Title }}</title>
|
<title>TFSTATED - {{ .Page.Title }}</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="{{ if .Page.LightMode }}light-theme{{ else }}black-theme{{ end }}">
|
<body class="{{ if .Page.Session.Data.Settings.LightMode }}light-theme{{ else }}black-theme{{ end }}">
|
||||||
<header>
|
<header>
|
||||||
<h6>TFSTATED</h6>
|
<h6>TFSTATED</h6>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<legend>Account Settings</legend>
|
<legend>Account Settings</legend>
|
||||||
<div style="align-items:center; display:grid; grid-template-columns:1fr 1fr;">
|
<div style="align-items:center; display:grid; grid-template-columns:1fr 1fr;">
|
||||||
<label for="dark-mode">Dark mode</label>
|
<label for="dark-mode">Dark mode</label>
|
||||||
<input {{ if not .Settings.LightMode }}checked{{ end }}
|
<input {{ if not .Page.Session.Data.Settings.LightMode }}checked{{ end }}
|
||||||
id="dark-mode"
|
id="dark-mode"
|
||||||
name="dark-mode"
|
name="dark-mode"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|
|
@ -5,26 +5,16 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.adyxax.org/adyxax/tfstated/pkg/model"
|
"git.adyxax.org/adyxax/tfstated/pkg/model"
|
||||||
"go.n16f.net/uuid"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Page struct {
|
type Page struct {
|
||||||
AccountId *uuid.UUID
|
Section string
|
||||||
IsAdmin bool
|
Session *model.Session
|
||||||
LightMode bool
|
Title string
|
||||||
Section string
|
|
||||||
Title string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func makePage(r *http.Request, page *Page) *Page {
|
func makePage(r *http.Request, page *Page) *Page {
|
||||||
accountCtx := r.Context().Value(model.AccountContextKey{})
|
page.Session = r.Context().Value(model.SessionContextKey{}).(*model.Session)
|
||||||
if accountCtx != nil {
|
|
||||||
account := accountCtx.(*model.Account)
|
|
||||||
page.AccountId = &account.Id
|
|
||||||
page.IsAdmin = account.IsAdmin
|
|
||||||
}
|
|
||||||
settings := r.Context().Value(model.SettingsContextKey{}).(*model.Settings)
|
|
||||||
page.LightMode = settings.LightMode
|
|
||||||
return page
|
return page
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,8 +26,8 @@ func handleLoginGET() http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Cache-Control", "no-store, no-cache")
|
w.Header().Set("Cache-Control", "no-store, no-cache")
|
||||||
|
|
||||||
account := r.Context().Value(model.AccountContextKey{})
|
session := r.Context().Value(model.SessionContextKey{}).(*model.Session)
|
||||||
if account != nil {
|
if session.Data.Account != nil {
|
||||||
http.Redirect(w, r, "/states", http.StatusFound)
|
http.Redirect(w, r, "/states", http.StatusFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ func handleLoginPOST(db *database.DB) http.Handler {
|
||||||
password := r.FormValue("password")
|
password := r.FormValue("password")
|
||||||
|
|
||||||
if username == "" || password == "" {
|
if username == "" || password == "" {
|
||||||
errorResponse(w, r, http.StatusBadRequest, nil)
|
errorResponse(w, r, http.StatusBadRequest, fmt.Errorf("Invalid username or password"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ok := validUsername.MatchString(username); !ok {
|
if ok := validUsername.MatchString(username); !ok {
|
||||||
|
@ -79,41 +79,31 @@ func handleLoginPOST(db *database.DB) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
session := r.Context().Value(model.SessionContextKey{}).(*model.Session)
|
session := r.Context().Value(model.SessionContextKey{}).(*model.Session)
|
||||||
sessionId, err := db.MigrateSession(session, account)
|
sessionId, session, err := db.MigrateSession(session, account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorResponse(w, r, http.StatusInternalServerError,
|
errorResponse(w, r, http.StatusInternalServerError,
|
||||||
fmt.Errorf("failed to migrate session: %w", err))
|
fmt.Errorf("failed to migrate session: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setSessionCookie(w, sessionId)
|
setSessionCookie(w, sessionId)
|
||||||
|
ctx := context.WithValue(r.Context(), model.SessionContextKey{}, session)
|
||||||
if err := db.DeleteExpiredSessions(); err != nil {
|
if err := db.DeleteExpiredSessions(); err != nil {
|
||||||
slog.Error("failed to delete expired sessions after user login", "err", err, "accountId", account.Id)
|
slog.Error("failed to delete expired sessions after user login", "err", err, "accountId", account.Id)
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, "/", http.StatusFound)
|
http.Redirect(w, r.WithContext(ctx), "/", http.StatusFound)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func loginMiddleware(db *database.DB, requireSession func(http.Handler) http.Handler) func(http.Handler) http.Handler {
|
func loginMiddleware(requireSession func(http.Handler) http.Handler) func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return requireSession(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return requireSession(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Cache-Control", "no-store, no-cache")
|
w.Header().Set("Cache-Control", "no-store, no-cache")
|
||||||
session := r.Context().Value(model.SessionContextKey{}).(*model.Session)
|
session := r.Context().Value(model.SessionContextKey{}).(*model.Session)
|
||||||
if session.AccountId == nil {
|
if session.Data.Account == nil {
|
||||||
http.Redirect(w, r, "/login", http.StatusFound)
|
http.Redirect(w, r, "/login", http.StatusFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
account, err := db.LoadAccountById(session.AccountId)
|
next.ServeHTTP(w, r)
|
||||||
if err != nil {
|
|
||||||
errorResponse(w, r, http.StatusInternalServerError,
|
|
||||||
fmt.Errorf("failed to load account by Id: %w", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if account == nil {
|
|
||||||
http.Redirect(w, r, "/login", http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx := context.WithValue(r.Context(), model.AccountContextKey{}, account)
|
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package webui
|
package webui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -17,15 +18,16 @@ func handleLogoutGET(db *database.DB) http.Handler {
|
||||||
}
|
}
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session := r.Context().Value(model.SessionContextKey{}).(*model.Session)
|
session := r.Context().Value(model.SessionContextKey{}).(*model.Session)
|
||||||
sessionId, err := db.MigrateSession(session, nil)
|
sessionId, session, err := db.MigrateSession(session, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorResponse(w, r, http.StatusInternalServerError,
|
errorResponse(w, r, http.StatusInternalServerError,
|
||||||
fmt.Errorf("failed to migrate session: %w", err))
|
fmt.Errorf("failed to migrate session: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setSessionCookie(w, sessionId)
|
setSessionCookie(w, sessionId)
|
||||||
|
ctx := context.WithValue(r.Context(), model.SessionContextKey{}, session)
|
||||||
render(w, logoutTemplate, http.StatusOK, logoutPage{
|
render(w, logoutTemplate, http.StatusOK, logoutPage{
|
||||||
Page: makePage(r, &Page{Title: "Logout", Section: "login"}),
|
Page: makePage(r.WithContext(ctx), &Page{Title: "Logout", Section: "login"}),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,8 @@ func addRoutes(
|
||||||
db *database.DB,
|
db *database.DB,
|
||||||
) {
|
) {
|
||||||
requireSession := sessionsMiddleware(db)
|
requireSession := sessionsMiddleware(db)
|
||||||
requireLogin := loginMiddleware(db, requireSession)
|
requireLogin := loginMiddleware(requireSession)
|
||||||
requireAdmin := adminMiddleware(db, requireLogin)
|
requireAdmin := adminMiddleware(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}", requireSession(handleAccountsIdResetPasswordGET(db)))
|
mux.Handle("GET /accounts/{id}/reset/{token}", requireSession(handleAccountsIdResetPasswordGET(db)))
|
||||||
|
|
|
@ -2,10 +2,8 @@ package webui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.adyxax.org/adyxax/tfstated/pkg/database"
|
"git.adyxax.org/adyxax/tfstated/pkg/database"
|
||||||
|
@ -40,26 +38,20 @@ func sessionsMiddleware(db *database.DB) func(http.Handler) http.Handler {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx := context.WithValue(r.Context(), model.SessionContextKey{}, session)
|
ctx := context.WithValue(r.Context(), model.SessionContextKey{}, session)
|
||||||
var settings model.Settings
|
|
||||||
if err := json.Unmarshal(session.Settings, &settings); err != nil {
|
|
||||||
slog.Error("failed to unmarshal session settings", "err", err)
|
|
||||||
}
|
|
||||||
ctx = context.WithValue(ctx, model.SettingsContextKey{}, &settings)
|
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sessionId, err := db.CreateSession(nil, nil)
|
sessionId, session, err := db.CreateSession(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorResponse(w, r, http.StatusInternalServerError,
|
errorResponse(w, r, http.StatusInternalServerError,
|
||||||
fmt.Errorf("failed to create session: %w", err))
|
fmt.Errorf("failed to create session: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setSessionCookie(w, sessionId)
|
setSessionCookie(w, sessionId)
|
||||||
var settings model.Settings
|
ctx := context.WithValue(r.Context(), model.SessionContextKey{}, session)
|
||||||
ctx := context.WithValue(r.Context(), model.SettingsContextKey{}, &settings)
|
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,10 +18,8 @@ var settingsTemplates = template.Must(template.ParseFS(htmlFS, "html/base.html",
|
||||||
|
|
||||||
func handleSettingsGET(db *database.DB) http.Handler {
|
func handleSettingsGET(db *database.DB) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
settings := r.Context().Value(model.SettingsContextKey{}).(*model.Settings)
|
|
||||||
render(w, settingsTemplates, http.StatusOK, SettingsPage{
|
render(w, settingsTemplates, http.StatusOK, SettingsPage{
|
||||||
Page: makePage(r, &Page{Title: "Settings", Section: "settings"}),
|
Page: makePage(r, &Page{Title: "Settings", Section: "settings"}),
|
||||||
Settings: settings,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -36,16 +34,16 @@ func handleSettingsPOST(db *database.DB) http.Handler {
|
||||||
settings := model.Settings{
|
settings := model.Settings{
|
||||||
LightMode: darkMode != "1",
|
LightMode: darkMode != "1",
|
||||||
}
|
}
|
||||||
account := r.Context().Value(model.AccountContextKey{}).(*model.Account)
|
session := r.Context().Value(model.SessionContextKey{}).(*model.Session)
|
||||||
err := db.SaveAccountSettings(account, &settings)
|
session.Data.Settings = &settings
|
||||||
|
err := db.SaveAccountSettings(session.Data.Account, &settings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorResponse(w, r, http.StatusInternalServerError, err)
|
errorResponse(w, r, http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx := context.WithValue(r.Context(), model.SettingsContextKey{}, &settings)
|
ctx := context.WithValue(r.Context(), model.SessionContextKey{}, session)
|
||||||
page := makePage(r.WithContext(ctx), &Page{Title: "Settings", Section: "settings"})
|
|
||||||
render(w, settingsTemplates, http.StatusOK, SettingsPage{
|
render(w, settingsTemplates, http.StatusOK, SettingsPage{
|
||||||
Page: page,
|
Page: makePage(r.WithContext(ctx), &Page{Title: "Settings", Section: "settings"}),
|
||||||
Settings: &settings,
|
Settings: &settings,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue