import fastify from 'fastify'; import { config } from '@config/index.js'; import { setupAuthHooks } from '@core/auth/middleware.js'; import { sessionRepository } from '@core/storage/repositories/sessions.js'; import { scrapeQueueRepository } from '@core/storage/repositories/scrapeQueue.js'; import { channelRepository } from '@core/storage/repositories/channels.js'; import { userRepository } from '@core/storage/repositories/users.js'; import { voteRepository } from '@core/storage/repositories/votes.js'; import { banRepository } from '@core/storage/repositories/bans.js'; import { logRepository } from '@core/storage/repositories/logs.js'; // Import routes import { authRoutes } from '@modules/auth/routes.js'; import { channelRoutes } from '@modules/channels/routes.js'; import { searchRoutes } from '@modules/search/routes.js'; import { userRoutes } from '@modules/user/routes.js'; import { adminRoutes } from '@modules/admin/routes.js'; async function buildApp() { const app = fastify({ logger: { level: config.LOG_LEVEL, transport: config.NODE_ENV === 'development' ? { target: 'pino-pretty' } : undefined, }, trustProxy: true, }); // Register plugins await app.register(import('@fastify/cookie'), { secret: config.SESSION_SECRET, parseOptions: {}, }); await app.register(import('@fastify/session'), { secret: config.SESSION_SECRET, cookie: { secure: config.NODE_ENV === 'production', httpOnly: true, sameSite: 'lax', maxAge: config.sessionTtlMs, path: '/', }, saveUninitialized: false, rolling: true, store: { get: async (sid: string) => { const session = await sessionRepository.findValidSession(sid); return session?.data || null; }, set: async (sid: string, data: Record, ttl: number) => { // Handled by sessionRepository }, destroy: async (sid: string) => { await sessionRepository.deleteSession(sid); }, }, }); await app.register(import('@fastify/csrf-protection'), { sessionPlugin: '@fastify/session', cookieOpts: { signed: true }, }); await app.register(import('@fastify/rate-limit'), { max: 100, timeWindow: '1 minute', keyGenerator: (req) => req.ip, }); await app.register(import('@fastify/helmet'), { contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'], fontSrc: ["'self'", 'https://fonts.gstatic.com'], imgSrc: ["'self'", 'data:', 'https:'], connectSrc: ["'self'"], }, }, }); await app.register(import('@fastify/static'), { root: './public', prefix: '/public/', }); await app.register(import('@fastify/view'), { engine: { nunjucks: await import('nunjucks'), }, templates: './src/views', options: { autoescape: true, noCache: config.NODE_ENV === 'development', }, }); // Initialize repositories await Promise.all([ userRepository.initialize(), channelRepository.initialize(), voteRepository.initialize(), banRepository.initialize(), logRepository.initialize(), sessionRepository.initialize(), scrapeQueueRepository.initialize(), ]); // Setup auth hooks setupAuthHooks(app); // Health check app.get('/health', async () => ({ status: 'ok', timestamp: new Date().toISOString() })); // Register modules await app.register(authRoutes, { prefix: '/auth' }); await app.register(channelRoutes, { prefix: '/channels' }); await app.register(searchRoutes, { prefix: '/' }); await app.register(userRoutes, { prefix: '/user' }); await app.register(adminRoutes, { prefix: '/admin' }); // 404 handler app.setNotFoundHandler(async (request, reply) => { return reply.status(404).view('pages/404.njk', { url: request.url }); }); // Error handler app.setErrorHandler(async (error, request, reply) => { request.log.error(error); await logRepository.log({ level: 'error', message: error.message, context: { url: request.url, method: request.method }, error: { name: error.name, message: error.message, stack: error.stack }, userId: request.user?.id, ip: request.ip, userAgent: request.headers['user-agent'], url: request.url, method: request.method, statusCode: error.statusCode || 500, }); const statusCode = error.statusCode || 500; return reply.status(statusCode).view('pages/error.njk', { error: config.NODE_ENV === 'development' ? error.message : 'Internal Server Error', statusCode, }); }); return app; } async function start() { try { const app = await buildApp(); await app.listen({ port: config.PORT, host: '0.0.0.0' }); console.log(`🚀 Server running at ${config.APP_URL}`); } catch (err) { console.error('Failed to start server:', err); process.exit(1); } } if (import.meta.url === `file://${process.argv[1]}`) { start(); } export { buildApp };