nexus / src /lib /auth /verify.server.ts
betterwithage's picture
deploy(hf): sync szl-holdings/nexus@a0cd605f4d1021510a8e351de4016b2453e091e5 derived COPY set
8441695 verified
Raw History Blame Contribute Delete
4.97 kB
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<VerifiedUser | null> {
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<string> {
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;
}