94 lines · 4.2 KB
import { all, batch, first, stmt } from "./db";
import { badRequest, conflict, forbidden, notFoundError } from "./errors";
import { canAdministerAccount, loadRepoById } from "./repos";
import { deleteRepository } from "./repo-ops";
import { deleteRunnerRow } from "./ci/runner-registration";
import { disableSite } from "./sites/site";
import { forgetSite, siteDb, sitesOwnedBy } from "./sites/store";
import { accountDb } from "./scoped-db";
import { dropAccountState } from "./account-scope";
import { cancelDomainAddon } from "./billing/domain-addon";
import type { Viewer } from "~/lib/types";
import { assertMayDeleteOrg } from "./verified-writes";

export const MAX_ORG_REPOS_DELETED = 100;

/**
 * Deletes an organization. Owners only, with the org name typed as confirmation. Refuses while repositories
 * remain unless `deleteRepos`, in which case each goes through `deleteRepository`. The org's current and
 * previous names are retired so they can never be re-registered (GIT-9); everything keyed to the account
 * (memberships, runners, registration tokens, secrets, sites, projects, LFS usage) cascades with the row.
 */
export async function deleteOrganization(input: {
  org: string;
  confirm: string;
  deleteRepos: boolean;
  viewer: Viewer;
}): Promise<{ deletedRepos: string[] }> {
  const org = await first<{ id: string; name: string; disabled_at: number | null }>(
    "SELECT id, name, disabled_at FROM accounts WHERE name = ? AND kind = 'org'",
    input.org,
  );
  if (!org) throw notFoundError("Organization not found");
  if (org.disabled_at && !input.viewer.isAdmin) throw forbidden("This organization is disabled and can't be deleted");
  await assertMayDeleteOrg(org.id, input.viewer);
  if (!(await canAdministerAccount(org.id, input.viewer))) {
    const member = await first(
      "SELECT 1 AS ok FROM org_members WHERE org_id = ? AND user_id = ?",
      org.id,
      input.viewer.id,
    );