48 lines · 1.9 KB
import { first } from "./db";
import { HttpError } from "./errors";
import {
  EMAIL_UNVERIFIED,
  EMAIL_UNVERIFIED_MESSAGE,
  VERIFY_EMAIL_PATH,
  canWriteAsViewer,
} from "~/lib/email-verification";
import type { Viewer } from "~/lib/types";

/**
 * The one gate for writing and paying (AUTH-B1). It passes for a verified primary email, a linked GitHub
 * account, site admins and instances without email (all folded into `viewer.emailVerified` by toViewer, the
 * same rule as metering's TRUSTED_USER). Account self-service (profile, emails, password, 2FA, sessions,
 * deletion) and starring/following stay open so the holder can always fix or leave the account.
 */
export function assertVerifiedForWrites(viewer: Pick<Viewer, "isAdmin" | "emailVerified">): void {
  if (canWriteAsViewer(viewer)) return;
  throw new HttpError(403, EMAIL_UNVERIFIED_MESSAGE, { code: EMAIL_UNVERIFIED, verifyPath: VERIFY_EMAIL_PATH });
}

/** Deleting a repository is self-service only for its owning user; deleting one as a collaborator or org admin is a write. */
export function assertMayDeleteRepo(
  repo: { owner_id: string },
  viewer: Pick<Viewer, "id" | "isAdmin" | "emailVerified">,
): void {
  if (repo.owner_id !== viewer.id) assertVerifiedForWrites(viewer);
}

/** Deleting an organization is self-service only when the viewer is its sole member. */
export async function assertMayDeleteOrg(
  orgId: string,
  viewer: Pick<Viewer, "id" | "isAdmin" | "emailVerified">,
): Promise<void> {
  if (canWriteAsViewer(viewer)) return;
  const others = await first<{ ok: number }>(
    "SELECT 1 AS ok FROM org_members WHERE org_id = ? AND user_id != ? LIMIT 1",
    orgId,
    viewer.id,
  );