batchCooking/apps/api/src/modules/profile/profile.routes.ts
Nicolas acab18ac4a feat(recipes): catalogue v2 - visibilité, favoris, régimes et catalogue d'ingrédients exhaustif
Recipe catalog v2:
- Recipe gagne visibility (PERSONAL/HOUSE/PUBLIC), authorId, authorHouseId
- Favoris par utilisateur (RecipeFavorite), régimes associés (RecipeDiet)
- Aliments "pas aimés" par utilisateur (UserProfileDislikedIngredient),
  distinct des allergies médicales
- API: GET /recipes?tab=favoris|perso|foyer|publique avec contrôle d'accès
  complet, POST/DELETE /recipes/:id/favorite, édition/suppression réservées
  à l'auteur (403 NOT_RECIPE_AUTHOR), GET/PATCH /profile/disliked-ingredients
- Frontend: vue maître-détail (onglets + tableau + panneau détail),
  formulaire enrichi (visibilité, régimes), section préférences pour les
  aliments pas aimés

Catalogue d'ingrédients de référence:
- Extension du seed de 39 à ~430 ingrédients (viandes, poissons/fruits de
  mer, légumes, fruits, féculents, condiments/sauces, épices/herbes, pains
  à sandwich, cuisines italienne/asiatique/mexicaine/maghrébine, liquides
  et boissons de cuisine, bouillons/fonds)
- Chaque ingrédient lié à ses allergènes UE (IngredientAllergy) — les 14
  allergènes réglementaires restent tous couverts
- Seeding optimisé en requêtes groupées (createMany/diff ciblé) plutôt
  qu'un upsert par ligne, pour garder resetDatabase() rapide en test

Tests: 102 tests Mocha + 32 scénarios BDD, tous verts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 09:29:11 +02:00

72 lines
2.3 KiB
TypeScript

import { wrapAsyncHandler } from "@batch-cooking/express-tools";
import {
updateAllergiesSchema,
updateDietSchema,
updateDislikedIngredientsSchema,
} from "@batch-cooking/shared";
import { Router } from "express";
import { type AuthLocals, requireAuth } from "../../middlewares/require-auth.js";
import {
getAllergyIds,
getDislikedIngredientIds,
updateAllergies,
updateDiet,
updateDislikedIngredients,
} from "./profile.service.js";
/** Router mounted at `/profile` in app.ts. Every route requires a session — this is the authenticated user's own profile. */
export const profileRouter = Router();
/** The regime step of the profile journey (signup wizard and the `/foyer` settings page both call this). */
profileRouter.patch(
"/diet",
requireAuth,
wrapAsyncHandler<unknown, AuthLocals>(async (req, res) => {
const input = updateDietSchema.parse(req.body);
const profile = await updateDiet(res.locals.userProfile.id, input.dietId);
res.status(200).json(profile);
}),
);
profileRouter.get(
"/allergies",
requireAuth,
wrapAsyncHandler<unknown, AuthLocals>(async (_req, res) => {
const allergyIds = await getAllergyIds(res.locals.userProfile.id);
res.status(200).json(allergyIds);
}),
);
/** The allergen/intolerance step of the profile journey — same callers as PATCH /diet above. */
profileRouter.patch(
"/allergies",
requireAuth,
wrapAsyncHandler<unknown, AuthLocals>(async (req, res) => {
const input = updateAllergiesSchema.parse(req.body);
const allergyIds = await updateAllergies(res.locals.userProfile.id, input.allergyIds);
res.status(200).json(allergyIds);
}),
);
profileRouter.get(
"/disliked-ingredients",
requireAuth,
wrapAsyncHandler<unknown, AuthLocals>(async (_req, res) => {
const ids = await getDislikedIngredientIds(res.locals.userProfile.id);
res.status(200).json(ids);
}),
);
/** Personal taste preference — distinct from `/allergies`, which is medical. Managed from `/parametres/preferences` (see `PreferencesPage.tsx`). */
profileRouter.patch(
"/disliked-ingredients",
requireAuth,
wrapAsyncHandler<unknown, AuthLocals>(async (req, res) => {
const input = updateDislikedIngredientsSchema.parse(req.body);
const ids = await updateDislikedIngredients(
res.locals.userProfile.id,
input.dislikedIngredientIds,
);
res.status(200).json(ids);
}),
);