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>
72 lines
2.3 KiB
TypeScript
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);
|
|
}),
|
|
);
|