297 lines · 11.4 KB
import { all, batch, first, run, stmt } from "./db";
import { randomToken, sha256Hex } from "./crypto";
import { emailTemplates, queueEmail } from "./email";
import { badRequest } from "./errors";
import { publicUrl } from "./events/emit";
import { WAITLIST_INVITE_HOURS } from "~/lib/waitlist";

const CAP_KEY = "signup_cap";
const INVITE_TTL_MS = WAITLIST_INVITE_HOURS * 60 * 60 * 1000;
/** Invites sent per advance; a cron or the next cap change picks up the rest. */
const INVITES_PER_RUN = 100;
export const MAX_SIGNUP_CAP = 1_000_000;

/** Seats held by people invited off the waitlist whose invite hasn't lapsed. */
const HELD_SEATS = "(SELECT COUNT(*) FROM signup_waitlist WHERE invite_expires_at > ? AND bypass_cap = 0)";
const USERS = "(SELECT COUNT(*) FROM users)";

/** The admin-set cap on user accounts, or null for no cap. */
export async function signupCap(): Promise<number | null> {
  const row = await first<{ value: string }>("SELECT value FROM instance_settings WHERE key = ?", CAP_KEY);
  const cap = row ? Number(row.value) : Number.NaN;
  return Number.isInteger(cap) && cap >= 0 ? cap : null;
}

export async function setSignupCap(cap: number | null): Promise<void> {
  if (cap === null) {
    await run("DELETE FROM instance_settings WHERE key = ?", CAP_KEY);
  } else {
    await run(
      `INSERT INTO instance_settings (key, value, updated_at) VALUES (?, ?, ?)
       ON CONFLICT (key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,
      CAP_KEY,
      String(cap),
      Date.now(),
    );
  }
  await advanceWaitlist();
}

export type SignupSeat =