86 lines
3.4 KiB
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
|