import fastify, { FastifyInstance } from 'fastify'; import cors from '@fastify/cors'; import multipart from '@fastify/multipart'; import jwt from '@fastify/jwt'; import rateLimit from '@fastify/rate-limit'; import { prisma } from './services/prisma'; import { runWithTenant } from '@repo/database'; import { whatsappRoutes } from './routes/whatsapp'; import { studentRoutes } from './routes/student'; import { adminRoutes } from './routes/admin'; import { organizationRoutes } from './routes/organizations'; import { aiRoutes } from './routes/ai'; import { paymentRoutes } from './routes/payments'; import { analyticsRoutes } from './routes/analytics'; import { billingRoutes } from './routes/billing'; import { notificationRoutes } from './routes/notifications'; import { authRoutes } from './routes/auth'; import campaignRoutes from './routes/campaigns'; import { internalRoutes } from './routes/internal'; import { superAdminRoutes } from './routes/super-admin'; import { setupErrorHandler } from './utils/errors'; import { injectTenantConfig } from './middleware/tenant'; import { validateApiKey } from './middleware/validateApiKey'; import { verifyJwt } from './middleware/verifyJwt'; import { enforceOrgIsolation } from './middleware/enforceOrgIsolation'; export async function buildApp() { const server: FastifyInstance = fastify({ logger: process.env.NODE_ENV === 'test' ? false : true, disableRequestLogging: process.env.NODE_ENV === 'production' }); server.decorate('prisma', prisma); const corsOrigins = process.env.CORS_ORIGINS ? process.env.CORS_ORIGINS.split(',').map(o => o.trim()) : ['https://admin.xamle.studio', 'https://xamle.studio']; await server.register(cors, { origin: corsOrigins, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'x-api-key', 'x-organization-id'], credentials: true }); await server.register(multipart, { limits: { fileSize: 10 * 1024 * 1024 } }); await server.register(jwt, { secret: (() => { if (!process.env.JWT_SECRET) throw new Error('JWT_SECRET env var is required'); return process.env.JWT_SECRET; })() }); await server.register(rateLimit, { max: 200, timeWindow: '1 minute', keyGenerator: (req) => req.ip }); setupErrorHandler(server); // Routes & Hooks server.register(async (scope) => { scope.addHook('preHandler', async (request, reply) => { const isApiKey = await validateApiKey(request); if (isApiKey) { request.organizationId = request.headers['x-organization-id'] as string; } else { await verifyJwt(request, reply); if (reply.sent) return; await enforceOrgIsolation(request, reply); if (reply.sent) return; } await injectTenantConfig(request, reply); if (reply.sent) return; if (request.organizationId) { return new Promise((resolve) => { runWithTenant(request.organizationId as string, resolve); }); } }); scope.register(adminRoutes, { prefix: '/v1/admin' }); scope.register(organizationRoutes, { prefix: '/v1/organizations' }); scope.register(aiRoutes, { prefix: '/v1/ai' }); scope.register(paymentRoutes, { prefix: '/v1/payments' }); scope.register(analyticsRoutes, { prefix: '/v1/analytics' }); scope.register(billingRoutes, { prefix: '/v1/billing' }); scope.register(notificationRoutes, { prefix: '/v1/notifications' }); scope.register(campaignRoutes, { prefix: '/v1/organizations' }); }); // Super-admin routes — JWT-only scope, deliberately NO runWithTenant so Prisma // queries are cross-org (tenant extension only filters when AsyncLocalStorage is set). server.register(async (scope) => { scope.addHook('preHandler', async (request, reply) => { await verifyJwt(request, reply); }); scope.register(superAdminRoutes, { prefix: '/v1/super-admin' }); }); server.register(whatsappRoutes, { prefix: '/v1/whatsapp' }); server.register(studentRoutes, { prefix: '/v1/student' }); server.register(authRoutes, { prefix: '/v1/auth' }); // Internal routes (worker→API calls, API-key only — no JWT, no tenant injection) server.register(async (scope) => { scope.addHook('preHandler', async (request, reply) => { const isApiKey = await validateApiKey(request); if (!isApiKey) { reply.code(401).send({ error: 'Unauthorized' }); } }); scope.register(internalRoutes); }); return server; }