fix(api): autorise l'origine WebView Capacitor dans CORS_ORIGIN #82

Merged
kyuno merged 1 commit from fix/cors-origin-capacitor-app into main 2026-09-07 08:39:56 +02:00
3 changed files with 42 additions and 7 deletions

View file

@ -11,7 +11,7 @@ JWT_SECRET=changeme-generate-a-real-random-secret-at-least-32-chars
# Optional — defaults shown (see src/config/env.ts) # Optional — defaults shown (see src/config/env.ts)
# JWT_EXPIRES_IN=7d # JWT_EXPIRES_IN=7d
# AUTH_COOKIE_NAME=session # AUTH_COOKIE_NAME=session
# CORS_ORIGIN=http://localhost:5173 # CORS_ORIGIN=http://localhost:5173,https://localhost
# INTENT_SERVICE_BASE_URL=http://localhost:8000 # INTENT_SERVICE_BASE_URL=http://localhost:8000
# Required — services/tech-step-intent-service must be running locally (see # Required — services/tech-step-intent-service must be running locally (see

View file

@ -35,10 +35,11 @@ export function createServer(): ExpressServer {
// pipeline (its "finish" listener still fires for a request that never // pipeline (its "finish" listener still fires for a request that never
// makes it past CORS/body-parsing, not just ones that reach a route). // makes it past CORS/body-parsing, not just ones that reach a route).
server.addMiddleware(requestLogger); server.addMiddleware(requestLogger);
// One allowed origin: the app (`CORS_ORIGIN`). The admin surface // Allowed origin(s) come from `CORS_ORIGIN` (see its own doc comment in
// (`/admin/*`) is served by this same API and consumed by `apps/web`'s // config/env.ts for why the packaged Android app's origin must always be
// own `/admin/*` routes — same origin as the rest of the app, so no // one of them). The admin surface (`/admin/*`) is served by this same API
// extra CORS entry. // 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.setupCore({ corsOrigin: env.CORS_ORIGIN });
server.addRoute("get", "/health", (_req: Request, res: Response) => { server.addRoute("get", "/health", (_req: Request, res: Response) => {

View file

@ -34,8 +34,42 @@ const envSchema = z.object({
JWT_EXPIRES_IN: z.string().default("7d"), JWT_EXPIRES_IN: z.string().default("7d"),
/** Name of the httpOnly cookie carrying the session JWT. */ /** Name of the httpOnly cookie carrying the session JWT. */
AUTH_COOKIE_NAME: z.string().default("session"), AUTH_COOKIE_NAME: z.string().default("session"),
/** Origin allowed by CORS — must match wherever apps/web is served from. */ /**
CORS_ORIGIN: z.string().default("http://localhost:5173"), * Origin(s) allowed by CORS, comma-separated — parsed below into a
* `string[]` for `ExpressServerCoreOptions.corsOrigin`
* (`app.ts`/`express-server.ts`, which passes it straight to the `cors`
* package) and reused as-is for the Socket.IO handshake's own CORS config
* (`server.ts`, which needs its own since the WS upgrade never runs
* through `setupCore`'s middleware).
*
* Must always include `https://localhost`: the packaged Android app's
* WebView is served from exactly that fixed origin (Capacitor's default
* for `androidScheme`/hostname — see `apps/web/capacitor.config.ts`'s own
* doc comment), and every request it makes to this API — `/auth/login`
* and `/auth/signup` included — is genuinely cross-origin, unlike
* apps/web's *browser* build, which is served from the very same origin
* as this API in production (see docker-compose.yml's `app` service) and
* so never needs an entry here at all. Missing this origin doesn't fail
* loudly: the `cors` package still answers a preflight `OPTIONS` with 204
* for an origin it's about to reject (it only *omits*
* `Access-Control-Allow-Origin`), so the request the app's WebView logs
* looks identical to a normal one right up until the browser silently
* drops the real `POST` — surfacing client-side as a generic "unexpected
* error" with no 401/403 anywhere to point at the actual cause. Default
* covers both the Vite dev server and the packaged app out of the box; a
* real deployment's own web origin (e.g. a production domain) never needs
* adding here for the reason above, but the env var still allows
* extending/overriding this list (comma-separated) if that ever changes.
*/
CORS_ORIGIN: z
.string()
.default("http://localhost:5173,https://localhost")
.transform((value) =>
value
.split(",")
.map((origin) => origin.trim())
.filter((origin) => origin.length > 0),
),
/** /**
* Absolute path to the built frontend (`apps/web/dist`), to serve * Absolute path to the built frontend (`apps/web/dist`), to serve
* alongside the API. Optional, no default — only set inside the * alongside the API. Optional, no default — only set inside the