Répond à deux besoins : permettre à chaque foyer de choisir quelles sources apparaissent dans ses onglets de recettes, et distinguer les sources à API officielle des sources scrapées. - RecipeSourceAdapter.official (booléen, sans défaut — chaque adaptateur doit le déclarer explicitement) synchronisé sur Source.official par syncRecipeSources. - HouseSource : table de jointure opt-in (House <-> Source) — aucune ligne = source masquée. Un foyer nouvellement créé ne voit aucune source tant qu'il ne les active pas explicitement. - GET /reference/sources (catalogue des sources implémentées, avec le flag officiel). - GET/PATCH /house/current/sources (lecture/remplacement complet des sources activées par le foyer courant). - recipe.service.ts : sourceVisibilityWhere() filtre désormais TOUS les onglets (perso/foyer/publique/favoris) — une recette sans source reste toujours visible ; une recette importée ne l'est que si sa source est activée pour le foyer du viewer. Un viewer sans foyer ne voit aucune recette sourcée. Côté web : - Nouvelle étape /onboarding/sources dans le wizard d'inscription, atteinte uniquement si un foyer vient d'être créé/rejoint (sinon on saute direct aux allergènes) ; s'auto-saute aussi si aucune source n'est encore implémentée (catalogue vide aujourd'hui). - Nouvelle section « Sources de recettes » dans /parametres/foyer (masquée dans les mêmes conditions), avec sauvegarde à la volée (même pattern que les autres préférences hot-saved). - SourceSelect (features/house/), grille de cases à cocher avec badge officiel/non-officielle, sur le même principe qu'AllergySelect. 172 tests backend passent (dont 25 nouveaux). Build et lint propres. Vérifié manuellement en navigateur : le parcours d'onboarding saute bien l'étape sources (catalogue vide) et affiche « 4 sur 4 » quand un foyer a été créé ; la section paramètres reste invisible tant qu'aucune source n'existe. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
134 lines
4.3 KiB
TypeScript
134 lines
4.3 KiB
TypeScript
import { HttpError } from "@batch-cooking/error-tools";
|
|
import { wrapAsyncHandler } from "@batch-cooking/express-tools";
|
|
import {
|
|
ErrorCode,
|
|
createHouseSchema,
|
|
joinHouseSchema,
|
|
renameHouseSchema,
|
|
updateHouseSourcesSchema,
|
|
} from "@batch-cooking/shared";
|
|
import { Router } from "express";
|
|
import { type AuthLocals, requireAuth } from "../../middlewares/require-auth.js";
|
|
import {
|
|
createHouse,
|
|
deleteHouse,
|
|
getCurrentHouse,
|
|
getHouseSourceIds,
|
|
joinHouse,
|
|
leaveCurrentHouse,
|
|
removeMember,
|
|
renameHouse,
|
|
updateHouseSources,
|
|
} from "./house.service.js";
|
|
|
|
/** Router mounted at `/house` in app.ts. Every route requires a session — a household is per-user (via their profile), never public. */
|
|
export const houseRouter = Router();
|
|
|
|
houseRouter.get(
|
|
"/current",
|
|
requireAuth,
|
|
wrapAsyncHandler<unknown, AuthLocals>(async (_req, res) => {
|
|
const house = await getCurrentHouse(res.locals.userProfile.houseId);
|
|
res.status(200).json(house);
|
|
}),
|
|
);
|
|
|
|
/** The household step of the profile journey (onboarding wizard and the `/parametres/foyer` settings page both call this) — renaming, open to any member. */
|
|
houseRouter.patch(
|
|
"/current",
|
|
requireAuth,
|
|
wrapAsyncHandler<unknown, AuthLocals>(async (req, res) => {
|
|
const input = renameHouseSchema.parse(req.body);
|
|
const house = await renameHouse(res.locals.userProfile.houseId, input.name);
|
|
res.status(200).json(house);
|
|
}),
|
|
);
|
|
|
|
/** Creates a new household for a profile that doesn't have one yet — the "create" half of the optional household step. */
|
|
houseRouter.post(
|
|
"/",
|
|
requireAuth,
|
|
wrapAsyncHandler<unknown, AuthLocals>(async (req, res) => {
|
|
const input = createHouseSchema.parse(req.body);
|
|
const house = await createHouse(
|
|
res.locals.userProfile.id,
|
|
res.locals.userProfile.houseId,
|
|
input.name,
|
|
);
|
|
res.status(201).json(house);
|
|
}),
|
|
);
|
|
|
|
/** Joins an existing household by invite code — the "join" half of the optional household step. */
|
|
houseRouter.post(
|
|
"/join",
|
|
requireAuth,
|
|
wrapAsyncHandler<unknown, AuthLocals>(async (req, res) => {
|
|
const input = joinHouseSchema.parse(req.body);
|
|
const house = await joinHouse(
|
|
res.locals.userProfile.id,
|
|
res.locals.userProfile.houseId,
|
|
input.inviteCode,
|
|
);
|
|
res.status(200).json(house);
|
|
}),
|
|
);
|
|
|
|
/** Removes the caller from their own household — see `deleteHouse` below for removing the household itself. */
|
|
houseRouter.post(
|
|
"/leave",
|
|
requireAuth,
|
|
wrapAsyncHandler<unknown, AuthLocals>(async (_req, res) => {
|
|
await leaveCurrentHouse(res.locals.userProfile.id, res.locals.userProfile.houseId);
|
|
res.status(204).end();
|
|
}),
|
|
);
|
|
|
|
/** Deletes the household entirely — every member loses it. Admin-only, see `house.service.ts`. */
|
|
houseRouter.delete(
|
|
"/current",
|
|
requireAuth,
|
|
wrapAsyncHandler<unknown, AuthLocals>(async (_req, res) => {
|
|
await deleteHouse(res.locals.userProfile.id, res.locals.userProfile.houseId);
|
|
res.status(204).end();
|
|
}),
|
|
);
|
|
|
|
/** Which recipe sources the household currently sees in its recipe tabs — the source step of the onboarding wizard and the `/parametres/foyer` settings page both call this. */
|
|
houseRouter.get(
|
|
"/current/sources",
|
|
requireAuth,
|
|
wrapAsyncHandler<unknown, AuthLocals>(async (_req, res) => {
|
|
const sourceIds = await getHouseSourceIds(res.locals.userProfile.houseId);
|
|
res.status(200).json(sourceIds);
|
|
}),
|
|
);
|
|
|
|
/** Replaces the household's enabled-source set — same callers as the GET above. */
|
|
houseRouter.patch(
|
|
"/current/sources",
|
|
requireAuth,
|
|
wrapAsyncHandler<unknown, AuthLocals>(async (req, res) => {
|
|
const input = updateHouseSourcesSchema.parse(req.body);
|
|
const sourceIds = await updateHouseSources(res.locals.userProfile.houseId, input.sourceIds);
|
|
res.status(200).json(sourceIds);
|
|
}),
|
|
);
|
|
|
|
/** Removes one specific member from the caller's household. Admin-only, see `house.service.ts`. */
|
|
houseRouter.delete(
|
|
"/members/:memberId",
|
|
requireAuth,
|
|
wrapAsyncHandler<unknown, AuthLocals>(async (req, res) => {
|
|
const memberId = Number(req.params.memberId);
|
|
if (!Number.isInteger(memberId)) {
|
|
throw new HttpError(400, ErrorCode.VALIDATION_ERROR, "memberId must be an integer");
|
|
}
|
|
const house = await removeMember(
|
|
res.locals.userProfile.id,
|
|
res.locals.userProfile.houseId,
|
|
memberId,
|
|
);
|
|
res.status(200).json(house);
|
|
}),
|
|
);
|