import { getRequest } from "@tanstack/react-start/server"; import { requestAuthPolicy } from "./federation"; import { gateIdentityEnabled } from "./gate-identity.server"; import { auth, authConfigured, authMode, authRefusal } from "./server"; /** * Server-side session resolution (server-only). * * Because this app runs its OWN Better Auth at same-origin `/api/auth/*`, the * session cookie is sent with every request to this app — server functions AND * SSR loaders included. So we resolve the user straight from the request cookies * via `auth.api.getSession` (no client-minted JWT needed). Never trust a * client-supplied user id — only the result of this verification. */ /** True when a real database is configured server-side. */ const databaseConfigured = Boolean(process.env.DATABASE_URL?.trim()); /** Re-export so callers can branch on it without importing `server.ts`. */ export { authConfigured, authRefusal }; /** How this request is authenticated (see `requestAuthPolicy` in `./federation`). */ function policy() { return requestAuthPolicy(authMode, { gateIdentity: gateIdentityEnabled(), databaseConfigured, }); } if (databaseConfigured && authMode === "disabled") { console.error( "[auth] DATABASE_URL is set but auth is disabled (VITE_AUTH_ENABLED=false) " + "— requireUserId() will reject every request (fail closed) rather than " + "share one dev user on a real database.", ); } /** Dev fallback user id, used only when auth is disabled (VITE_AUTH_ENABLED=false). */ export const DEV_USER_ID = "dev-user"; /** * Thrown by `requireUserId` when the caller has no valid session. Carries * `status: 401`; the message is a stable contract — match * `err.message === "Unauthorized"` client-side to send the visitor to sign-in. */ export class UnauthorizedError extends Error { readonly status = 401; constructor() { super("Unauthorized"); this.name = "UnauthorizedError"; } } /** * Thrown by `requireUserId` when federated sign-in was refused at startup (a * deployment without its own broker client; see `./federation`). Carries * `status: 503`: the server is misconfigured, so the visitor is not sent to a * sign-in that cannot work, and the shared dev user is never substituted. */ export class AuthUnavailableError extends Error { readonly status = 503; constructor(reason: string) { super(`Auth unavailable: ${reason}`); this.name = "AuthUnavailableError"; } } export type VerifiedUser = { id: string; email: string | null }; /** * Resolve the signed-in user from the current request, or `null` when auth isn't * configured / nobody is signed in. Safe to call from server functions and SSR * loaders. * * `bearerToken` is for the LIVE PREVIEW: the app runs in a partitioned iframe * whose cookies don't reach the server, so `authMiddleware` forwards the session * as a bearer token, which we present as `Authorization: Bearer …` (the `bearer` * plugin resolves it). When deployed no token is passed and the cookie is used. */ export async function getSessionUser( bearerToken?: string, ): Promise { if (policy() !== "session") return null; const request = getRequest(); if (!request) return null; let headers = request.headers; if (bearerToken) { headers = new Headers(request.headers); headers.set("Authorization", `Bearer ${bearerToken}`); } const session = await auth.api.getSession({ headers }); if (!session?.user) return null; return { id: session.user.id, email: session.user.email ?? null }; } /** * Resolve the current user id for a server function, or throw when unauthorized. * Prefer `authMiddleware` (`./middleware`), which calls this for you. * - Auth enabled -> the verified session user id; throws * `UnauthorizedError` when signed out. Works in the sandbox preview too (real * sign-in via the baked preview client). * - Auth disabled (`VITE_AUTH_ENABLED=false`) + `DATABASE_URL` set -> throw (fail * closed): one shared dev user on a real database would let every visitor * read/write everyone's rows. * - Auth disabled + no database -> the shared dev user id. * - Federated sign-in refused (deployed without the per-app broker client) -> * throw `AuthUnavailableError`, with or without a database. Never the dev user. */ export async function requireUserId(bearerToken?: string): Promise { switch (policy()) { case "refuse-federation": throw new AuthUnavailableError(authRefusal ?? "federated sign-in refused"); case "refuse-database": throw new Error( "Auth is disabled (VITE_AUTH_ENABLED=false) but DATABASE_URL is set — " + "refusing to fall back to the shared dev user against a real database.", ); case "dev-user": return DEV_USER_ID; case "session": break; } const user = await getSessionUser(bearerToken); if (!user) throw new UnauthorizedError(); return user.id; }