CognxSafeTrack
feat: backlog P0→P3 — toast system, payments, tenant isolation, feedback handler, i18n parity
6dd9bad | import fastify, { FastifyInstance } from 'fastify'; | |
| import cors from '@fastify/cors'; | |
| import multipart from '@fastify/multipart'; | |
| import jwt from '@fastify/jwt'; | |
| 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 { notificationRoutes } from './routes/notifications'; | |
| import { authRoutes } from './routes/auth'; | |
| import campaignRoutes from './routes/campaigns'; | |
| import { internalRoutes } from './routes/internal'; | |
| 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: process.env.JWT_SECRET || 'super-secret-dev-key' | |
| }); | |
| 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(notificationRoutes, { prefix: '/v1/notifications' }); | |
| scope.register(campaignRoutes, { prefix: '/v1/organizations' }); | |
| }); | |
| 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; | |
| } | |