diff --git a/backend/src/main.ts b/backend/src/main.ts index 27a2863..c5754bf 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -9,6 +9,34 @@ import { LogRequestInterceptor } from './common/interceptors/log-request.interce import * as path from 'path'; import * as express from 'express'; +/** GET/HEAD photos : CORS + CORP pour Flutter web (cross-origin `Image.network`). Pas de cookie sur ces URLs. */ +function setStaticImageCorsHeaders(res: express.Response): void { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin'); +} + +const staticImageServeOptions = { + index: false, + fallthrough: true, + setHeaders: (res: express.Response) => setStaticImageCorsHeaders(res), +}; + +/** Préflight si le navigateur interroge OPTIONS sur les médias. */ +function uploadsCorsPreflight( + req: express.Request, + res: express.Response, + next: express.NextFunction, +): void { + if (req.method === 'OPTIONS') { + setStaticImageCorsHeaders(res); + res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS'); + res.setHeader('Access-Control-Max-Age', '86400'); + res.status(204).end(); + return; + } + next(); +} + /** Répertoire disque contenant le dossier `photos` (ex. /app/uploads si photos dans /app/uploads/photos). */ function resolveUploadsFilesystemRoot(): string { const raw = process.env.UPLOAD_PHOTOS_DIR?.trim(); @@ -29,34 +57,31 @@ async function bootstrap() { app.useBodyParser('json', { limit: '15mb' }); app.useBodyParser('urlencoded', { extended: true, limit: '15mb' }); - // Photos : chemins stockés en base type /uploads/photos/... - // - /uploads/ : si le proxy route ce préfixe vers Nest. - // - /api/v1/uploads/ : même trafic que l’API (Traefik PathPrefix /api → pas besoin de règle /uploads). - const uploadsRoot = resolveUploadsFilesystemRoot(); - app.useStaticAssets(uploadsRoot, { - prefix: '/uploads/', - index: false, - fallthrough: true, - }); - app - .getHttpAdapter() - .getInstance() - .use( - '/api/v1/uploads', - express.static(uploadsRoot, { index: false, fallthrough: true }), - ); - - // Log de chaque appel API si LOG_API_REQUESTS=true (mode debug) - app.useGlobalInterceptors(new LogRequestInterceptor()); - - // Configuration CORS pour autoriser les requêtes depuis localhost (dev) et production + // CORS global **avant** les fichiers statiques pour que les réponses API et idéalement la chaîne Express restent cohérentes. app.enableCors({ - origin: true, // Autorise toutes les origines (dev) - à restreindre en prod + origin: true, // Reflète l’Origin (dev / prod) — routes API + cookies JWT methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'Accept'], credentials: true, }); + const expressApp = app.getHttpAdapter().getInstance(); + const uploadsRoot = resolveUploadsFilesystemRoot(); + + // Photos : chemins stockés en base type /uploads/photos/... + // En-têtes explicites sur les réponses fichier (les statics peuvent répondre sans repasser par la même couche que les contrôleurs). + expressApp.use('/uploads', uploadsCorsPreflight); + expressApp.use('/api/v1/uploads', uploadsCorsPreflight); + + app.useStaticAssets(uploadsRoot, { + prefix: '/uploads/', + ...staticImageServeOptions, + }); + expressApp.use('/api/v1/uploads', express.static(uploadsRoot, staticImageServeOptions)); + + // Log de chaque appel API si LOG_API_REQUESTS=true (mode debug) + app.useGlobalInterceptors(new LogRequestInterceptor()); + app.useGlobalPipes( new ValidationPipe({ whitelist: true,