NGU-Web/server/src/migrations/009_superadmin.sql

86 lines
3.4 KiB
SQL

-- ═══════════════════════════════════════════════════════════════
-- 009_superadmin.sql
--
-- Adds a third role above 'admin'. A CHECK constraint can't be
-- altered in place, so the table is rebuilt — the recipe from the
-- SQLite docs, in the order it has to happen.
--
-- Foreign keys are OFF for the duration on purpose. `sessions`
-- references admin_users(id), and:
--
-- * with FKs ON, DROP TABLE admin_users fires the ON DELETE
-- CASCADE and empties `sessions` — everyone signed out;
-- * with FKs ON, the RENAME afterwards tries to rewrite the
-- REFERENCES clause in `sessions` and fails, because the table
-- it points at no longer exists.
--
-- With them OFF neither happens: `sessions` keeps pointing at the
-- name "admin_users", which the rename puts back underneath it.
--
-- ⚠ PRAGMA foreign_keys is a no-op inside a transaction. If the
-- migration runner wraps each file in BEGIN/COMMIT, this file will
-- appear to work and then fail at the rename. Check the runner
-- before applying, or run this one by hand:
--
-- sudo systemctl stop ngu-api
-- sudo sqlite3 /var/lib/ngu/ngu.db < 009_superadmin.sql
-- sudo systemctl start ngu-api
--
-- Verify after:
--
-- PRAGMA user_version; -- 9
-- PRAGMA foreign_key_check; -- no rows
-- SELECT email, role FROM admin_users;
-- ═══════════════════════════════════════════════════════════════
PRAGMA foreign_keys = OFF;
CREATE TABLE admin_users_new (
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,
-- Listed low to high. The application treats these as a ladder,
-- not a set: 'superadmin' passes every check 'admin' passes.
-- The default stays 'admin' — a new account should never arrive
-- at the top of the ladder by accident.
role TEXT NOT NULL DEFAULT 'admin'
CHECK (role IN ('viewer', 'editor', 'admin', 'superadmin')),
is_active INTEGER NOT NULL DEFAULT 1 CHECK (is_active IN (0, 1)),
last_login_at TEXT
) STRICT;
-- Columns listed explicitly rather than SELECT *, so this breaks
-- loudly if the old shape isn't what this file assumes.
INSERT INTO admin_users_new
(id, created_at, email, name, password_hash, google_sub,
role, is_active, last_login_at)
SELECT
id, created_at, email, name, password_hash, google_sub,
role, is_active, last_login_at
FROM admin_users;
DROP TABLE admin_users;
ALTER TABLE admin_users_new RENAME TO admin_users;
-- Informational: prints offending rows and returns nothing if the
-- rebuild left the graph intact.
PRAGMA foreign_key_check;
PRAGMA foreign_keys = ON;
PRAGMA user_version = 9; -- ← set to this migration's number