fix(api): CORS + CORP sur réponses statiques /uploads (Flutter web)

- enableCors avant les statics
- Access-Control-Allow-Origin * + Cross-Origin-Resource-Policy sur fichiers
- OPTIONS préflight pour /uploads et /api/v1/uploads

Made-with: Cursor
This commit is contained in:
MARTIN Julien 2026-04-11 17:11:22 +02:00
parent 3bfdadd301
commit 4fe29799cd

View File

@ -9,6 +9,34 @@ import { LogRequestInterceptor } from './common/interceptors/log-request.interce
import * as path from 'path'; import * as path from 'path';
import * as express from 'express'; 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). */ /** Répertoire disque contenant le dossier `photos` (ex. /app/uploads si photos dans /app/uploads/photos). */
function resolveUploadsFilesystemRoot(): string { function resolveUploadsFilesystemRoot(): string {
const raw = process.env.UPLOAD_PHOTOS_DIR?.trim(); const raw = process.env.UPLOAD_PHOTOS_DIR?.trim();
@ -29,34 +57,31 @@ async function bootstrap() {
app.useBodyParser('json', { limit: '15mb' }); app.useBodyParser('json', { limit: '15mb' });
app.useBodyParser('urlencoded', { extended: true, limit: '15mb' }); app.useBodyParser('urlencoded', { extended: true, limit: '15mb' });
// Photos : chemins stockés en base type /uploads/photos/... // CORS global **avant** les fichiers statiques pour que les réponses API et idéalement la chaîne Express restent cohérentes.
// - /uploads/ : si le proxy route ce préfixe vers Nest.
// - /api/v1/uploads/ : même trafic que lAPI (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
app.enableCors({ app.enableCors({
origin: true, // Autorise toutes les origines (dev) - à restreindre en prod origin: true, // Reflète lOrigin (dev / prod) — routes API + cookies JWT
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'Accept'], allowedHeaders: ['Content-Type', 'Authorization', 'Accept'],
credentials: true, 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( app.useGlobalPipes(
new ValidationPipe({ new ValidationPipe({
whitelist: true, whitelist: true,