src/features/auth-failures.ts
Rama main · réplica de lectura del repositorio original.
/**
* How a refused or failed auth answers: the failure responses keyed by reason,
* each with an html and a json variant.
*/
import { t } from "#i18n";
import { apiErrorResponse } from "#routes/api/cors.ts";
import { htmlResponse, redirectResponse } from "#routes/response.ts";
/** The HTML 403 body: a role refusal says the account cannot open the page.
* An owner-only refusal says only the owner account can. The audience fact
* comes from the same route declaration that the guards admit by. */
const forbiddenBody = (detail?: ForbiddenDetail): string =>
detail === "owner-only"
? t("auth.forbidden_owner_only")
: t("auth.forbidden_role");
/** Shared auth failure response factories */
const htmlForbidden = () => htmlResponse(forbiddenBody(), 403);
const jsonForbidden = () => apiErrorResponse("Forbidden", 403);
/** Auth failure responses keyed by reason, with html and json variants side-by-side. */
const AUTH_FAILURES = {
forbidden: {
html: (detail?: ForbiddenDetail) =>
htmlResponse(forbiddenBody(detail), 403),
json: jsonForbidden,
},
"invalid-api-key": {
html: htmlForbidden,
json: () => apiErrorResponse("Invalid API key", 401),
},
"invalid-csrf": {
html: () => htmlResponse("Invalid CSRF token", 403),
json: jsonForbidden,
},
"not-authenticated": {
html: () => redirectResponse("/admin"),
json: () => apiErrorResponse("Not authenticated", 401),
},
} satisfies Record<
string,
Record<"html" | "json", (...args: never[]) => Response>
>;
type AuthFailureReason = keyof typeof AUTH_FAILURES;
/** The channel a failure answers on: the html page or the json API. */
export type AuthChannel = keyof (typeof AUTH_FAILURES)[AuthFailureReason];
/** How a forbidden response narrows its who-can-open message: the route's
* declared audience knows whether the page is owner-only. */
export type ForbiddenDetail = "owner-only";
/** Construct a standardized auth failure response. */
export const authFailure = (
channel: AuthChannel,
reason: AuthFailureReason,
forbiddenDetail?: ForbiddenDetail,
): Response => {
const factory = AUTH_FAILURES[reason][channel] as (
detail?: ForbiddenDetail,
) => Response;
return factory(forbiddenDetail);
};