/** * @ai-summary Fastify app configuration with feature registration * @ai-context Each feature capsule registers its routes independently */ import Fastify, { FastifyInstance } from 'fastify'; import cors from '@fastify/cors'; import helmet from '@fastify/helmet'; import fastifyMultipart from '@fastify/multipart'; // Core plugins import authPlugin from './core/plugins/auth.plugin'; import adminGuardPlugin, { setAdminGuardPool } from './core/plugins/admin-guard.plugin'; import tierGuardPlugin from './core/plugins/tier-guard.plugin'; import loggingPlugin from './core/plugins/logging.plugin'; import errorPlugin from './core/plugins/error.plugin'; import { appConfig } from './core/config/config-loader'; // Fastify feature routes import { authRoutes } from './features/auth'; import { vehiclesRoutes } from './features/vehicles/api/vehicles.routes'; import { fuelLogsRoutes } from './features/fuel-logs/api/fuel-logs.routes'; import { stationsRoutes } from './features/stations/api/stations.routes'; import { communityStationsRoutes } from './features/stations/api/community-stations.routes'; import { documentsRoutes } from './features/documents/api/documents.routes'; import { maintenanceRoutes } from './features/maintenance'; import { platformRoutes } from './features/platform'; import { adminRoutes } from './features/admin/api/admin.routes'; import { auditLogRoutes } from './features/audit-log/api/audit-log.routes'; import { notificationsRoutes } from './features/notifications'; import { userProfileRoutes } from './features/user-profile'; import { onboardingRoutes } from './features/onboarding'; import { userPreferencesRoutes } from './features/user-preferences'; import { userExportRoutes } from './features/user-export'; import { pool } from './core/config/database'; import { configRoutes } from './core/config/config.routes'; async function buildApp(): Promise { const app = Fastify({ logger: false, // Use custom logging plugin instead maxParamLength: 1000, // Required for long Google Maps photo references }); // Core middleware plugins await app.register(helmet); await app.register(cors); await app.register(loggingPlugin); await app.register(errorPlugin); // Multipart upload support with config-driven size limits const parseSizeToBytes = (val: string): number => { // Accept forms like "10MB", "5M", "1048576", "20kb", case-insensitive const s = String(val).trim().toLowerCase(); const match = s.match(/^(\d+)(b|kb|k|mb|m|gb|g)?$/i); if (!match) { // Fallback: try to parse integer bytes const n = parseInt(s, 10); return Number.isFinite(n) && n > 0 ? n : 10 * 1024 * 1024; // default 10MB } const num = parseInt(match[1], 10); const unit = match[2] || 'b'; switch (unit) { case 'b': return num; case 'k': case 'kb': return num * 1024; case 'm': case 'mb': return num * 1024 * 1024; case 'g': case 'gb': return num * 1024 * 1024 * 1024; default: return num; } }; const fileSizeLimit = parseSizeToBytes(appConfig.config.performance.max_request_size); await app.register(fastifyMultipart, { limits: { fileSize: fileSizeLimit, }, }); // Authentication plugin await app.register(authPlugin); // Admin guard plugin - initializes after auth plugin await app.register(adminGuardPlugin); setAdminGuardPool(pool); // Tier guard plugin - for subscription tier enforcement await app.register(tierGuardPlugin); // Health check app.get('/health', async (_request, reply) => { return reply.code(200).send({ status: 'healthy', timestamp: new Date().toISOString(), environment: process.env['NODE_ENV'], features: ['admin', 'auth', 'config', 'onboarding', 'vehicles', 'documents', 'fuel-logs', 'stations', 'maintenance', 'platform', 'notifications', 'user-profile', 'user-preferences', 'user-export'] }); }); // API-prefixed health for Traefik route validation and diagnostics app.get('/api/health', async (_request, reply) => { return reply.code(200).send({ status: 'healthy', scope: 'api', timestamp: new Date().toISOString(), features: ['admin', 'auth', 'config', 'onboarding', 'vehicles', 'documents', 'fuel-logs', 'stations', 'maintenance', 'platform', 'notifications', 'user-profile', 'user-preferences', 'user-export'] }); }); // Traefik forward-auth integration endpoint app.get('/auth/verify', { preHandler: [app.authenticate] }, async (request, reply) => { const user = request.user; const userId = user?.sub || 'unknown'; const rolesClaim = user?.['https://motovaultpro.com/roles']; const roles = Array.isArray(rolesClaim) ? rolesClaim : []; reply .header('X-Auth-User', userId) .header('X-Auth-Roles', roles.join(',')) .code(200) .send({ status: 'verified', userId, roles, verifiedAt: new Date().toISOString() }); }); // Register Fastify feature routes await app.register(authRoutes, { prefix: '/api' }); await app.register(onboardingRoutes, { prefix: '/api' }); await app.register(platformRoutes, { prefix: '/api' }); await app.register(vehiclesRoutes, { prefix: '/api' }); await app.register(documentsRoutes, { prefix: '/api' }); await app.register(fuelLogsRoutes, { prefix: '/api' }); await app.register(stationsRoutes, { prefix: '/api' }); await app.register(communityStationsRoutes, { prefix: '/api' }); await app.register(maintenanceRoutes, { prefix: '/api' }); await app.register(adminRoutes, { prefix: '/api' }); await app.register(auditLogRoutes, { prefix: '/api' }); await app.register(notificationsRoutes, { prefix: '/api' }); await app.register(userProfileRoutes, { prefix: '/api' }); await app.register(userPreferencesRoutes, { prefix: '/api' }); await app.register(userExportRoutes, { prefix: '/api' }); await app.register(configRoutes, { prefix: '/api' }); // 404 handler app.setNotFoundHandler(async (_request, reply) => { return reply.code(404).send({ error: 'Route not found' }); }); return app; } export { buildApp }; // For compatibility with existing server.ts let appInstance: FastifyInstance | null = null; export async function getApp(): Promise { if (!appInstance) { appInstance = await buildApp(); } return appInstance; } export default buildApp;