All checks were successful
L'app Android n'arrivait a se logger/inscrire nulle part (204 + "erreur inattendue" cote client) : CORS_ORIGIN n'etait jamais defini en prod (docker-compose.yml ne le passe pas a `app`), donc il retombait sur son default `http://localhost:5173` (le serveur Vite de dev), qui ne matche evidemment jamais l'origine reelle de l'appli packagee (`https://localhost`, cf. capacitor.config.ts). Le site web n'est lui jamais touche : il est servi depuis la meme origine que l'API en prod, donc jamais de preflight CORS du tout sur /auth/*. Le symptome trompeur vient du package `cors` : un preflight OPTIONS recoit toujours 204, meme pour une origine rejetee — la difference invisible sans inspecter les headers est l'absence de Access-Control-Allow-Origin, qui fait ensuite bloquer le vrai POST cote client sans jamais atteindre le serveur. Vu depuis les logs serveur, la route a bien l'air "appelee" (le preflight matche le meme chemin) alors que /auth/login lui-meme n'est jamais reellement recu. CORS_ORIGIN devient une liste separee par des virgules (le type `ExpressServerCoreOptions.corsOrigin` acceptait deja `string | string[]`) et son default inclut desormais `https://localhost` — l'origine de l'appli packagee est fixe et non secrete (contrairement au domaine du site en prod, qui n'a de toute facon pas besoin d'etre dans cette liste puisqu'il est same-origin), donc la coder en dur dans le default couvre tout deploiement sans configuration supplementaire. Verifie en local : script ad hoc qui demarre le vrai `createServer()` et envoie un preflight OPTIONS /auth/login avec differentes origines — `https://localhost` et `http://localhost:5173` recoivent maintenant Access-Control-Allow-Origin, une origine arbitraire non listee continue de ne pas le recevoir (pas de bypass en wildcard). Egalement `pnpm --filter api exec tsc --noEmit` et `pnpm exec biome check` sur les fichiers touches. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
116 lines
6.2 KiB
TypeScript
116 lines
6.2 KiB
TypeScript
import { errorHandlerService } from "@batch-cooking/error-tools";
|
|
import { createErrorMiddleware, ExpressServer } from "@batch-cooking/express-tools";
|
|
import { ErrorCode } from "@batch-cooking/shared";
|
|
import type { Express, Request, Response } from "express";
|
|
import { env } from "./config/env.js";
|
|
import { errorLogger } from "./middlewares/error-logger.js";
|
|
import { requestLogger } from "./middlewares/request-logger.js";
|
|
import { adminRouter } from "./modules/admin/admin.routes.js";
|
|
import { authRouter } from "./modules/auth/auth.routes.js";
|
|
import { cookingSessionRouter } from "./modules/cooking-session/cooking-session.routes.js";
|
|
import { houseRouter } from "./modules/house/house.routes.js";
|
|
import { techStepWorkerRouter } from "./modules/internal/tech-step-worker.routes.js";
|
|
import { mobileRouter } from "./modules/mobile/mobile.routes.js";
|
|
import { planningRouter } from "./modules/planning/planning.routes.js";
|
|
import { preferencesRouter } from "./modules/preferences/preferences.routes.js";
|
|
import { profileRouter } from "./modules/profile/profile.routes.js";
|
|
import { recipeRouter } from "./modules/recipe/recipe.routes.js";
|
|
import { referenceRouter } from "./modules/reference/reference.routes.js";
|
|
import { shoppingListRouter } from "./modules/shopping-list/shopping-list.routes.js";
|
|
import { sourcesRouter } from "./modules/sources/sources.routes.js";
|
|
|
|
/**
|
|
* Builds the API's `ExpressServer`: standard middleware, routes, and the
|
|
* final error handler, in that order. Returns the `ExpressServer` wrapper
|
|
* (not just the raw Express app) so `server.ts` can call `.listen()` on
|
|
* it — {@link createApp} below is the thinner entry point that exposes
|
|
* just the raw `Express` instance, for test tooling (supertest) that
|
|
* expects one.
|
|
*/
|
|
export function createServer(): ExpressServer {
|
|
const server = new ExpressServer();
|
|
// First middleware registered, before even setupCore's own (CORS/JSON
|
|
// body parsing/cookies) — it only reads `req`/`res`, so it doesn't need
|
|
// to run after them, and mounting it first means it wraps the *whole*
|
|
// pipeline (its "finish" listener still fires for a request that never
|
|
// makes it past CORS/body-parsing, not just ones that reach a route).
|
|
server.addMiddleware(requestLogger);
|
|
// Allowed origin(s) come from `CORS_ORIGIN` (see its own doc comment in
|
|
// config/env.ts for why the packaged Android app's origin must always be
|
|
// one of them). The admin surface (`/admin/*`) is served by this same API
|
|
// and consumed by `apps/web`'s own `/admin/*` routes — same origin as the
|
|
// rest of the app, so no extra CORS entry.
|
|
server.setupCore({ corsOrigin: env.CORS_ORIGIN });
|
|
|
|
server.addRoute("get", "/health", (_req: Request, res: Response) => {
|
|
res.status(200).json({ status: "ok" });
|
|
});
|
|
|
|
server.mountRouter("/auth", authRouter);
|
|
// Admin application surface — its own auth (`requireAdmin`, distinct
|
|
// cookie/secret), never the end-user session. Mounted at `/admin/api`,
|
|
// *not* bare `/admin`: apps/web's own admin pages live at `/admin/*` too
|
|
// (`/admin/monitoring`, `/admin/login`...), and this API is served from
|
|
// the same origin/process as that frontend (see `serveStaticFrontend`
|
|
// below). A bare `/admin` mount put `GET /admin/monitoring` (API) and the
|
|
// page route `/admin/monitoring` in direct conflict — Express matches by
|
|
// path alone, so a hard navigation/refresh on that page was answered by
|
|
// this router's own handler (raw JSON) instead of falling through to the
|
|
// SPA's `index.html`. The `/api` segment guarantees no current or future
|
|
// admin page path can ever collide with an admin API path again.
|
|
server.mountRouter("/admin/api", adminRouter);
|
|
server.mountRouter("/house", houseRouter);
|
|
// Not user-facing — `services/tech-step-llm-worker` only, guarded by
|
|
// `requireInternalWorker` on every route within (see that router's own
|
|
// doc comment), never `requireAuth`. Mounted alongside the other routers
|
|
// rather than nested under one of them since it isn't scoped to a single
|
|
// recipe/step the way `recipeRouter`'s own correction routes are.
|
|
server.mountRouter("/internal/tech-steps", techStepWorkerRouter);
|
|
// Public proxy for Forgejo's Releases API (issues #65/#66) — see
|
|
// mobile.service.ts's doc comment for why this can't just be called
|
|
// directly from the browser.
|
|
server.mountRouter("/mobile", mobileRouter);
|
|
server.mountRouter("/planning", planningRouter);
|
|
server.mountRouter("/preferences", preferencesRouter);
|
|
server.mountRouter("/profile", profileRouter);
|
|
server.mountRouter("/recipes", recipeRouter);
|
|
server.mountRouter("/reference", referenceRouter);
|
|
server.mountRouter("/shopping-list", shoppingListRouter);
|
|
server.mountRouter("/cooking-session", cookingSessionRouter);
|
|
server.mountRouter("/sources", sourcesRouter);
|
|
|
|
// Serves the built frontend (production Docker image only — see
|
|
// FRONTEND_DIST_DIR's doc comment in config/env.ts). Must come after
|
|
// every API route above (so they always win) and before the catch-all
|
|
// 404 below (so unmatched GETs fall through to the SPA's index.html
|
|
// instead of a JSON 404).
|
|
if (env.FRONTEND_DIST_DIR) {
|
|
server.serveStaticFrontend(env.FRONTEND_DIST_DIR);
|
|
}
|
|
|
|
// No route matched — same shape as every other error response, via the
|
|
// shared ErrorCode contract, so clients never special-case 404s.
|
|
server.addMiddleware((_req: Request, res: Response) => {
|
|
res.status(404).json({ code: ErrorCode.NOT_FOUND, message: "Not found" });
|
|
});
|
|
|
|
// Two error-handling middlewares in a row (Express runs them in
|
|
// registration order, same as regular middleware) — errorLogger logs the
|
|
// error, then hands it on (`next(err)`) to the real one: all the "what
|
|
// status/body does this error map to" logic lives in ErrorHandlerService,
|
|
// from @batch-cooking/error-tools — this stays a thin adapter.
|
|
server.setErrorHandler(errorLogger);
|
|
server.setErrorHandler(createErrorMiddleware(errorHandlerService));
|
|
|
|
return server;
|
|
}
|
|
|
|
/**
|
|
* Builds a fresh Express application instance (no shared mutable state
|
|
* between calls — used both indirectly by the real server entrypoint
|
|
* (`server.ts`, via {@link createServer}) and directly by tests, which
|
|
* each get their own app via supertest).
|
|
*/
|
|
export function createApp(): Express {
|
|
return createServer().instance;
|
|
}
|