freedge/backend/src/server.ts
ordinarthur e80341bc0c fix(server): allow cross-origin resource loading for /uploads
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>
2026-04-08 12:41:36 +02:00

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();