- 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
129 lines
4.3 KiB
TypeScript
129 lines
4.3 KiB
TypeScript
import { NestFactory } from '@nestjs/core';
|
||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||
import { AppModule } from './app.module';
|
||
import { ConfigService } from '@nestjs/config';
|
||
import { SwaggerModule } from '@nestjs/swagger/dist/swagger-module';
|
||
import { DocumentBuilder } from '@nestjs/swagger';
|
||
import { ValidationPipe } from '@nestjs/common';
|
||
import { LogRequestInterceptor } from './common/interceptors/log-request.interceptor';
|
||
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();
|
||
const photosDir = raw
|
||
? path.isAbsolute(raw)
|
||
? raw
|
||
: path.resolve(process.cwd(), raw)
|
||
: path.join(process.cwd(), 'uploads', 'photos');
|
||
return path.dirname(photosDir);
|
||
}
|
||
|
||
async function bootstrap() {
|
||
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
|
||
logger: ['error', 'warn', 'log', 'debug', 'verbose'],
|
||
});
|
||
|
||
// Inscription (photos base64 dans le JSON) : sans limite > défaut Express (~100 ko) → 413/500 ou corps tronqué.
|
||
app.useBodyParser('json', { limit: '15mb' });
|
||
app.useBodyParser('urlencoded', { extended: true, limit: '15mb' });
|
||
|
||
// 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, // 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,
|
||
forbidNonWhitelisted: true,
|
||
transform: true,
|
||
})
|
||
);
|
||
|
||
const configService = app.get(ConfigService);
|
||
|
||
const port = configService.get<number>('app.port', 3000);
|
||
app.setGlobalPrefix('api/v1');
|
||
|
||
const config = new DocumentBuilder()
|
||
.setTitle("P'titsPas API")
|
||
.setDescription("API pour l'application P'titsPas")
|
||
.setVersion('1.0.0')
|
||
.addBearerAuth(
|
||
{
|
||
type: 'http',
|
||
scheme: 'Bearer',
|
||
bearerFormat: 'JWT',
|
||
name: 'Authorization',
|
||
description: 'Enter JWT token',
|
||
in: 'header',
|
||
},
|
||
'access-token',
|
||
)
|
||
//.addServer('/api/v1')
|
||
.build();
|
||
|
||
const document = SwaggerModule.createDocument(app, config);
|
||
SwaggerModule.setup('api/v1/swagger', app, document);
|
||
|
||
|
||
|
||
await app.listen(port);
|
||
console.log(`✅ P'titsPas API is running on: ${await app.getUrl()}`);
|
||
}
|
||
|
||
bootstrap().catch((err) => {
|
||
console.error('❌ Error starting the application:', err);
|
||
process.exit(1);
|
||
});
|