Helmet's default 'Cross-Origin-Resource-Policy: same-origin' header was blocking the frontend (http://localhost:5173) from loading images and audio served by the backend at /uploads/*. Set policy to 'cross-origin' so images can be embedded in the frontend. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
115 lines
3.2 KiB
TypeScript
115 lines
3.2 KiB
TypeScript
import 'dotenv/config';
|
|
import { validateEnv } from './utils/env';
|
|
validateEnv();
|
|
|
|
import Fastify from 'fastify';
|
|
import { PrismaClient } from '@prisma/client';
|
|
import helmet from '@fastify/helmet';
|
|
import rateLimit from '@fastify/rate-limit';
|
|
import cors from '@fastify/cors';
|
|
import fastifyStatic from '@fastify/static';
|
|
import * as path from 'node:path';
|
|
|
|
import authPlugin from './plugins/auth';
|
|
import stripePlugin from './plugins/stripe';
|
|
import aiPlugin from './plugins/ai';
|
|
import googleAuthPlugin from './plugins/google-auth';
|
|
|
|
import authRoutes from './routes/auth';
|
|
import recipesRoutes from './routes/recipes';
|
|
import usersRoutes from './routes/users';
|
|
|
|
const fastify = Fastify({
|
|
logger: {
|
|
level: process.env.LOG_LEVEL || 'info',
|
|
},
|
|
bodyLimit: 10 * 1024 * 1024, // 10 MB
|
|
});
|
|
|
|
const prisma = new PrismaClient();
|
|
fastify.decorate('prisma', prisma);
|
|
|
|
async function bootstrap(): Promise<void> {
|
|
// --- Sécurité ---
|
|
await fastify.register(helmet, {
|
|
contentSecurityPolicy: false,
|
|
// Autorise le frontend (cross-origin) à charger les images/audio servis
|
|
// depuis /uploads/*. Sans ça, le navigateur bloque avec
|
|
// ERR_BLOCKED_BY_RESPONSE.NotSameOrigin.
|
|
crossOriginResourcePolicy: { policy: 'cross-origin' },
|
|
});
|
|
|
|
await fastify.register(rateLimit, {
|
|
max: 100,
|
|
timeWindow: '1 minute',
|
|
});
|
|
|
|
const allowedOrigins = (
|
|
process.env.CORS_ORIGINS || 'http://localhost:5173,http://127.0.0.1:5173'
|
|
)
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
|
|
await fastify.register(cors, {
|
|
origin: (origin, cb) => {
|
|
if (!origin) return cb(null, true);
|
|
if (allowedOrigins.includes(origin)) return cb(null, true);
|
|
cb(new Error(`Origin ${origin} non autorisée`), false);
|
|
},
|
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
credentials: true,
|
|
});
|
|
|
|
// --- Fichiers statiques (fallback local pour images/audio) ---
|
|
await fastify.register(fastifyStatic, {
|
|
root: path.resolve('./uploads'),
|
|
prefix: '/uploads/',
|
|
decorateReply: false,
|
|
});
|
|
|
|
// --- Plugins applicatifs ---
|
|
await fastify.register(authPlugin);
|
|
await fastify.register(stripePlugin);
|
|
await fastify.register(aiPlugin);
|
|
await fastify.register(googleAuthPlugin);
|
|
|
|
// --- Routes ---
|
|
await fastify.register(authRoutes, { prefix: '/auth' });
|
|
await fastify.register(recipesRoutes, { prefix: '/recipes' });
|
|
await fastify.register(usersRoutes, { prefix: '/users' });
|
|
|
|
fastify.get('/health', async () => ({ status: 'ok', uptime: process.uptime() }));
|
|
|
|
fastify.addHook('onClose', async () => {
|
|
await prisma.$disconnect();
|
|
});
|
|
}
|
|
|
|
const shutdown = async (signal: string): Promise<void> => {
|
|
fastify.log.info(`${signal} reçu, arrêt en cours...`);
|
|
try {
|
|
await fastify.close();
|
|
process.exit(0);
|
|
} catch (err) {
|
|
fastify.log.error(err);
|
|
process.exit(1);
|
|
}
|
|
};
|
|
process.on('SIGINT', () => void shutdown('SIGINT'));
|
|
process.on('SIGTERM', () => void shutdown('SIGTERM'));
|
|
|
|
async function start(): Promise<void> {
|
|
try {
|
|
await bootstrap();
|
|
const port = Number(process.env.PORT) || 3000;
|
|
await fastify.listen({ port, host: '0.0.0.0' });
|
|
} catch (err) {
|
|
fastify.log.error(err);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
void start();
|