batchCooking/apps/api/src/middlewares/require-auth.ts
Nicolas 1d03effc77 API: endpoints foyer/profil (nom, régime, allergènes) (step 2/6)
- GET/PATCH /house/current — renomme le foyer de l'utilisateur connecté.
  PATCH avec houseId null -> 404 HOUSE_NOT_FOUND.
- PATCH /profile/diet { dietId: number | null } — régime du profil ;
  null l'efface (étape skippable du parcours). dietId invalide ->
  404 DIET_NOT_FOUND.
- GET/PATCH /profile/allergies — allergènes/intolérances, liste d'IDs ;
  PATCH remplace l'ensemble complet (pas une fusion, cohérent avec un
  multi-select). ID invalide -> 404 ALLERGY_NOT_FOUND.
- 3 nouveaux ErrorCode (4041-4043) + libellés fr.
- Extraction de toSafeProfile() dans src/lib/safe-profile.ts —
  auparavant dupliqué dans auth.service.ts et require-auth.ts,
  profile.service.ts le réutilise aussi.
- Tests Mocha (28 passing) + Cucumber (15 scenarios) — même convention
  que le reste, doc README.

Deuxième commit de la feature profil/foyer/régime/allergènes —
composants front partagés dans le commit suivant.
2026-08-16 23:18:46 +02:00

68 lines
2.8 KiB
TypeScript

import { HttpError } from "@batch-cooking/error-tools";
import { ErrorCode, type SafeUserProfile } from "@batch-cooking/shared";
import type { NextFunction, Request, Response } from "express";
import { env } from "../config/env.js";
import { prisma } from "../db/prisma.js";
import { verifyAuthToken } from "../lib/jwt.js";
import { toSafeProfile } from "../lib/safe-profile.js";
/**
* Shape of `res.locals` once {@link requireAuth} has run successfully. Type
* a route handler's response as `Response<unknown, AuthLocals>` (see
* `auth.routes.ts`'s `/me` handler) to read `res.locals.userProfile` fully
* typed, no cast needed.
*/
export interface AuthLocals {
/** The authenticated profile, resolved from the session cookie's JWT. */
userProfile: SafeUserProfile;
}
/**
* Express middleware guarding routes that require an authenticated
* profile. Reads the session cookie, verifies the JWT, and re-checks
* `tokenVersion` against the database — so a stateless JWT can still be
* invalidated server-side (e.g. on password change / logout-everywhere,
* once that feature exists) despite carrying no server-side session.
*
* On success, attaches the resolved profile to `res.locals.userProfile`
* (typed via {@link AuthLocals}) for downstream handlers to use.
* Deliberately `res.locals` rather than augmenting Express's global
* `Request` type via `declare global`: `res.locals` is Express's own
* built-in mechanism for exactly this (passing data from a middleware to
* the next handler), typed per-route through a generic parameter — no
* project-wide ambient augmentation silently changing every `Request` in
* the codebase, whether or not it went through this middleware.
*
* @throws {HttpError} `401 NOT_AUTHENTICATED` for any failure — missing
* cookie, malformed/expired JWT, unknown profile, or stale tokenVersion.
* Never distinguishes the reason to the client.
*/
export async function requireAuth(
req: Request,
res: Response<unknown, AuthLocals>,
next: NextFunction,
) {
try {
const token = req.cookies?.[env.AUTH_COOKIE_NAME];
if (typeof token !== "string") {
throw new HttpError(401, ErrorCode.NOT_AUTHENTICATED, "Not authenticated");
}
const payload = verifyAuthToken(token);
const profile = await prisma.userProfile.findUnique({ where: { id: payload.userProfileId } });
if (!profile || profile.tokenVersion !== payload.tokenVersion) {
throw new HttpError(401, ErrorCode.NOT_AUTHENTICATED, "Not authenticated");
}
res.locals.userProfile = toSafeProfile(profile);
next();
} catch (err) {
if (err instanceof HttpError) {
next(err);
} else {
// Covers jwt.verify failures (expired/invalid/malformed token).
next(new HttpError(401, ErrorCode.NOT_AUTHENTICATED, "Not authenticated"));
}
}
}