import { all, first, run } from "./db";
import { accountDb } from "./scoped-db";
import { badRequest, conflict, notFoundError } from "./errors";
import { revokeDeployTokensCreatedBy } from "./access-revocation";
import { backfillDone } from "./backfill";
import { DEPLOY_TOKEN_INDEX } from "./deploy-token-index";
import { userChanged } from "./user-row";
export type SiteAdmin = { id: string; name: string };
export async function listSiteAdmins(): Promise<SiteAdmin[]> {
return all<SiteAdmin>(
"SELECT a.id, a.name FROM users u JOIN accounts a ON a.id = u.account_id WHERE u.is_admin = 1 ORDER BY a.name",
);
}
/**
* Grants or removes site admin. Demotion also revokes deploy tokens the user minted on repositories they only
* administered as a site admin (`revokeDeployTokensCreatedBy` keeps the ones they still administer another way),
* which is why admins must be changed here rather than in the database. The last admin can't be removed.
*/
export async function setSiteAdmin(name: string, admin: boolean): Promise<string> {
const user = await first<{ id: string; is_admin: number; email_verified_at: number | null }>(
"SELECT a.id, u.is_admin, u.email_verified_at FROM accounts a JOIN users u ON u.account_id = a.id WHERE a.name = ?",
name,
);
if (!user) throw notFoundError("User not found");
if (admin) {
if (user.email_verified_at === null) throw badRequest("Only users with a verified email can be site admins");
await run("UPDATE users SET is_admin = 1 WHERE account_id = ?", user.id);
} else {
const demoted = await run(
"UPDATE users SET is_admin = 0 WHERE account_id = ?1 AND EXISTS (SELECT 1 FROM users WHERE is_admin = 1 AND account_id != ?1)",
user.id,
);
if (demoted.meta.changes === 0 && user.is_admin === 1) throw conflict("The last site admin can't be removed");
}
await userChanged(user.id);
if (!admin && user.is_admin === 1) await revokeDeployTokensCreatedBy(user.id, await mintedRepoIds(user.id));
return user.id;