import { createServerFn } from "@tanstack/react-start"; import { viewerMiddleware } from "./middleware"; import { all, first } from "~/server/db"; import { accountDb } from "~/server/scoped-db"; import { periodOf } from "~/server/metering/record"; import { billingSettings, planBlock, withinSpendingLimit } from "~/server/metering/quota"; import { stripeEnabled } from "~/server/billing/config"; import { secondFactorMethods } from "~/server/two-factor"; import { meterDef } from "~/lib/meters"; import type { MeterId } from "~/lib/meters"; import { rateLabel } from "~/lib/pricing-display"; import { disabledNotice, disabledOrgsOf } from "~/server/account-disable"; import { canWriteAsViewer } from "~/lib/email-verification"; const MAXED = 100; const MAX_NOTICES = 4; export type QuotaNotice = { kind: "quota"; id: string; accountName: string; accountKind: "user" | "org"; billingPath: string; meter: string; meterLabel: string; rate: string | null; period: string; plan: string; /** Spending-limit notices name the limit, not a meter. */ spend: boolean; canUpgrade: boolean; }; export type SecurityNotice = { kind: "mfa"; id: string }; /** A verified account with no live personal access token cannot push or use the API yet. */ export type TokenNotice = { kind: "tokens"; id: string }; export type DisabledOrgNotice = { kind: "disabled"; id: string; accountName: string; message: string; }; export type AccountNotice = QuotaNotice | SecurityNotice | DisabledOrgNotice | TokenNotice; /** * Nags for the signed-in user: any account they can pay for (themselves, orgs they own) that maxed out an * allowance or its spending limit this period and is still blocked, and a reminder to set up two-factor. The * cron records each 100% crossing in usage_alerts, so this is one read for all accounts plus a headroom check * only for those that crossed. */ export const getAccountNotices = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .handler(async ({ context }): Promise => { const viewer = context.viewer; if (!viewer) return []; const period = periodOf(); const accounts = await all<{ id: string; name: string; kind: "user" | "org"; }>( `SELECT id, name, kind FROM accounts WHERE id = ?1 UNION SELECT a.id, a.name, a.kind FROM org_members m JOIN accounts a ON a.id = m.org_id WHERE m.user_id = ?1 AND m.role = 'owner'`, viewer.id, ); const notices: AccountNotice[] = (await disabledOrgsOf(viewer.id)).map((org) => ({ kind: "disabled", id: `disabled:${org.id}`, accountName: org.name, message: disabledNotice(org.reason).message, })); // Two-factor goes first so a run of quota notices can never push it out. const mfa = await secondFactorMethods(viewer.id); const hasPassword = await first<{ one: number }>( "SELECT 1 AS one FROM users WHERE account_id = ? AND password_hash IS NOT NULL", viewer.id, ); if (!mfa.totp && !mfa.passkey && hasPassword) notices.push({ kind: "mfa", id: "mfa" }); if (canWriteAsViewer(viewer)) { const liveToken = await accountDb(viewer.id).first<{ one: number }>( "SELECT 1 AS one FROM access_tokens WHERE user_id = ? AND (expires_at IS NULL OR expires_at > ?) LIMIT 1", viewer.id, Date.now(), ); if (!liveToken) notices.push({ kind: "tokens", id: "tokens" }); } const alerts = ( await Promise.all( accounts.map((account) => accountDb(account.id).all<{ account_id: string; subject: string }>( "SELECT DISTINCT account_id, subject FROM usage_alerts WHERE period = ? AND threshold >= ? AND account_id = ?", period, MAXED, account.id, ), ), ) ).flat(); const billing = stripeEnabled(); for (const account of accounts) { if (notices.length >= MAX_NOTICES) break; const subjects = alerts.filter((alert) => alert.account_id === account.id).map((alert) => alert.subject); if (subjects.length === 0) continue; const settings = await billingSettings(account.id); for (const subject of subjects) { const spend = subject === "spend"; // Alerts record the crossing; drop it once the account has room again (upgraded, credit, higher limit). const stillBlocked = spend ? !(await withinSpendingLimit(account.id)) : meterDef(subject) !== null && (await planBlock(account.id, subject as MeterId)) !== null; if (!stillBlocked) continue; notices.push({ kind: "quota", id: `quota:${account.id}:${period}:${subject}`, accountName: account.name, accountKind: account.kind, billingPath: account.kind === "org" ? `/orgs/${encodeURIComponent(account.name)}/billing` : "/settings/billing", meter: subject, meterLabel: spend ? "spending limit" : (meterDef(subject)?.label ?? subject), rate: spend ? null : rateLabel(subject), period, plan: settings.plan.label, spend, canUpgrade: billing && !settings.plan.billsOverage, }); } } return notices.slice(0, MAX_NOTICES); });