51 lines · 2.4 KB
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;