221 lines
13 KiB
JavaScript
221 lines
13 KiB
JavaScript
"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
|