Files
HealthLog/src/lib/auth/admin-roster.ts
T
Marc-André Bombeck b37e4f5df7 fix(admin): the last-admin rule counts only admins who can sign in
Demoting an admin counted every admin, pending deletion included, so after
one admin deleted their account the other could step down and leave the
instance with no admin who can sign in or undo the deletion. Both the
demotion and an admin's own deletion now count the other admins not pending
deletion, inside the transaction that makes the change, under one roster
lock, so two at the same moment cannot each count the other.
2026-10-25 17:02:32 +01:00

38 lines
1.3 KiB
TypeScript

/**
* The rule that an instance keeps an admin who can sign in.
*
* Two changes can take the last one away: an admin demoted, and an admin
* deleting their own account. Both count the other admins who are not pending
* deletion (one pending deletion cannot sign in and is erased when the window
* ends) under one transaction-scoped lock, so two of them at the same moment
* cannot each count the other and leave nobody.
*/
import type { Prisma } from "@/generated/prisma/client";
import { ACTIVE_ACCOUNT } from "@/lib/account-deletion/active-account";
/** Thrown inside the transaction to roll the change back. */
export class LastAdminError extends Error {
constructor() {
super("the instance would be left without an admin who can sign in");
this.name = "LastAdminError";
}
}
/**
* Hold the admin roster for the rest of `tx`, then throw `LastAdminError`
* unless an admin other than `leavingId` stays, not pending deletion.
*/
export async function assertAnotherAdminStays(
tx: Prisma.TransactionClient,
leavingId: string,
): Promise<void> {
await tx.$queryRaw`
SELECT 1 AS locked
FROM pg_advisory_xact_lock(hashtextextended('admin-roster', 0))
`;
const others = await tx.user.count({
where: { role: "ADMIN", id: { not: leavingId }, ...ACTIVE_ACCOUNT },
});
if (others === 0) throw new LastAdminError();
}