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 { // --- 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 => { 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 { 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();