90 lines · 3.6 KB
import { env } from "cloudflare:workers";
import { all, chunks, first, placeholders } from "../db";
import { accountDb } from "../scoped-db";
import { notFoundError } from "../errors";
import { hostingSuspendedKey } from "./metering";
import { purgeAccountHostingCache } from "./cache-purge";

export type HostingSuspension = { accountId: string; name: string; suspendedAt: number };

async function accountByName(name: string): Promise<{ id: string; name: string }> {
  const account = await first<{ id: string; name: string }>(
    "SELECT id, name FROM accounts WHERE name = ? COLLATE NOCASE",
    name,
  );
  if (!account) throw notFoundError("No such account");
  return account;
}

/** Operator action: stops every Site of the account until resumed; plan changes leave it set. */
export async function suspendHosting(name: string): Promise<string> {
  const { id } = await accountByName(name);
  await suspendHostingFor(id);
  return id;
}

export async function suspendHostingFor(accountId: string): Promise<void> {
  const now = Date.now();
  await accountDb(accountId).run(
    `INSERT INTO account_billing (account_id, hosting_suspended_at, updated_at) VALUES (?1, ?2, ?2)
     ON CONFLICT (account_id) DO UPDATE SET hosting_suspended_at = excluded.hosting_suspended_at, updated_at = excluded.updated_at`,
    accountId,
    now,
  );
  await env.KV.put(hostingSuspendedKey(accountId), String(now), { metadata: { suspendedAt: now } });
  // KV reads are edge-cached for up to a minute; purging the CDN at least stops cached assets from outliving that.
  await purgeAccountHostingCache(accountId).catch((error: unknown) =>
    console.warn("Hosting cache purge failed", accountId, error),
  );
}