chore(tfstated): change database account id format to uuidv7

This commit is contained in:
Julien Dessaux 2025-01-31 20:53:29 +01:00
parent 98c7d6f578
commit ab548d249b
Signed by: adyxax
GPG key ID: F92E51B86E07177E
8 changed files with 27 additions and 20 deletions

View file

@ -25,6 +25,10 @@ func (db *DB) InitAdminAccount() error {
return fmt.Errorf("failed to select if there is an admin account in the database: %w", err) return fmt.Errorf("failed to select if there is an admin account in the database: %w", err)
} }
if !hasAdminAccount { if !hasAdminAccount {
var accountId uuid.UUID
if err := accountId.Generate(uuid.V7); err != nil {
return fmt.Errorf("failed to generate account id: %w", err)
}
var password uuid.UUID var password uuid.UUID
if err := password.Generate(uuid.V4); err != nil { if err := password.Generate(uuid.V4); err != nil {
return fmt.Errorf("failed to generate initial admin password: %w", err) return fmt.Errorf("failed to generate initial admin password: %w", err)
@ -32,13 +36,14 @@ func (db *DB) InitAdminAccount() error {
salt := helpers.GenerateSalt() salt := helpers.GenerateSalt()
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(username, salt, password_hash, is_admin, settings) `INSERT INTO accounts(id, username, salt, password_hash, is_admin, settings)
VALUES ("admin", :salt, :hash, TRUE, :settings) VALUES (:id, "admin", :salt, :hash, TRUE, :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("salt", salt), sql.Named("id", accountId),
sql.Named("hash", hash), sql.Named("hash", hash),
[]byte("{}"), sql.Named("salt", salt),
sql.Named("settings", []byte("{}")),
); err == nil { ); err == nil {
AdvertiseAdminPassword(password.String()) AdvertiseAdminPassword(password.String())
} else { } else {
@ -49,17 +54,17 @@ func (db *DB) InitAdminAccount() error {
}) })
} }
func (db *DB) LoadAccountUsernames() (map[int]string, error) { func (db *DB) LoadAccountUsernames() (map[string]string, error) {
rows, err := db.Query( rows, err := db.Query(
`SELECT id, username FROM accounts;`) `SELECT id, username 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)
} }
defer rows.Close() defer rows.Close()
accounts := make(map[int]string) accounts := make(map[string]string)
for rows.Next() { for rows.Next() {
var ( var (
id int id string
username string username string
) )
err = rows.Scan(&id, &username) err = rows.Scan(&id, &username)
@ -74,7 +79,7 @@ func (db *DB) LoadAccountUsernames() (map[int]string, error) {
return accounts, nil return accounts, nil
} }
func (db *DB) LoadAccountById(id int) (*model.Account, error) { func (db *DB) LoadAccountById(id string) (*model.Account, error) {
account := model.Account{ account := model.Account{
Id: id, Id: id,
} }
@ -99,7 +104,7 @@ func (db *DB) LoadAccountById(id int) (*model.Account, error) {
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 account by id %d: %w", id, err) return nil, fmt.Errorf("failed to load account by id %s: %w", id, 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)

View file

@ -3,7 +3,7 @@ CREATE TABLE schema_version (
) STRICT; ) STRICT;
CREATE TABLE accounts ( CREATE TABLE accounts (
id INTEGER PRIMARY KEY, id TEXT PRIMARY KEY,
username TEXT NOT NULL, username TEXT NOT NULL,
salt BLOB NOT NULL, salt BLOB NOT NULL,
password_hash BLOB NOT NULL, password_hash BLOB NOT NULL,
@ -16,7 +16,7 @@ CREATE UNIQUE INDEX accounts_username on accounts(username);
CREATE TABLE sessions ( CREATE TABLE sessions (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
account_id INTEGER NOT NULL, account_id TEXT NOT NULL,
created INTEGER NOT NULL DEFAULT (unixepoch()), created INTEGER NOT NULL DEFAULT (unixepoch()),
updated INTEGER NOT NULL DEFAULT (unixepoch()), updated INTEGER NOT NULL DEFAULT (unixepoch()),
data TEXT NOT NULL, data TEXT NOT NULL,
@ -34,7 +34,7 @@ CREATE UNIQUE INDEX states_path on states(path);
CREATE TABLE versions ( CREATE TABLE versions (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
account_id INTEGER NOT NULL, account_id TEXT NOT NULL,
state_id INTEGER, state_id INTEGER,
data BLOB, data BLOB,
lock TEXT, lock TEXT,

View file

@ -60,7 +60,7 @@ func (db *DB) LoadStateById(stateId int) (*model.State, error) {
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 state id %s from database: %w", stateId, err) return nil, fmt.Errorf("failed to load state id %d from database: %w", stateId, err)
} }
state.Created = time.Unix(created, 0) state.Created = time.Unix(created, 0)
state.Updated = time.Unix(updated, 0) state.Updated = time.Unix(updated, 0)
@ -96,7 +96,7 @@ func (db *DB) LoadStates() ([]model.State, error) {
} }
// returns true in case of id mismatch // returns true in case of id mismatch
func (db *DB) SetState(path string, accountID int, data []byte, lockID string) (bool, error) { func (db *DB) SetState(path string, accountID string, data []byte, lockID string) (bool, error) {
encryptedData, err := db.dataEncryptionKey.EncryptAES256(data) encryptedData, err := db.dataEncryptionKey.EncryptAES256(data)
if err != nil { if err != nil {
return false, fmt.Errorf("failed to encrypt state data: %w", err) return false, fmt.Errorf("failed to encrypt state data: %w", err)

View file

@ -11,7 +11,7 @@ import (
type AccountContextKey struct{} type AccountContextKey struct{}
type Account struct { type Account struct {
Id int Id string
Username string Username string
Salt []byte Salt []byte
PasswordHash []byte PasswordHash []byte

View file

@ -8,7 +8,7 @@ type SessionContextKey struct{}
type Session struct { type Session struct {
Id string Id string
AccountId int AccountId string
Created time.Time Created time.Time
Updated time.Time Updated time.Time
Data any Data any

View file

@ -1,13 +1,14 @@
package model package model
import ( import (
"encoding/json"
"time" "time"
) )
type Version struct { type Version struct {
AccountId int AccountId string
Created time.Time Created time.Time
Data []byte Data json.RawMessage
Id int Id int
Lock *string Lock *string
StateId int StateId int

View file

@ -3,7 +3,8 @@
<form action="/login" method="post"> <form action="/login" method="post">
<fieldset> <fieldset>
<div class="field border label{{ if .Forbidden }} invalid{{ end}}"> <div class="field border label{{ if .Forbidden }} invalid{{ end}}">
<input id="username" <input autofocus
id="username"
name="username" name="username"
type="text" type="text"
value="{{ .Username }}" value="{{ .Username }}"

View file

@ -15,7 +15,7 @@ func handleStateGET(db *database.DB) http.Handler {
type StatesData struct { type StatesData struct {
Page *Page Page *Page
State *model.State State *model.State
Usernames map[int]string Usernames map[string]string
Versions []model.Version Versions []model.Version
} }
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {