import type Database from 'better-sqlite3'; export function migrateAuthSchema(handle: Database.Database): void { handle.exec(` CREATE TABLE IF NOT EXISTS auth_users ( id TEXT PRIMARY KEY, username TEXT NOT NULL COLLATE NOCASE UNIQUE, display_name TEXT NOT NULL, password_hash TEXT NOT NULL, role TEXT NOT NULL CHECK(role IN ('admin','operator','viewer')), disabled INTEGER NOT NULL DEFAULT 0, must_change_password INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, last_login_at INTEGER ); CREATE TABLE IF NOT EXISTS auth_sessions ( token_hash TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES auth_users(id) ON DELETE CASCADE, created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, last_seen_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS auth_sessions_user ON auth_sessions(user_id); CREATE TABLE IF NOT EXISTS auth_rate_limits ( key TEXT PRIMARY KEY, window_start INTEGER NOT NULL, attempts INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS auth_audit ( id INTEGER PRIMARY KEY AUTOINCREMENT, actor_id TEXT, action TEXT NOT NULL, target_id TEXT, details TEXT NOT NULL, created_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS auth_audit_created_at ON auth_audit(created_at); `); } export function migrateRememberedSessions(handle: Database.Database): void { const columns = handle.pragma('table_info(auth_sessions)') as { name: string }[]; if (columns.some((column) => column.name === 'remembered')) return; handle.exec(` ALTER TABLE auth_sessions ADD COLUMN remembered INTEGER NOT NULL DEFAULT 0 CHECK(remembered IN (0,1)); `); } /** Run with foreign keys disabled by the migration runner before its transaction. */ export function migrateSuperViewerRole(handle: Database.Database): void { const schema = handle .prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='auth_users'") .get() as { sql: string }; if (schema.sql.includes("'super_viewer'")) return; if (handle.pragma('foreign_keys', { simple: true })) throw new Error('The auth role migration requires foreign keys to be disabled'); // Rebuild the parent table without renaming it first, so session references // keep pointing to auth_users and existing sessions are not cascade-deleted. handle.exec(` CREATE TABLE auth_users_super_viewer ( id TEXT PRIMARY KEY, username TEXT NOT NULL COLLATE NOCASE UNIQUE, display_name TEXT NOT NULL, password_hash TEXT NOT NULL, role TEXT NOT NULL CHECK(role IN ('admin','super_viewer','operator','viewer')), disabled INTEGER NOT NULL DEFAULT 0, must_change_password INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, last_login_at INTEGER ); INSERT INTO auth_users_super_viewer ( id,username,display_name,password_hash,role,disabled,must_change_password, created_at,updated_at,last_login_at ) SELECT id,username,display_name,password_hash,role,disabled,must_change_password, created_at,updated_at,last_login_at FROM auth_users; DROP TABLE auth_users; ALTER TABLE auth_users_super_viewer RENAME TO auth_users; `); } /** Run with foreign keys disabled by the migration runner before its transaction. */ export function migrateModeratorRole(handle: Database.Database): void { const schema = handle .prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='auth_users'") .get() as { sql: string }; if (schema.sql.includes("'moderator'")) return; if (handle.pragma('foreign_keys', { simple: true })) throw new Error('The auth role migration requires foreign keys to be disabled'); // Rebuild without renaming the parent: existing session references must stay // attached to auth_users, including remembered sessions added after v17. handle.exec(` CREATE TABLE auth_users_moderator ( id TEXT PRIMARY KEY, username TEXT NOT NULL COLLATE NOCASE UNIQUE, display_name TEXT NOT NULL, password_hash TEXT NOT NULL, role TEXT NOT NULL CHECK(role IN ('admin','super_viewer','operator','moderator','viewer')), disabled INTEGER NOT NULL DEFAULT 0, must_change_password INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, last_login_at INTEGER ); INSERT INTO auth_users_moderator ( id,username,display_name,password_hash,role,disabled,must_change_password, created_at,updated_at,last_login_at ) SELECT id,username,display_name,password_hash,role,disabled,must_change_password, created_at,updated_at,last_login_at FROM auth_users; DROP TABLE auth_users; ALTER TABLE auth_users_moderator RENAME TO auth_users; `); }