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>
39 lines
1.1 KiB
TypeScript
39 lines
1.1 KiB
TypeScript
import { z } from "zod";
|
|
|
|
export const FieldErrorSchema = z.object({
|
|
field: z.string(),
|
|
message: z.string(),
|
|
});
|
|
|
|
export const ApiErrorResponseSchema = z.object({
|
|
error: z.string(),
|
|
code: z.string(),
|
|
fields: z.array(FieldErrorSchema).optional(),
|
|
});
|
|
|
|
export type FieldError = z.infer<typeof FieldErrorSchema>;
|
|
export type ApiErrorResponse = z.infer<typeof ApiErrorResponseSchema>;
|
|
|
|
/**
|
|
* Map a ZodError's issues to the FieldError[] shape used in API
|
|
* validation responses. Keeps the path-flattening rule in one place so
|
|
* every route returns validation errors in the same shape.
|
|
*/
|
|
export function zodIssuesToFieldErrors(error: z.ZodError): FieldError[] {
|
|
return error.issues.map((i) => ({
|
|
field: i.path.join("."),
|
|
message: i.message,
|
|
}));
|
|
}
|
|
|
|
/** Standard error codes */
|
|
export const ERROR_CODES = {
|
|
VALIDATION_ERROR: "VALIDATION_ERROR",
|
|
NOT_FOUND: "NOT_FOUND",
|
|
UNAUTHORIZED: "UNAUTHORIZED",
|
|
FORBIDDEN: "FORBIDDEN",
|
|
CONFLICT: "CONFLICT",
|
|
RATE_LIMITED: "RATE_LIMITED",
|
|
INTERNAL_ERROR: "INTERNAL_ERROR",
|
|
TERMS_OUTDATED: "TERMS_OUTDATED",
|
|
} as const;
|