- 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.
68 lines
2.8 KiB
TypeScript
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"));
|
|
}
|
|
}
|
|
}
|