trails/apps/journal/app/lib/api-guard.server.ts
Ullrich Schäfer 067e2ebd0b
fix: enforce Terms gate on bearer-token API requests
Mobile API requests authenticated via OAuth2 bearer tokens bypassed the
Terms gate that the root loader applies to web cookie sessions. Extend
requireApiUser to compare the user's termsVersion with TERMS_VERSION
and return a structured 403 { code: "TERMS_OUTDATED", currentTermsVersion }
on mismatch so mobile clients can surface their own re-acceptance UI.

Spec delta on journal-auth captures the new requirement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 01:59:28 +02:00

38 lines
1.3 KiB
TypeScript

import { getAuthenticatedUser } from "./oauth.server.ts";
import { TERMS_VERSION } from "./legal.ts";
import { ERROR_CODES } from "@trails-cool/api";
/**
* Require authentication for an API route. Returns the user or throws a
* Response: 401 if unauthenticated, 403 with `TERMS_OUTDATED` if the user's
* stored `terms_version` is missing or stale relative to the current
* `TERMS_VERSION`. Mirrors the cookie-session terms gate enforced by the
* root loader, so bearer-token API traffic can't bypass it.
*/
export async function requireApiUser(request: Request) {
const user = await getAuthenticatedUser(request);
if (!user) {
throw Response.json(
{ error: "Unauthorized", code: ERROR_CODES.UNAUTHORIZED },
{ status: 401 },
);
}
if (user.termsVersion !== TERMS_VERSION) {
throw Response.json(
{
error: "Terms of Service have been updated and must be re-accepted",
code: ERROR_CODES.TERMS_OUTDATED,
currentTermsVersion: TERMS_VERSION,
},
{ status: 403 },
);
}
return user;
}
/**
* Return a structured API error response.
*/
export function apiError(status: number, code: string, message: string, fields?: Array<{ field: string; message: string }>) {
return Response.json({ error: message, code, fields }, { status });
}