/** * FIX-14: Physical separation of backoffice users (admin/editor) from * storefront customers (identity_users). Backoffice users get their own * table, their own sessions, and their own auth cookie/session mechanism. * * Customers (role = 'customer') remain in identity_users. * Admin/editor users are COPIED into backoffice_users (preserving password * hash and mfa_enrolled flag) and then REMOVED from identity_users so the * tables do not overlap at all. */ /** @param {import('node-pg-migrate'). MigrationBuilder} pgm */ export const up = (pgm) => { // 1) Create the new backoffice schema namespace (table names: backoffice_*) pgm.sql(` CREATE TABLE backoffice_users ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), email citext NOT NULL UNIQUE, password_hash text NOT NULL, role text NOT NULL DEFAULT 'admin' CHECK (role IN ('admin', 'editor')), mfa_enrolled boolean NOT NULL DEFAULT false, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ) `); pgm.sql(` CREATE TABLE backoffice_sessions ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES backoffice_users(id) ON DELETE CASCADE, token_hash text NOT NULL UNIQUE, created_at timestamptz NOT NULL DEFAULT now(), expires_at timestamptz NOT NULL, revoked_at timestamptz ) `); pgm.sql('CREATE INDEX backoffice_sessions_user_id_idx ON backoffice_sessions (user_id)'); // 2) Migrate admin/editor rows from identity_users → backoffice_users. // Preserve id and password_hash so existing sessions/hashes survive. pgm.sql(` INSERT INTO backoffice_users (id, email, password_hash, role, mfa_enrolled, created_at, updated_at) SELECT id, email, password_hash, CASE WHEN role = 'editor' THEN 'editor' ELSE 'admin' END, COALESCE(mfa_enrolled, false), created_at, updated_at FROM identity_users WHERE role IN ('admin', 'editor') ON CONFLICT (id) DO NOTHING `); // 3) Remove admin/editor rows from identity_users so the tables are disjoint. pgm.sql(`DELETE FROM identity_users WHERE role IN ('admin', 'editor')`); }; /** @param {import('node-pg-migrate'). MigrationBuilder} pgm */ export const down = (pgm) => { // Recreate the dropped rows in identity_users so the schema is reversible // for the dev environment (best-effort — email uniqueness may collide if // a customer signed up with the same email after the migration). pgm.sql(` INSERT INTO identity_users (id, email, password_hash, role, created_at, updated_at) SELECT id, email, password_hash, CASE WHEN role = 'editor' THEN 'admin' ELSE role END, created_at, updated_at FROM backoffice_users ON CONFLICT (id) DO NOTHING `); pgm.sql('DROP TABLE IF EXISTS backoffice_sessions'); pgm.sql('DROP TABLE IF EXISTS backoffice_users'); };