CognxSafeTrack Claude Sonnet 4.6 commited on
Commit
cbaf159
·
1 Parent(s): a2d334c

fix(security): wire auth middleware chain in app.ts preHandler

Browse files

validateApiKey, 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>

Files changed (1) hide show
  1. 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
+ }