import { createServerFn } from "@tanstack/react-start"; import { env } from "cloudflare:workers"; import { unreadNotifications } from "~/server/user-inbox"; import { enforceRateLimit, requestIp } from "~/server/limits"; import { z } from "zod"; import { viewerMiddleware } from "./middleware"; import { createSession, destroySession, disabledSessionNotice, loadUserByLogin } from "~/server/auth"; import { verifyPassword, verifyPasswordAgainstNobody } from "~/server/crypto"; import { createUser, githubOAuthEnabled, signupsEnabled } from "~/server/accounts"; import { badRequest } from "~/server/errors"; import { rehashIfStale, sendEmailVerification } from "~/server/account-security"; import { requiresSecondFactor, startPendingLogin } from "~/server/two-factor"; import { accountName, email, password } from "~/lib/validation"; import { requireTurnstile, turnstileSiteKey } from "~/server/turnstile"; import { clientAnalyticsConfig } from "~/server/analytics"; import { recordTermsAcceptance } from "~/server/legal"; import { assertAccountActive } from "~/server/account-disable"; import { isSupporter } from "~/server/supporter"; const turnstileToken = z.string().max(2048).optional(); export const getSession = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .handler(async ({ context }) => { const [unread, supporter] = context.viewer ? await Promise.all([unreadNotifications(context.viewer.id), isSupporter(context.viewer.id)]) : [0, false]; const disabled = context.viewer ? null : await disabledSessionNotice(); return { viewer: context.viewer, supporter, unreadNotifications: unread, analytics: clientAnalyticsConfig(), disabled, publicUrl: env.PUBLIC_URL, }; }); export const getAuthOptions = createServerFn({ method: "GET" }).handler(async () => { return { signupsEnabled: signupsEnabled(), githubEnabled: githubOAuthEnabled(), turnstileSiteKey: turnstileSiteKey(), }; }); export const signUp = createServerFn({ method: "POST" }) .validator( z.object({ username: accountName, email, password, turnstileToken, bootstrapToken: z.string().max(200).optional(), inviteToken: z.string().max(200).optional(), acceptTerms: z.literal(true, { message: "Accept the Terms of Service and Privacy Policy to continue" }), }), ) .handler(async ({ data }) => { await enforceRateLimit("AUTH_LIMITER", `signup:${requestIp()}`); await requireTurnstile(data.turnstileToken, "signup"); const id = await createUser({ name: data.username, email: data.email, password: data.password, githubId: null, displayName: null, bootstrapToken: data.bootstrapToken ?? null, inviteToken: data.inviteToken ?? null, }); await recordTermsAcceptance(id); await createSession(id); await sendEmailVerification(id, data.email); return { ok: true }; }); export const signIn = createServerFn({ method: "POST" }) .validator(z.object({ login: z.string().trim().min(1), password: z.string().min(1), turnstileToken })) .handler(async ({ data }) => { const login = data.login.toLowerCase(); await enforceRateLimit("AUTH_LIMITER", `login:${requestIp()}`); await requireTurnstile(data.turnstileToken, "login"); await enforceRateLimit("AUTH_LIMITER", `login:${login}:${requestIp()}`); // AUTH-A3: an account-level budget too, so spraying one account from many IPs still hits a ceiling. await enforceRateLimit("AUTH_LIMITER", `login:acct:${login}`); const user = await loadUserByLogin(data.login); // AUTH-P6: username and email are both logins, so the account budget is also keyed by the resolved user. if (user) await enforceRateLimit("AUTH_LIMITER", `login:user:${user.id}`); // AUTH-A5: unknown logins burn the same PBKDF2 work as a wrong password, so timing does not enumerate accounts. const ok = user?.password_hash ? await verifyPassword(data.password, user.password_hash) : await verifyPasswordAgainstNobody(data.password); if (!user?.password_hash || !ok) { throw badRequest("Incorrect username or password"); } await rehashIfStale(user.id, data.password, user.password_hash); await assertAccountActive(user.id); if (await requiresSecondFactor(user.id)) { await startPendingLogin(user.id); return { ok: true, twoFactor: true }; } await createSession(user.id); return { ok: true, twoFactor: false }; }); export const signOut = createServerFn({ method: "POST" }).handler(async () => { await destroySession(); return { ok: true }; });