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
 | 
				
			||||||
	Settings  json.RawMessage
 | 
						Data    *SessionData
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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,7 +44,6 @@ 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,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
					 | 
				
			||||||
	IsAdmin   bool
 | 
					 | 
				
			||||||
	LightMode bool
 | 
					 | 
				
			||||||
	Section string
 | 
						Section string
 | 
				
			||||||
 | 
						Session *model.Session
 | 
				
			||||||
	Title   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