"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.app = void 0; const express_1 = __importDefault(require("express")); const helmet_1 = __importDefault(require("helmet")); const cors_1 = __importDefault(require("cors")); const compression_1 = __importDefault(require("compression")); const env_1 = require("./config/env"); const logger_1 = require("./utils/logger"); const database_1 = require("./config/database"); const redis_1 = require("./config/redis"); const metrics_1 = require("./utils/metrics"); const error_handler_1 = require("./middleware/error-handler"); const rate_limit_1 = require("./middleware/rate-limit"); const auth_routes_1 = require("./modules/auth/auth.routes"); const users_routes_1 = require("./modules/users/users.routes"); const campaigns_routes_1 = require("./modules/influence/campaigns/campaigns.routes"); const campaign_emails_routes_1 = require("./modules/influence/campaign-emails/campaign-emails.routes"); const email_queue_routes_1 = require("./modules/influence/email-queue/email-queue.routes"); const representatives_routes_1 = require("./modules/influence/representatives/representatives.routes"); const campaigns_public_routes_1 = require("./modules/influence/campaigns/campaigns-public.routes"); const responses_routes_1 = require("./modules/influence/responses/responses.routes"); const locations_routes_1 = require("./modules/map/locations/locations.routes"); const bulk_geocode_routes_1 = require("./modules/map/locations/bulk-geocode.routes"); const cuts_routes_1 = require("./modules/map/cuts/cuts.routes"); const shifts_routes_1 = require("./modules/map/shifts/shifts.routes"); const shift_series_routes_1 = __importDefault(require("./modules/map/shifts/shift-series.routes")); const settings_routes_1 = require("./modules/map/settings/settings.routes"); const qr_routes_1 = require("./modules/qr/qr.routes"); const listmonk_routes_1 = require("./modules/listmonk/listmonk.routes"); const pages_public_routes_1 = require("./modules/pages/pages-public.routes"); const pages_admin_routes_1 = require("./modules/pages/pages-admin.routes"); const blocks_routes_1 = require("./modules/pages/blocks.routes"); const docs_routes_1 = require("./modules/docs/docs.routes"); const services_routes_1 = require("./modules/services/services.routes"); const settings_routes_2 = require("./modules/settings/settings.routes"); const canvass_routes_1 = require("./modules/map/canvass/canvass.routes"); const tracking_routes_1 = require("./modules/map/tracking/tracking.routes"); const geocoding_routes_1 = require("./modules/map/geocoding/geocoding.routes"); const pangolin_routes_1 = require("./modules/pangolin/pangolin.routes"); const nar_import_routes_1 = require("./modules/map/locations/nar-import.routes"); const email_templates_admin_routes_1 = __importDefault(require("./modules/email-templates/email-templates-admin.routes")); const observability_routes_1 = require("./modules/observability/observability.routes"); const crypto_1 = require("./utils/crypto"); const email_service_1 = require("./services/email.service"); const email_queue_service_1 = require("./services/email-queue.service"); const geocode_queue_service_1 = require("./services/geocode-queue.service"); const listmonk_proxy_service_1 = require("./services/listmonk-proxy.service"); const pages_service_1 = require("./modules/pages/pages.service"); const canvass_service_1 = require("./modules/map/canvass/canvass.service"); const tracking_service_1 = require("./modules/map/tracking/tracking.service"); const app = (0, express_1.default)(); exports.app = app; // Trust proxy for reverse proxy (nginx adds X-Forwarded-For) app.set('trust proxy', 1); // --- Middleware Stack --- app.use((0, helmet_1.default)({ contentSecurityPolicy: env_1.env.NODE_ENV === 'production' ? undefined : false, })); app.use((0, cors_1.default)({ origin: env_1.env.CORS_ORIGINS.split(',').map(s => s.trim()), credentials: true, })); app.use((0, compression_1.default)()); app.use(express_1.default.json({ limit: '10mb' })); app.use(express_1.default.urlencoded({ extended: true })); app.use(rate_limit_1.globalRateLimit); // Request metrics app.use((req, res, next) => { const end = metrics_1.httpRequestDuration.startTimer(); res.on('finish', () => { const route = req.route?.path || req.path; const labels = { method: req.method, route, status_code: res.statusCode.toString() }; end(labels); metrics_1.httpRequestsTotal.inc(labels); }); next(); }); // --- Health Check --- app.get('/api/health', rate_limit_1.healthMetricsRateLimit, async (_req, res) => { const checks = {}; try { await database_1.prisma.$queryRaw `SELECT 1`; checks.database = 'ok'; } catch { checks.database = 'error'; } try { await redis_1.redis.ping(); checks.redis = 'ok'; } catch { checks.redis = 'error'; } const healthy = Object.values(checks).every(v => v === 'ok'); res.status(healthy ? 200 : 503).json({ status: healthy ? 'healthy' : 'degraded', checks }); }); // --- Metrics Endpoint --- app.get('/api/metrics', rate_limit_1.healthMetricsRateLimit, async (_req, res) => { res.set('Content-Type', metrics_1.register.contentType); res.end(await metrics_1.register.metrics()); }); // --- API Routes --- app.use('/api/auth', auth_routes_1.authRouter); app.use('/api/users', users_routes_1.usersRouter); app.use('/api/campaigns', campaigns_public_routes_1.campaignPublicRouter); // Public campaign details (no auth) app.use('/api/campaigns', responses_routes_1.responseCampaignPublicRouter); // Public response routes (no auth) app.use('/api/campaigns', campaign_emails_routes_1.campaignEmailsPublicRouter); // Public email routes (no auth) app.use('/api/campaigns', campaign_emails_routes_1.campaignEmailsAdminRouter); // Admin email routes (auth required) app.use('/api/campaigns', campaigns_routes_1.campaignsRouter); // Admin campaign CRUD (auth required) app.use('/api/responses', responses_routes_1.responsesPublicRouter); // Public response actions (no auth) app.use('/api/responses', responses_routes_1.responsesAdminRouter); // Admin response moderation (auth required) app.use('/api/email-queue', email_queue_routes_1.emailQueueRouter); app.use('/api/representatives', representatives_routes_1.representativesRouter); app.use('/api/map/locations', locations_routes_1.locationsPublicRouter); // Public map data (no auth) app.use('/api/map/locations', locations_routes_1.locationsAdminRouter); // Admin location CRUD (auth required) app.use('/api/map/locations/bulk-geocode', bulk_geocode_routes_1.bulkGeocodeRouter); // Bulk re-geocoding (admin auth required) app.use('/api/map/nar-import', nar_import_routes_1.narImportRouter); // NAR server-side import (MAP_ADMIN+) app.use('/api/map/cuts', cuts_routes_1.cutsPublicRouter); // Public cut polygons (no auth) app.use('/api/map/cuts', cuts_routes_1.cutsAdminRouter); // Admin cut CRUD (auth required) app.use('/api/map/shifts', shifts_routes_1.shiftsPublicRouter); // Public shift listing + signup (no auth) app.use('/api/map/shifts', shifts_routes_1.shiftsVolunteerRouter); // Volunteer shift signup (auth required) app.use('/api/map/shifts/series', shift_series_routes_1.default); // Shift series management (MAP_ADMIN+) app.use('/api/map/shifts', shifts_routes_1.shiftsAdminRouter); // Admin shift CRUD (auth required) app.use('/api/map/geocoding', geocoding_routes_1.geocodingRouter); // Geocoding search (MAP_ADMIN+) app.use('/api/map/settings', settings_routes_1.mapSettingsRouter); // Map settings (public GET, auth PUT) app.use('/api/qr', qr_routes_1.qrRouter); // QR code generation (public) app.use('/api/listmonk', listmonk_routes_1.listmonkRouter); // Listmonk newsletter sync (SUPER_ADMIN) app.use('/api/email-templates', email_templates_admin_routes_1.default); // Email template management (ADMIN roles) app.use('/api/pages', pages_public_routes_1.pagesPublicRouter); // Public landing pages (no auth) app.use('/api/pages', pages_admin_routes_1.pagesAdminRouter); // Admin landing page CRUD (auth required) app.use('/api/page-blocks', blocks_routes_1.blocksRouter); // Admin page block library (auth required) app.use('/api/docs', docs_routes_1.docsRouter); // Docs status + config (auth required) app.use('/api/services', services_routes_1.servicesRouter); // Platform services status (SUPER_ADMIN) app.use('/api/map/canvass', canvass_routes_1.canvassVolunteerRouter); // Volunteer canvass routes (auth required) app.use('/api/map/canvass', canvass_routes_1.canvassAdminRouter); // Admin canvass routes (MAP_ADMIN+) app.use('/api/map/tracking', tracking_routes_1.trackingVolunteerRouter); // Volunteer GPS tracking (auth required) app.use('/api/map/tracking', tracking_routes_1.trackingAdminRouter); // Admin GPS tracking (MAP_ADMIN+) app.use('/api/settings', settings_routes_2.siteSettingsRouter); // Site settings (public GET, SUPER_ADMIN PUT) app.use('/api/pangolin', pangolin_routes_1.pangolinRouter); // Pangolin tunnel management (SUPER_ADMIN) app.use('/api/observability', observability_routes_1.observabilityRouter); // Observability / monitoring (SUPER_ADMIN) // --- Error Handler (must be last) --- app.use(error_handler_1.errorHandler); // --- Start Server --- async function start() { try { await database_1.prisma.$connect(); logger_1.logger.info('Database connected'); // Initialize encryption for DB-stored secrets (SMTP password, etc.) // In production, require separate encryption key (not JWT secret reuse) if (env_1.env.NODE_ENV === 'production' && !env_1.env.ENCRYPTION_KEY) { throw new Error('ENCRYPTION_KEY must be set in production (do not reuse JWT_ACCESS_SECRET)'); } (0, crypto_1.initEncryption)(env_1.env.ENCRYPTION_KEY || env_1.env.JWT_ACCESS_SECRET); // Rebuild SMTP transporter from DB settings (env fallback for empty fields) await email_service_1.emailService.rebuildTransporter(); email_queue_service_1.emailQueueService.startWorker(); geocode_queue_service_1.geocodeQueueService.startWorker(); (0, listmonk_proxy_service_1.startProxy)(); // Close abandoned canvass sessions on startup + hourly canvass_service_1.canvassService.closeAbandonedSessions().catch(() => { }); setInterval(() => { canvass_service_1.canvassService.closeAbandonedSessions().catch(() => { }); }, 60 * 60 * 1000); // Clean old tracking data on startup + daily tracking_service_1.trackingService.cleanupOldData(30).catch(() => { }); setInterval(() => tracking_service_1.trackingService.cleanupOldData(30).catch(() => { }), 24 * 60 * 60 * 1000); // Close stale tracking sessions (no data for 2h) — hourly tracking_service_1.trackingService.closeStaleTrackingSessions(120).catch(() => { }); setInterval(() => tracking_service_1.trackingService.closeStaleTrackingSessions(120).catch(() => { }), 60 * 60 * 1000); // Sync MkDocs overrides on startup pages_service_1.pagesService.syncOverrides() .then(({ imported, updated }) => { if (imported > 0 || updated > 0) { logger_1.logger.info(`Startup sync: imported ${imported}, updated ${updated} MkDocs overrides`); } }) .catch((err) => { logger_1.logger.warn('Startup sync of MkDocs overrides failed:', err); }); // Validate MkDocs exports on startup pages_service_1.pagesService.validateExports() .then(({ validated, repaired, errors }) => { if (repaired > 0 || errors.length > 0) { logger_1.logger.info(`Validation: ${validated} checked, ${repaired} repaired, ${errors.length} errors`); } }) .catch((err) => logger_1.logger.warn('Validation failed:', err)); // Schedule daily validation setInterval(() => { pages_service_1.pagesService.validateExports().catch((err) => { logger_1.logger.warn('Scheduled validation failed:', err); }); }, 24 * 60 * 60 * 1000); app.listen(env_1.env.PORT, () => { logger_1.logger.info(`API server running on port ${env_1.env.PORT} [${env_1.env.NODE_ENV}]`); }); } catch (err) { logger_1.logger.error('Failed to start server:', err); process.exit(1); } } start(); // Graceful shutdown for (const signal of ['SIGTERM', 'SIGINT']) { process.on(signal, async () => { logger_1.logger.info(`${signal} received, shutting down...`); await (0, listmonk_proxy_service_1.stopProxy)(); await email_queue_service_1.emailQueueService.close(); await geocode_queue_service_1.geocodeQueueService.close(); await database_1.prisma.$disconnect(); redis_1.redis.disconnect(); process.exit(0); }); } //# sourceMappingURL=server.js.map