129 lines
5.1 KiB
SQL
129 lines
5.1 KiB
SQL
CREATE TABLE IF NOT EXISTS admins (
|
|
id TEXT PRIMARY KEY,
|
|
username TEXT NOT NULL,
|
|
username_norm TEXT NOT NULL UNIQUE,
|
|
display_name TEXT NOT NULL,
|
|
password_hash TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','disabled')),
|
|
must_change_password INTEGER NOT NULL DEFAULT 1 CHECK(must_change_password IN (0,1)),
|
|
auth_version INTEGER NOT NULL DEFAULT 1 CHECK(auth_version >= 1),
|
|
version INTEGER NOT NULL DEFAULT 1 CHECK(version >= 1),
|
|
created_at INTEGER NOT NULL,
|
|
created_by TEXT REFERENCES admins(id) ON DELETE RESTRICT,
|
|
password_changed_at INTEGER,
|
|
last_login_at INTEGER,
|
|
disabled_at INTEGER,
|
|
disabled_by TEXT REFERENCES admins(id) ON DELETE RESTRICT
|
|
) STRICT;
|
|
CREATE UNIQUE INDEX IF NOT EXISTS admins_username_norm_uq ON admins(username_norm);
|
|
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
token_hash TEXT PRIMARY KEY,
|
|
admin_id TEXT NOT NULL REFERENCES admins(id) ON DELETE CASCADE,
|
|
csrf_hash TEXT NOT NULL,
|
|
auth_version INTEGER NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
last_seen_at INTEGER NOT NULL,
|
|
idle_expires_at INTEGER NOT NULL,
|
|
absolute_expires_at INTEGER NOT NULL
|
|
) STRICT;
|
|
CREATE INDEX IF NOT EXISTS sessions_admin_idx ON sessions(admin_id);
|
|
CREATE INDEX IF NOT EXISTS sessions_expiry_idx ON sessions(idle_expires_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS expenses (
|
|
id TEXT PRIMARY KEY,
|
|
paid_at INTEGER NOT NULL,
|
|
amount_cents INTEGER NOT NULL CHECK(amount_cents > 0 AND amount_cents <= 999999999999),
|
|
note TEXT NOT NULL DEFAULT '',
|
|
status TEXT NOT NULL DEFAULT 'unreimbursed' CHECK(status IN ('unreimbursed','reimbursed')),
|
|
version INTEGER NOT NULL DEFAULT 1 CHECK(version >= 1),
|
|
created_at INTEGER NOT NULL,
|
|
created_by TEXT NOT NULL REFERENCES admins(id) ON DELETE RESTRICT,
|
|
updated_at INTEGER NOT NULL,
|
|
updated_by TEXT NOT NULL REFERENCES admins(id) ON DELETE RESTRICT,
|
|
reimbursed_at INTEGER,
|
|
reimbursed_by TEXT REFERENCES admins(id) ON DELETE RESTRICT,
|
|
deleted_at INTEGER,
|
|
deleted_by TEXT REFERENCES admins(id) ON DELETE RESTRICT
|
|
) STRICT;
|
|
CREATE INDEX IF NOT EXISTS expenses_list_idx ON expenses(deleted_at, status, paid_at DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS attachments (
|
|
id TEXT PRIMARY KEY,
|
|
expense_id TEXT NOT NULL REFERENCES expenses(id) ON DELETE CASCADE,
|
|
kind TEXT NOT NULL CHECK(kind IN ('payment_proof','invoice')),
|
|
storage_path TEXT NOT NULL UNIQUE,
|
|
original_name TEXT NOT NULL,
|
|
mime_type TEXT NOT NULL,
|
|
size_bytes INTEGER NOT NULL CHECK(size_bytes > 0),
|
|
sha256 TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
created_by TEXT NOT NULL REFERENCES admins(id) ON DELETE RESTRICT
|
|
) STRICT;
|
|
CREATE INDEX IF NOT EXISTS attachments_expense_idx ON attachments(expense_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS audit_events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
occurred_at INTEGER NOT NULL,
|
|
request_id TEXT NOT NULL,
|
|
actor_admin_id TEXT,
|
|
actor_username TEXT,
|
|
action TEXT NOT NULL,
|
|
target_type TEXT NOT NULL,
|
|
target_id TEXT,
|
|
outcome TEXT NOT NULL CHECK(outcome IN ('success','denied','failure')),
|
|
before_json TEXT CHECK(before_json IS NULL OR json_valid(before_json)),
|
|
after_json TEXT CHECK(after_json IS NULL OR json_valid(after_json)),
|
|
metadata_json TEXT CHECK(metadata_json IS NULL OR json_valid(metadata_json))
|
|
) STRICT;
|
|
CREATE INDEX IF NOT EXISTS audit_time_idx ON audit_events(occurred_at DESC);
|
|
CREATE INDEX IF NOT EXISTS audit_target_idx ON audit_events(target_type, target_id, occurred_at DESC);
|
|
CREATE TRIGGER IF NOT EXISTS audit_events_no_update BEFORE UPDATE ON audit_events
|
|
BEGIN SELECT RAISE(ABORT, 'audit_events are append-only'); END;
|
|
CREATE TRIGGER IF NOT EXISTS audit_events_no_delete BEFORE DELETE ON audit_events
|
|
BEGIN SELECT RAISE(ABORT, 'audit_events are append-only'); END;
|
|
|
|
CREATE TABLE IF NOT EXISTS system_settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
) STRICT;
|
|
|
|
CREATE TABLE IF NOT EXISTS export_jobs (
|
|
id TEXT PRIMARY KEY,
|
|
admin_id TEXT NOT NULL REFERENCES admins(id) ON DELETE CASCADE,
|
|
session_hash TEXT NOT NULL,
|
|
status TEXT NOT NULL CHECK(status IN ('queued','building','ready','failed','expired')),
|
|
selection_json TEXT NOT NULL CHECK(json_valid(selection_json)),
|
|
snapshot_json TEXT NOT NULL CHECK(json_valid(snapshot_json)),
|
|
file_path TEXT,
|
|
file_name TEXT NOT NULL,
|
|
size_bytes INTEGER,
|
|
sha256 TEXT,
|
|
error_message TEXT,
|
|
created_at INTEGER NOT NULL,
|
|
ready_at INTEGER,
|
|
expires_at INTEGER NOT NULL
|
|
) STRICT;
|
|
CREATE INDEX IF NOT EXISTS exports_expiry_idx ON export_jobs(expires_at);
|
|
CREATE INDEX IF NOT EXISTS exports_session_idx ON export_jobs(session_hash);
|
|
|
|
CREATE TABLE IF NOT EXISTS login_attempts (
|
|
key_hash TEXT PRIMARY KEY,
|
|
window_start INTEGER NOT NULL,
|
|
failures INTEGER NOT NULL,
|
|
blocked_until INTEGER
|
|
) STRICT;
|
|
|
|
CREATE TABLE IF NOT EXISTS file_deletions (
|
|
id TEXT PRIMARY KEY,
|
|
storage_path TEXT NOT NULL,
|
|
reason TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','complete','failed')),
|
|
attempts INTEGER NOT NULL DEFAULT 0,
|
|
last_error TEXT,
|
|
created_at INTEGER NOT NULL,
|
|
completed_at INTEGER
|
|
) STRICT;
|
|
CREATE INDEX IF NOT EXISTS file_deletions_status_idx ON file_deletions(status);
|