Files
HIPCTF2/backend/src/database/migrations/1700000000800-AddUserLastAdminTriggers.ts
T

58 lines
2.0 KiB
TypeScript

import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Deployment-wide safety net for the "always retain at least one admin"
* invariant. Even if a future code path bypasses
* `UsersService.applyLastAdminSafeMutation`, the database itself aborts
* the offending UPDATE/DELETE with a clear `LAST_ADMIN` error message.
*
* Triggers fire before the row mutation, so the offending statement never
* commits. The service-level wrapper still rolls back with 409 LAST_ADMIN
* in normal operation; the triggers are the belt-and-braces guard for
* cross-instance / out-of-band mistakes.
*
* Forward-only.
*/
export class AddUserLastAdminTriggers1700000000800 implements MigrationInterface {
name = 'AddUserLastAdminTriggers1700000000800';
public async up(queryRunner: QueryRunner): Promise<void> {
// UPDATE trigger: blocks any role mutation that would leave the system
// with zero admins. We use WHEN to short-circuit so the trigger body
// only fires when (a) the old role was admin, (b) the new role is no
// longer admin, and (c) no other admin row exists.
await queryRunner.query(`
CREATE TRIGGER IF NOT EXISTS "trg_user_last_admin_update"
BEFORE UPDATE OF "role" ON "user"
FOR EACH ROW
WHEN OLD."role" = 'admin' AND NEW."role" <> 'admin'
AND NOT EXISTS (
SELECT 1 FROM "user" AS "other"
WHERE "other"."role" = 'admin' AND "other"."id" <> OLD."id"
)
BEGIN
SELECT RAISE(ABORT, 'LAST_ADMIN');
END;
`);
// DELETE trigger: blocks deletion of the only remaining admin row.
await queryRunner.query(`
CREATE TRIGGER IF NOT EXISTS "trg_user_last_admin_delete"
BEFORE DELETE ON "user"
FOR EACH ROW
WHEN OLD."role" = 'admin'
AND NOT EXISTS (
SELECT 1 FROM "user" AS "other"
WHERE "other"."role" = 'admin' AND "other"."id" <> OLD."id"
)
BEGIN
SELECT RAISE(ABORT, 'LAST_ADMIN');
END;
`);
}
public async down(): Promise<void> {
// Forward-only.
}
}