93 lines
5.0 KiB
TypeScript
93 lines
5.0 KiB
TypeScript
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;
|
|
`);
|
|
}
|