55 lines
2.3 KiB
SQL
55 lines
2.3 KiB
SQL
-- ═══════════════════════════════════════════════════════════════
|
|
-- 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);
|