Ctrl K
ComunidadGuías…

src/features/csrf.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * Form parsing and CSRF utilities
 */

import { errorRedirect } from "#routes/response.ts";
import { getSearchParam } from "#routes/url.ts";
import {
  csrfInvalidFormMessage,
  signCsrfToken,
  verifySignedCsrfToken,
} from "#shared/csrf.ts";
import { type Flash, getFlash, setFlashFormId } from "#shared/flash-context.ts";
import { FormParams } from "#shared/form-data.ts";
import { setSavedFormData } from "#shared/forms/saved-data.ts";
import { validateMessageText } from "#shared/inbound-message.ts";
import type { ResponseHandler } from "#shared/response-steps.ts";

export { FormParams } from "#shared/form-data.ts";

/**
 * Read and validate the shared "message" field of a contact/support form.
 * Returns the message text, or an error redirect to `path` when it is missing
 * or too long. The `string | Response` shape mirrors the session guards, so a
 * caller writes `if (x instanceof Response) return x;`.
 */
export const requireMessageField = (
  form: FormParams,
  path: string,
): string | Response => {
  const message = form.getString("message");
  const error = validateMessageText(message);
  return error ? errorRedirect(path, error) : message;
};

/**
 * Parse form data from request
 */
export const parseFormData = async (request: Request): Promise<FormParams> => {
  const text = await request.text();
  return new FormParams(text);
};

/**
 * Extract text fields from FormData as FormParams (skips File entries).
 * Handles multi-value fields (for example, checkbox groups) via append.
 */
export const formDataToParams = (formData: FormData): FormParams => {
  const params = new FormParams();
  for (const [key, value] of formData.entries()) {
    if (typeof value === "string") params.append(key, value);
  }
  return params;
};

/** Parse a submitted form and pull out its CSRF token field in one step. */
export const parseFormWithCsrf = async (
  request: Request,
): Promise<{ form: FormParams; formCsrf: string }> => {
  const form = await parseFormData(request);
  return { form, formCsrf: form.getString("csrf_token") };
};

/**
 * Read a required uploaded file from a multipart form. Returns the file, or the
 * caller's error response when the field is missing or empty.
 */
export const requireUploadedFile = (
  formData: FormData,
  field: string,
  onMissing: () => Response,
): File | Response => {
  const file = formData.get(field);
  return file instanceof File && file.size > 0 ? file : onMissing();
};

/** CSRF form result type */
export type CsrfFormResult =
  | { ok: true; form: FormParams }
  | { ok: false; response: Response };

/**
 * Parse form with CSRF validation.
 * Verifies the form token's HMAC signature and expiry.
 * On failure, generates a fresh token (stored for CsrfForm) before calling onInvalid.
 */
export const requireCsrfForm = async (
  request: Request,
  onInvalid: () => Response | Promise<Response>,
): Promise<CsrfFormResult> => {
  const { form, formCsrf } = await parseFormWithCsrf(request);

  // Always save form data so validation errors can restore user input.
  // This clears any stale data from a prior request and makes the current
  // submission available to renderFields/getSavedValue during re-rendering.
  setSavedFormData(form);

  if (formCsrf && (await verifySignedCsrfToken(formCsrf))) {
    return { form, ok: true };
  }

  await signCsrfToken();
  return { ok: false, response: await onInvalid() };
};

/**
 * Parse a CSRF-protected form, re-rendering the form on invalid CSRF.
 * Centralizes the default invalid/expired message.
 * On failure, generates a fresh token (stored for CsrfForm) and calls onInvalid.
 */
export const withCsrfForm = async (
  request: Request,
  onInvalid: (message: string, status: number) => Response | Promise<Response>,
  handler: ResponseHandler<[form: FormParams]>,
): Promise<Response> => {
  const csrf = await requireCsrfFormWithMessage(request, (message) =>
    onInvalid(message, 403),
  );
  return csrf.ok ? handler(csrf.form) : csrf.response;
};

/**
 * Parse a CSRF form, building the failure response from the standard
 * invalid/expired token message. The message-to-Response step stays the
 * caller's, so each surface can pick its own status and shape.
 */
export const requireCsrfFormWithMessage = (
  request: Request,
  onInvalid: (message: string) => Response | Promise<Response>,
): Promise<CsrfFormResult> =>
  requireCsrfForm(request, () => onInvalid(csrfInvalidFormMessage()));

/**
 * Record the form a redirect targeted (`?form=`) so a matching CsrfForm renders
 * the flash inline, and return the flash for callers that need it. The flash
 * itself is already in the request context (set by middleware). The Layout
 * backstop or the targeted form renders it. Handlers no longer thread it.
 */
export const applyFlash = (request: Request): Flash => {
  setFlashFormId(getSearchParam(request, "form"));
  return getFlash();
};