import { createServerFn } from "@tanstack/react-start"; import { unreadNotifications } from "~/server/user-inbox"; import { enforceRateLimit, requestIp } from "~/server/limits"; import { z } from "zod"; import { viewerMiddleware } from "./middleware"; import { createSession, destroySession, 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"; export const getSession = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .handler(async ({ context }) => { const unread = context.viewer ? await unreadNotifications(context.viewer.id) : 0; return { viewer: context.viewer, unreadNotifications: unread }; }); export const getAuthOptions = createServerFn({ method: "GET" }).handler(async () => { return { signupsEnabled: signupsEnabled(), githubEnabled: githubOAuthEnabled() }; }); export const signUp = createServerFn({ method: "POST" }) .validator(z.object({ username: accountName, email, password })) .handler(async ({ data }) => { await enforceRateLimit("AUTH_LIMITER", `signup:${requestIp()}`); const id = await createUser({ name: data.username, email: data.email, password: data.password, githubId: null, displayName: null, }); 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) })) .handler(async ({ data }) => { const login = data.login.toLowerCase(); await enforceRateLimit("AUTH_LIMITER", `login:${requestIp()}`); 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-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); 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 }; });