v1.4 - added admin page and auth

This commit is contained in:
Zaldimmar 2026-09-25 02:36:49 -05:00
parent 5efdafbb97
commit 1f0aa3078f
29 changed files with 5264 additions and 217 deletions

View file

@ -0,0 +1,55 @@
-- ═══════════════════════════════════════════════════════════════
-- 003 AUTHENTICATION
--
-- Two tables: who may sign in, and who currently is signed in.
--
-- There is no self-signup and no registration endpoint. Accounts
-- are created from the CLI, on the box, by someone with shell
-- access. For a handful of staff that's the right trade: no
-- invite flow, no email delivery, no password-reset surface for
-- anyone to attack.
-- ═══════════════════════════════════════════════════════════════
CREATE TABLE admin_users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
-- Stored lowercased. The application lowercases on every read
-- and write, so the UNIQUE index is genuinely case-insensitive
-- without depending on a collation.
email TEXT NOT NULL UNIQUE,
name TEXT,
-- Nullable so a Google-only account can exist later with no
-- password at all. A row with both can use either route in.
password_hash TEXT,
-- Google's stable subject id. Nullable, unique when present —
-- SQLite allows any number of NULLs in a unique index.
google_sub TEXT UNIQUE,
role TEXT NOT NULL DEFAULT 'admin'
CHECK (role IN ('admin', 'viewer')),
is_active INTEGER NOT NULL DEFAULT 1 CHECK (is_active IN (0, 1)),
last_login_at TEXT
) STRICT;
-- One row per active login. The cookie holds a random token; this
-- table holds only its SHA-256, so a database leak doesn't hand
-- anyone a working session.
CREATE TABLE sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token_hash TEXT NOT NULL UNIQUE,
user_id INTEGER NOT NULL REFERENCES admin_users(id) ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
expires_at TEXT NOT NULL,
user_agent TEXT,
ip_hash TEXT
) STRICT;
CREATE INDEX sessions_user_idx ON sessions (user_id);
CREATE INDEX sessions_expiry_idx ON sessions (expires_at);