edtech / apps /api /src /app.ts
CognxSafeTrack
fix(super-admin): move routes to JWT-only scope to bypass tenant extension
ea8815c
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;
}