CognxSafeTrack commited on
Commit
071d56f
Β·
1 Parent(s): 41e0fe9

fix: restore and stabilize API index with clean route registration

Browse files
Files changed (1) hide show
  1. apps/api/src/index.ts +87 -341
apps/api/src/index.ts CHANGED
@@ -1,390 +1,136 @@
1
- import { logger } from './logger';
2
- import dns from 'node:dns';
3
- dns.setDefaultResultOrder('ipv4first');
4
-
5
- import Fastify from 'fastify';
6
- import fastifyStatic from '@fastify/static';
7
- import fastifyMultipart from '@fastify/multipart';
8
- import fastifyRateLimit from '@fastify/rate-limit';
9
- import fastifyJwt from '@fastify/jwt';
10
  import cors from '@fastify/cors';
 
 
 
11
  import { whatsappRoutes } from './routes/whatsapp';
 
12
  import { adminRoutes } from './routes/admin';
 
13
  import { aiRoutes } from './routes/ai';
14
  import { paymentRoutes, stripeWebhookRoute } from './routes/payments';
15
- import { internalRoutes } from './routes/internal';
16
- import { studentRoutes } from './routes/student';
17
- import { organizationRoutes } from './routes/organizations';
18
  import { analyticsRoutes } from './routes/analytics';
19
- import { startCleanupCron } from './services/cleanup';
20
- import { runWithTenant } from '@repo/database';
21
-
22
- declare module 'fastify' {
23
- interface FastifyInstance {
24
- prisma: any;
25
- }
26
- interface FastifyRequest {
27
- rawBody?: Buffer;
28
- }
29
- interface FastifyContextConfig {
30
- requireAuth?: boolean;
31
- }
32
- }
33
-
34
- declare module '@fastify/jwt' {
35
- interface FastifyJWT {
36
- user: {
37
- id: string;
38
- organizationId: string;
39
- role: 'STUDENT' | 'ORG_MEMBER' | 'ORG_ADMIN' | 'SUPER_ADMIN' | 'ADMIN';
40
- };
41
- }
42
- }
43
-
44
-
45
- // ── Fail-fast: check critical secrets on startup ───────────────────────────────
46
- // Only WHATSAPP_VERIFY_TOKEN is strictly needed at startup (for Meta webhook validation).
47
- // All other secrets are validated lazily (guarded routes return 503 if missing).
48
- const REQUIRED_ENV = ['WHATSAPP_VERIFY_TOKEN', 'JWT_SECRET'];
49
- const WARN_ENV = ['ADMIN_API_KEY', 'WHATSAPP_APP_SECRET'];
50
-
51
- for (const key of REQUIRED_ENV) {
52
- if (!process.env[key]) {
53
- logger.error(`[STARTUP] ❌ Missing required environment variable: ${key}`);
54
- process.exit(1);
55
- }
56
- }
57
- for (const key of WARN_ENV) {
58
- if (!process.env[key]) {
59
- logger.warn(`[STARTUP] ⚠️ ${key} not set β€” related features will be degraded`);
60
- }
61
- }
62
 
63
- // ─── SERVER SETUP (Gateway Role) ─────────────────────────────────────────────
64
- // This API acts as a public gateway, forwarding webhooks and handling admin routes.
65
- // It uses a bridge to communicate with the private WhatsApp worker.
66
- const server = Fastify({
67
  logger: true,
68
- ignoreTrailingSlash: true
69
  });
70
 
71
- // ── CORS ───────────────────────────────────────────────────────────────────────
72
  server.register(cors, {
73
- origin: true, // Allow all origins
74
- methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
75
- allowedHeaders: ['Content-Type', 'Authorization', 'x-organization-id']
 
76
  });
77
 
78
- // ── JWT ────────────────────────────────────────────────────────────────────────
79
- server.register(fastifyJwt, {
80
- secret: process.env.JWT_SECRET || 'dev-secret-keep-it-secret-keep-it-safe'
81
  });
82
 
83
- // ── Static & Multipart ────────────────────────────────────────────────────────
84
- server.register(fastifyStatic, {
85
- root: '/tmp',
86
- prefix: '/public/',
87
  });
88
- server.register(fastifyMultipart);
89
 
90
- // ── Prisma ─────────────────────────────────────────────────────────────────────
91
- import { prisma } from './services/prisma';
92
- server.decorate('prisma', prisma);
93
 
 
 
 
 
 
94
 
95
- // ── Rate Limiting ─────────────────────────────────────────────────────────────
96
- async function setupRateLimit() {
97
- await server.register(fastifyRateLimit, {
98
- global: true,
99
- max: 100,
100
- timeWindow: '15 minutes',
101
- errorResponseBuilder: (_request, context) => ({
102
- statusCode: 429,
103
- error: 'Too Many Requests',
104
- message: `Rate limit exceeded. Try again in ${context.after}.`,
105
- date: new Date(),
106
- expiresIn: context.after
107
- })
108
- });
109
- logger.info('[RATE-LIMIT] Global rate limiting enabled (100 req / 15 min)');
110
- }
111
 
112
- import { authRoutes } from './routes/auth';
113
-
114
- // ── Public Routes (no auth) ────────────────────────────────────────────────────
115
- logger.info('[STARTUP] Registering Auth routes...');
116
- server.register(authRoutes, { prefix: '/v1/auth' });
117
- logger.info('[STARTUP] Registering WhatsApp routes...');
118
- server.register(whatsappRoutes, { prefix: '/v1/whatsapp' });
119
- logger.info('[STARTUP] Registering Student routes...');
120
- server.register(studentRoutes, { prefix: '/v1/student' });
121
-
122
- // ── Stripe Webhook (public β€” Stripe can't send API Key, verified by signature) ─
123
- server.register(stripeWebhookRoute, { prefix: '/v1/payments' });
124
-
125
- // ── Private Routes (guarded by API Key or JWT) ─────────────────────────────────
126
- server.register(async function guardedRoutes(scope) {
127
- scope.addHook('onRequest', async (request, reply) => {
128
- if (request.method === 'OPTIONS') return;
129
-
130
- const apiKey = process.env.ADMIN_API_KEY;
131
- const authHeader = request.headers['authorization'];
132
-
133
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
134
- return reply.code(401).send({ error: 'Unauthorized', message: 'Missing Authorization header' });
135
- }
136
 
137
- const token = authHeader.slice(7);
138
-
139
- // 1. Try Legacy API Key (Super Admin)
140
- if (apiKey && token === apiKey) {
141
- request.user = {
142
- id: 'super-admin',
143
- organizationId: request.headers['x-organization-id'] as string || 'default-org-id',
144
- role: 'SUPER_ADMIN'
145
- };
146
- }
147
- // 2. Try JWT (Organization Admin/Member)
148
- else {
149
  try {
150
- const decoded = await request.jwtVerify() as any;
151
- request.user = decoded;
152
  } catch (err) {
153
- return reply.code(401).send({ error: 'Unauthorized', message: 'Invalid token or API key' });
154
  }
155
- }
156
-
157
- // 🏒 Multi-Tenant Enforcement
158
- const requestedOrgId = request.headers['x-organization-id'] as string;
159
- const user = request.user as any;
160
-
161
- // Ensure requested Org matches User's Org (unless Super Admin)
162
- if (user.role !== 'SUPER_ADMIN' && requestedOrgId && requestedOrgId !== user.organizationId) {
163
- return reply.code(403).send({ error: 'Forbidden', message: 'You do not have access to this organization' });
164
- }
165
-
166
- // Auto-inject OrgId into request if missing and available in token
167
- if (!requestedOrgId && user.organizationId) {
168
- request.headers['x-organization-id'] = user.organizationId;
169
- }
170
-
171
- // Final check for routes requiring an organization
172
- const isOrgIndependentRoute = request.url.startsWith('/v1/organizations') || request.url.startsWith('/v1/internal');
173
- if (!request.headers['x-organization-id'] && !isOrgIndependentRoute) {
174
- return reply.code(400).send({ error: 'Bad Request', message: 'Missing organization context' });
175
- }
176
-
177
- if (request.headers['x-organization-id']) {
178
- request.log.info(`[CONTEXT] Setting Organization Context: ${request.headers['x-organization-id']}`);
179
- }
180
- });
181
-
182
- scope.addHook('preHandler', (request, _reply, done) => {
183
- const orgId = request.headers['x-organization-id'] as string;
184
- if (orgId) {
185
- runWithTenant(orgId, done);
186
- } else {
187
- done();
188
- }
189
- });
190
-
191
- logger.info('[STARTUP] Entering guarded routes scope...');
192
-
193
- scope.register(async (adminScope) => {
194
- logger.info('[STARTUP] Registering Admin routes...');
195
- await adminScope.register(adminRoutes, { prefix: '/v1/admin' });
196
- });
197
-
198
- scope.register(async (orgScope) => {
199
- logger.info('[STARTUP] Registering Organization routes...');
200
- await orgScope.register(organizationRoutes, { prefix: '/v1/organizations' });
201
- });
202
 
203
- scope.register(async (aiScope) => {
204
- logger.info('[STARTUP] Registering AI routes...');
205
- await aiScope.register(aiRoutes, { prefix: '/v1/ai' });
206
- });
207
-
208
- scope.register(async (payScope) => {
209
- logger.info('[STARTUP] Registering Payment routes...');
210
- await payScope.register(paymentRoutes, { prefix: '/v1/payments' });
211
- });
212
-
213
- scope.register(async (anaScope) => {
214
- logger.info('[STARTUP] Registering Analytics routes...');
215
- await anaScope.register(analyticsRoutes, { prefix: '/v1/analytics' });
216
- });
217
-
218
- scope.register(async (intScope) => {
219
- logger.info('[STARTUP] Registering Internal routes...');
220
- await intScope.register(internalRoutes);
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  });
222
- });
223
-
224
- logger.info('[STARTUP] All routes registered. Preparing to listen...');
225
 
226
- // ── Health Routes (public) ─────────────────────────────────────────────────────
227
- server.get('/', async () => {
228
- return {
229
- status: 'ok',
230
- message: 'EdTech API Gateway is running',
231
- version: '1.1.0',
232
- timestamp: new Date().toISOString()
233
- };
234
- });
235
 
236
- // Health route is defined below with DB check
 
237
 
238
- server.get('/debug/net', async (_req, reply) => {
239
  try {
240
- const res = await fetch('https://www.google.com', { method: 'GET' });
241
- return reply.send({ ok: true, status: res.status });
242
- } catch (e: unknown) {
243
- return reply.code(500).send({ ok: false, error: (e as any)?.message || String(e) });
244
  }
245
  });
246
 
247
- server.get('/debug/graph', async (_req, reply) => {
248
- try {
249
- const res = await fetch('https://graph.facebook.com', { method: 'GET' });
250
- return reply.send({ ok: true, status: res.status });
251
- } catch (e: unknown) {
252
- return reply.code(500).send({ ok: false, error: e instanceof Error ? e.message : String(e) });
253
- }
254
- });
255
-
256
- server.get('/health', async (_request, reply) => {
257
- try {
258
- await server.prisma.$queryRaw`SELECT 1`;
259
- return { status: 'ok', timestamp: new Date().toISOString(), db: 'connected' };
260
- } catch (e: unknown) {
261
- return reply.code(500).send({ status: 'error', error: e instanceof Error ? e.message : String(e), dbUrl: process.env.DATABASE_URL?.replace(/:([^:@]+)@/, ':****@') });
262
- }
263
- });
264
-
265
- // ── Privacy Policy (required by Meta for app publication) ──────────────────────
266
- server.get('/v1/privacy', async (_req, reply) => {
267
- const html = `<!DOCTYPE html>
268
- <html lang="fr">
269
- <head>
270
- <meta charset="UTF-8" />
271
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
272
- <title>Politique de Confidentialité — XAMLÉ Studio</title>
273
- <style>
274
- body { font-family: system-ui, sans-serif; max-width: 760px; margin: 40px auto; padding: 0 20px; color: #1e293b; line-height: 1.7; }
275
- h1 { font-size: 2rem; margin-bottom: 4px; }
276
- h2 { font-size: 1.2rem; margin-top: 2rem; color: #0f172a; }
277
- p, li { color: #334155; }
278
- a { color: #059669; }
279
- .updated { color: #94a3b8; font-size: 0.9rem; }
280
- </style>
281
- </head>
282
- <body>
283
- <h1>Politique de ConfidentialitΓ©</h1>
284
- <p class="updated">Dernière mise à jour : 7 mars 2026</p>
285
-
286
- <h2>1. Qui sommes-nous ?</h2>
287
- <p>XAMLÉ Studio est une plateforme d'éducation entrepreneuriale accessible via WhatsApp, opérée par xamlé.studio. Contact : <a href="mailto:contact@xamle.studio">contact@xamle.studio</a></p>
288
-
289
- <h2>2. DonnΓ©es collectΓ©es</h2>
290
- <p>Lors de votre inscription et utilisation du service, nous collectons :</p>
291
- <ul>
292
- <li>Votre numΓ©ro de tΓ©lΓ©phone WhatsApp</li>
293
- <li>La langue choisie (FranΓ§ais ou Wolof)</li>
294
- <li>Votre secteur d'activitΓ© / projet professionnel (fourni volontairement)</li>
295
- <li>Vos rΓ©ponses aux exercices et contenus pΓ©dagogiques</li>
296
- <li>Les mΓ©tadonnΓ©es de vos messages (horodatage, type de message)</li>
297
- </ul>
298
-
299
- <h2>3. Utilisation des donnΓ©es</h2>
300
- <p>Vos donnΓ©es sont utilisΓ©es pour :</p>
301
- <ul>
302
- <li>Vous envoyer des leΓ§ons quotidiennes personnalisΓ©es via WhatsApp</li>
303
- <li>Personnaliser le contenu pΓ©dagogique Γ  votre secteur d'activitΓ©</li>
304
- <li>GΓ©nΓ©rer des documents AI (One-Pager PDF, Pitch Deck) basΓ©s sur votre parcours</li>
305
- <li>AmΓ©liorer la qualitΓ© de nos formations</li>
306
- <li>Traiter vos paiements pour les formations premium</li>
307
- </ul>
308
-
309
- <h2>4. Partage des donnΓ©es</h2>
310
- <p>Nous ne vendons jamais vos donnΓ©es. Elles peuvent Γͺtre partagΓ©es uniquement avec :</p>
311
- <ul>
312
- <li><strong>Meta / WhatsApp</strong> β€” pour l'acheminement des messages</li>
313
- <li><strong>OpenAI</strong> β€” pour la gΓ©nΓ©ration et personnalisation du contenu pΓ©dagogique</li>
314
- <li><strong>Stripe</strong> β€” pour le traitement sΓ©curisΓ© des paiements</li>
315
- <li><strong>Cloudflare</strong> β€” pour le stockage des documents gΓ©nΓ©rΓ©s</li>
316
- </ul>
317
-
318
- <h2>5. Conservation des donnΓ©es</h2>
319
- <p>Vos donnΓ©es sont conservΓ©es pendant toute la durΓ©e de votre inscription active. Vous pouvez demander la suppression de vos donnΓ©es Γ  tout moment en envoyant un e-mail Γ  <a href="mailto:contact@xamle.studio">contact@xamle.studio</a>.</p>
320
-
321
- <h2>6. Vos droits</h2>
322
- <p>ConformΓ©ment au RGPD et aux lois applicables, vous disposez du droit de :</p>
323
- <ul>
324
- <li>AccΓ©der Γ  vos donnΓ©es personnelles</li>
325
- <li>Corriger les donnΓ©es inexactes</li>
326
- <li>Demander la suppression de vos donnΓ©es</li>
327
- <li>Vous opposer au traitement de vos donnΓ©es</li>
328
- </ul>
329
- <p>Pour exercer ces droits, contactez-nous Γ  : <a href="mailto:contact@xamle.studio">contact@xamle.studio</a></p>
330
-
331
- <h2>7. SΓ©curitΓ©</h2>
332
- <p>Vos données sont protégées par chiffrement (TLS en transit, AES au repos). L'accès aux données est strictement limité aux systèmes nécessaires au fonctionnement du service.</p>
333
-
334
- <h2>8. Modifications</h2>
335
- <p>Cette politique peut Γͺtre mise Γ  jour. En cas de modification majeure, vous serez informΓ© via WhatsApp.</p>
336
-
337
- <h2>9. Contact</h2>
338
- <p>Pour toute question relative Γ  cette politique : <a href="mailto:contact@xamle.studio">contact@xamle.studio</a></p>
339
- </body>
340
- </html>`;
341
- return reply.code(200).type('text/html').send(html);
342
- });
343
-
344
-
345
-
346
- // ── Error Handler ─────────────────────────────────────────────────────────────
347
- server.setErrorHandler((error, request, reply) => {
348
- logger.error({
349
- msg: `[API-ERROR] ${request.method} ${request.url}`,
350
- error: error.message,
351
- stack: error.stack,
352
- headers: request.headers,
353
- body: request.body
354
- });
355
-
356
- reply.status(error.statusCode || 500).send({
357
- error: 'Internal Server Error',
358
- message: error.message,
359
- statusCode: error.statusCode || 500
360
- });
361
- });
362
-
363
- // ── Start Server ───────────────────────────────────────────────────────────────
364
  const start = async () => {
365
  try {
 
 
366
  const port = parseInt(process.env.PORT || '8080');
367
  logger.info(`[STARTUP] Attempting to listen on port ${port}...`);
368
 
369
- const isGateway = process.env.IS_GATEWAY === 'true' || process.env.HF_SPACE_ID !== undefined;
370
-
371
- // 1. Listen IMMEDIATELY so Railway sees the port as open
372
  await server.listen({ port, host: '0.0.0.0' });
373
  logger.info(`πŸš€ Server listening on http://0.0.0.0:${port}`);
374
- logger.info(`[STARTUP] Mode: ${isGateway ? 'GATEWAY (Forwarding Only)' : 'DIRECT (Processing)'}`);
375
 
376
- // 2. Initialize background services
377
- await setupRateLimit().catch(err => logger.error('[STARTUP] Rate limit setup failed:', err));
378
  startCleanupCron();
379
 
380
- logger.info('[STARTUP] Background services initialized.');
381
  } catch (err: any) {
382
  logger.error('[STARTUP] ❌ FATAL ERROR DURING STARTUP:');
383
  logger.error(err.message || String(err));
384
- if (err.stack) logger.error(err.stack);
385
-
386
- // Ensure logs are flushed
387
- console.error('CRITICAL STARTUP ERROR:', err);
388
  process.exit(1);
389
  }
390
  };
 
1
+ import 'dotenv/config';
2
+ import fastify, { FastifyInstance } from 'fastify';
 
 
 
 
 
 
 
3
  import cors from '@fastify/cors';
4
+ import { logger } from './logger';
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, stripeWebhookRoute } from './routes/payments';
 
 
 
13
  import { analyticsRoutes } from './routes/analytics';
14
+ import { internalRoutes } from './routes/internal';
15
+ import { authRoutes } from './routes/auth';
16
+ import { setupRateLimit } from './middleware/rate-limit';
17
+ import { startCleanupCron } from './scripts/cleanup-temp-files';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
+ const server: FastifyInstance = fastify({
 
 
 
20
  logger: true,
21
+ disableRequestLogging: process.env.NODE_ENV === 'production'
22
  });
23
 
24
+ // ── Middleware & Plugins ──────────────────────────────────────────────────────
25
  server.register(cors, {
26
+ origin: true, // Allow all for production debugging, can be restricted later
27
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
28
+ allowedHeaders: ['Content-Type', 'Authorization', 'x-api-key', 'x-organization-id'],
29
+ credentials: true
30
  });
31
 
32
+ server.register(require('@fastify/multipart'), {
33
+ limits: { fileSize: 10 * 1024 * 1024 } // 10MB
 
34
  });
35
 
36
+ server.register(require('@fastify/jwt'), {
37
+ secret: process.env.JWT_SECRET || '6f8d2e5b9a1c4f7b3e8d2a6c0b9e4f1a7d3c5b8e2a4c1f9b0d6e3a7c5b9e4f2d'
 
 
38
  });
 
39
 
40
+ // ── Route Registration ────────────────────────────────────────────────────────
41
+ const registerRoutes = async () => {
42
+ logger.info('[STARTUP] Registering routes...');
43
 
44
+ // 1. Public Routes
45
+ server.register(authRoutes, { prefix: '/v1/auth' });
46
+ server.register(whatsappRoutes, { prefix: '/v1/whatsapp' });
47
+ server.register(studentRoutes, { prefix: '/v1/student' });
48
+ server.register(stripeWebhookRoute, { prefix: '/v1/payments' });
49
 
50
+ // 2. Guarded Routes
51
+ server.register(async (scope) => {
52
+ scope.addHook('onRequest', async (request, reply) => {
53
+ if (request.method === 'OPTIONS') return;
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
+ const apiKey = process.env.ADMIN_API_KEY;
56
+
57
+ // Allow Admin API Key
58
+ if (apiKey && request.headers['x-api-key'] === apiKey) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
+ // Otherwise check JWT
 
 
 
 
 
 
 
 
 
 
 
61
  try {
62
+ await request.jwtVerify();
 
63
  } catch (err) {
64
+ return reply.code(401).send({ error: 'Unauthorized', message: 'Invalid or missing token' });
65
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
+ // Multi-Tenant Enforcement
68
+ const user = (request as any).user;
69
+ const requestedOrgId = request.headers['x-organization-id'] as string;
70
+
71
+ if (user && user.role !== 'SUPER_ADMIN') {
72
+ if (requestedOrgId && requestedOrgId !== user.organizationId) {
73
+ return reply.code(403).send({ error: 'Forbidden', message: 'Org mismatch' });
74
+ }
75
+ // Inject orgId from token if missing
76
+ if (!requestedOrgId) {
77
+ request.headers['x-organization-id'] = user.organizationId;
78
+ }
79
+ }
80
+ });
81
+
82
+ scope.addHook('preHandler', (request, _reply, done) => {
83
+ const orgId = request.headers['x-organization-id'] as string;
84
+ if (orgId) {
85
+ runWithTenant(orgId, done);
86
+ } else {
87
+ done();
88
+ }
89
+ });
90
+
91
+ // Protected Endpoints
92
+ scope.register(adminRoutes, { prefix: '/v1/admin' });
93
+ scope.register(organizationRoutes, { prefix: '/v1/organizations' });
94
+ scope.register(aiRoutes, { prefix: '/v1/ai' });
95
+ scope.register(paymentRoutes, { prefix: '/v1/payments' });
96
+ scope.register(analyticsRoutes, { prefix: '/v1/analytics' });
97
+ scope.register(internalRoutes);
98
  });
 
 
 
99
 
100
+ logger.info('[STARTUP] All routes registered.');
101
+ };
 
 
 
 
 
 
 
102
 
103
+ // ── Health & Debug (top-level) ────────────────────────────────────────────────
104
+ server.get('/', async () => ({ status: 'ok', service: 'Edtech API' }));
105
 
106
+ server.get('/health', async (_req, reply) => {
107
  try {
108
+ await prisma.$queryRaw`SELECT 1`;
109
+ return { status: 'ok', database: 'connected' };
110
+ } catch (err) {
111
+ reply.status(500).send({ status: 'error', database: 'disconnected' });
112
  }
113
  });
114
 
115
+ // ── Start Server ──────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  const start = async () => {
117
  try {
118
+ await registerRoutes();
119
+
120
  const port = parseInt(process.env.PORT || '8080');
121
  logger.info(`[STARTUP] Attempting to listen on port ${port}...`);
122
 
 
 
 
123
  await server.listen({ port, host: '0.0.0.0' });
124
  logger.info(`πŸš€ Server listening on http://0.0.0.0:${port}`);
 
125
 
126
+ // Background tasks
127
+ await setupRateLimit().catch(err => logger.error('[STARTUP] Rate limit error:', err));
128
  startCleanupCron();
129
 
 
130
  } catch (err: any) {
131
  logger.error('[STARTUP] ❌ FATAL ERROR DURING STARTUP:');
132
  logger.error(err.message || String(err));
133
+ console.error('CRITICAL ERROR:', err);
 
 
 
134
  process.exit(1);
135
  }
136
  };