CognxSafeTrack Claude Sonnet 4.6 commited on
Commit ·
cbaf159
1
Parent(s): a2d334c
fix(security): wire auth middleware chain in app.ts preHandler
Browse filesvalidateApiKey, verifyJwt, enforceOrgIsolation were created during
the debt refactor but never imported — admin routes were reachable
without a valid JWT token.
Chain: API key (worker) → skip JWT | user → jwtVerify →
enforceOrgIsolation → injectTenantConfig → runWithTenant
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- apps/api/src/app.ts +91 -0
apps/api/src/app.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import fastify, { FastifyInstance } from 'fastify';
|
| 2 |
+
import cors from '@fastify/cors';
|
| 3 |
+
import multipart from '@fastify/multipart';
|
| 4 |
+
import jwt from '@fastify/jwt';
|
| 5 |
+
import { prisma } from './services/prisma';
|
| 6 |
+
import { runWithTenant } from '@repo/database';
|
| 7 |
+
import { whatsappRoutes } from './routes/whatsapp';
|
| 8 |
+
import { studentRoutes } from './routes/student';
|
| 9 |
+
import { adminRoutes } from './routes/admin';
|
| 10 |
+
import { organizationRoutes } from './routes/organizations';
|
| 11 |
+
import { aiRoutes } from './routes/ai';
|
| 12 |
+
import { paymentRoutes } from './routes/payments';
|
| 13 |
+
import { analyticsRoutes } from './routes/analytics';
|
| 14 |
+
import { notificationRoutes } from './routes/notifications';
|
| 15 |
+
import { authRoutes } from './routes/auth';
|
| 16 |
+
import campaignRoutes from './routes/campaigns';
|
| 17 |
+
import { setupErrorHandler } from './utils/errors';
|
| 18 |
+
import { injectTenantConfig } from './middleware/tenant';
|
| 19 |
+
import { validateApiKey } from './middleware/validateApiKey';
|
| 20 |
+
import { verifyJwt } from './middleware/verifyJwt';
|
| 21 |
+
import { enforceOrgIsolation } from './middleware/enforceOrgIsolation';
|
| 22 |
+
|
| 23 |
+
export async function buildApp() {
|
| 24 |
+
const server: FastifyInstance = fastify({
|
| 25 |
+
logger: process.env.NODE_ENV === 'test' ? false : true,
|
| 26 |
+
disableRequestLogging: process.env.NODE_ENV === 'production'
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
server.decorate('prisma', prisma);
|
| 30 |
+
|
| 31 |
+
const corsOrigins = process.env.CORS_ORIGINS
|
| 32 |
+
? process.env.CORS_ORIGINS.split(',').map(o => o.trim())
|
| 33 |
+
: ['https://admin.xamle.studio', 'https://xamle.studio'];
|
| 34 |
+
|
| 35 |
+
await server.register(cors, {
|
| 36 |
+
origin: corsOrigins,
|
| 37 |
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
| 38 |
+
allowedHeaders: ['Content-Type', 'Authorization', 'x-api-key', 'x-organization-id'],
|
| 39 |
+
credentials: true
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
await server.register(multipart, {
|
| 43 |
+
limits: { fileSize: 10 * 1024 * 1024 }
|
| 44 |
+
});
|
| 45 |
+
|
| 46 |
+
await server.register(jwt, {
|
| 47 |
+
secret: process.env.JWT_SECRET || 'super-secret-dev-key'
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
setupErrorHandler(server);
|
| 51 |
+
|
| 52 |
+
// Routes & Hooks
|
| 53 |
+
server.register(async (scope) => {
|
| 54 |
+
scope.addHook('preHandler', async (request, reply) => {
|
| 55 |
+
const isApiKey = await validateApiKey(request);
|
| 56 |
+
|
| 57 |
+
if (isApiKey) {
|
| 58 |
+
request.organizationId = request.headers['x-organization-id'] as string;
|
| 59 |
+
} else {
|
| 60 |
+
await verifyJwt(request, reply);
|
| 61 |
+
if (reply.sent) return;
|
| 62 |
+
|
| 63 |
+
await enforceOrgIsolation(request, reply);
|
| 64 |
+
if (reply.sent) return;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
await injectTenantConfig(request, reply);
|
| 68 |
+
if (reply.sent) return;
|
| 69 |
+
|
| 70 |
+
if (request.organizationId) {
|
| 71 |
+
return new Promise((resolve) => {
|
| 72 |
+
runWithTenant(request.organizationId as string, resolve);
|
| 73 |
+
});
|
| 74 |
+
}
|
| 75 |
+
});
|
| 76 |
+
|
| 77 |
+
scope.register(adminRoutes, { prefix: '/v1/admin' });
|
| 78 |
+
scope.register(organizationRoutes, { prefix: '/v1/organizations' });
|
| 79 |
+
scope.register(aiRoutes, { prefix: '/v1/ai' });
|
| 80 |
+
scope.register(paymentRoutes, { prefix: '/v1/payments' });
|
| 81 |
+
scope.register(analyticsRoutes, { prefix: '/v1/analytics' });
|
| 82 |
+
scope.register(notificationRoutes, { prefix: '/v1/notifications' });
|
| 83 |
+
scope.register(campaignRoutes, { prefix: '/v1/campaigns' });
|
| 84 |
+
});
|
| 85 |
+
|
| 86 |
+
server.register(whatsappRoutes, { prefix: '/v1/whatsapp' });
|
| 87 |
+
server.register(studentRoutes, { prefix: '/v1/student' });
|
| 88 |
+
server.register(authRoutes, { prefix: '/v1/auth' });
|
| 89 |
+
|
| 90 |
+
return server;
|
| 91 |
+
}
|