import Fastify, { type FastifyInstance } from 'fastify'; import helmet from '@fastify/helmet'; import sensible from '@fastify/sensible'; import { ZodError } from 'zod'; import { env } from './config/env.js'; import { HttpError } from './utils/errors.js'; import prismaPlugin from './plugins/prisma.js'; import corsPlugin from './plugins/cors.js'; import rateLimitPlugin from './plugins/rate-limit.js'; import authPlugin from './plugins/auth.js'; import guestPlugin from './plugins/guest.js'; import staticPlugin from './plugins/static.js'; import multipartPlugin from './plugins/multipart.js'; import authRoutes from './modules/auth/auth.routes.js'; import profileRoutes from './modules/profile/profile.routes.js'; import wishesRoutes from './modules/wishes/wishes.routes.js'; import imagesRoutes from './modules/images/images.routes.js'; import publicRoutes from './modules/public/public.routes.js'; import metaRoutes from './modules/meta/meta.routes.js'; import { registerPurgeTrashJob } from './jobs/purge-trash.js'; export async function buildApp(): Promise { const app = Fastify({ logger: { level: env.LOG_LEVEL, transport: env.NODE_ENV === 'development' ? { target: 'pino-pretty', options: { translateTime: 'HH:MM:ss', singleLine: true } } : undefined, redact: { paths: ['req.body.password', 'req.headers.cookie', 'req.headers.authorization'], remove: true, }, }, trustProxy: true, bodyLimit: 1 * 1024 * 1024, }); app.setErrorHandler((err, request, reply) => { if (err instanceof HttpError) { return reply.code(err.statusCode).send({ error: err.code, message: err.message, details: err.details, }); } if (err instanceof ZodError) { return reply.code(400).send({ error: 'VALIDATION', message: 'Invalid input', details: err.flatten(), }); } if ((err as { statusCode?: number }).statusCode === 429) { return reply.code(429).send({ error: 'RATE_LIMITED', message: err.message }); } request.log.error({ err }, 'Unhandled error'); return reply.code(500).send({ error: 'INTERNAL', message: 'Internal server error' }); }); await app.register(helmet, { contentSecurityPolicy: false, crossOriginResourcePolicy: { policy: 'cross-origin' }, }); await app.register(sensible); await app.register(corsPlugin); await app.register(rateLimitPlugin); await app.register(authPlugin); await app.register(guestPlugin); await app.register(staticPlugin); await app.register(multipartPlugin); await app.register(prismaPlugin); await app.register(metaRoutes, { prefix: '/api' }); await app.register(authRoutes, { prefix: '/api/auth' }); await app.register(profileRoutes, { prefix: '/api/profile' }); await app.register(wishesRoutes, { prefix: '/api/wishes' }); await app.register(imagesRoutes, { prefix: '/api/wishes' }); await app.register(publicRoutes, { prefix: '/api/public' }); registerPurgeTrashJob(app); return app; }