488 lines
33 KiB
JavaScript
488 lines
33 KiB
JavaScript
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
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 auth_middleware_1 = require("./middleware/auth.middleware");
|
|
const rbac_middleware_1 = require("./middleware/rbac.middleware");
|
|
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 provisioning_routes_1 = require("./modules/users/provisioning.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 campaigns_user_routes_1 = require("./modules/influence/campaigns/campaigns-user.routes");
|
|
const campaigns_moderation_routes_1 = require("./modules/influence/campaigns/campaigns-moderation.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 listmonk_webhook_routes_1 = require("./modules/listmonk/listmonk-webhook.routes");
|
|
const meeting_planner_routes_1 = require("./modules/meeting-planner/meeting-planner.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 canvass_export_routes_1 = require("./modules/map/canvass/canvass-export.routes");
|
|
const tracking_routes_1 = require("./modules/map/tracking/tracking.routes");
|
|
const geocoding_routes_1 = require("./modules/map/geocoding/geocoding.routes");
|
|
const events_routes_1 = require("./modules/map/events/events.routes");
|
|
const pangolin_routes_1 = require("./modules/pangolin/pangolin.routes");
|
|
const rocketchat_routes_1 = require("./modules/rocketchat/rocketchat.routes");
|
|
const jitsi_routes_1 = require("./modules/jitsi/jitsi.routes");
|
|
const rocketchat_webhook_service_1 = require("./services/rocketchat-webhook.service");
|
|
const gancio_client_1 = require("./services/gancio.client");
|
|
const gancio_settings_sync_service_1 = require("./services/gancio-settings-sync.service");
|
|
const nar_import_routes_1 = require("./modules/map/locations/nar-import.routes");
|
|
const area_import_routes_1 = require("./modules/map/locations/area-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 upgrade_routes_1 = require("./modules/upgrade/upgrade.routes");
|
|
const registry_routes_1 = require("./modules/registry/registry.routes");
|
|
const dashboard_routes_1 = require("./modules/dashboard/dashboard.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 notification_queue_service_1 = require("./services/notification-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 verification_token_service_1 = require("./services/verification-token.service");
|
|
const password_reset_token_service_1 = require("./services/password-reset-token.service");
|
|
const payments_public_routes_1 = require("./modules/payments/payments-public.routes");
|
|
const payments_admin_routes_1 = require("./modules/payments/payments-admin.routes");
|
|
const donation_pages_public_routes_1 = require("./modules/payments/donation-pages-public.routes");
|
|
const donation_pages_admin_routes_1 = require("./modules/payments/donation-pages-admin.routes");
|
|
const webhook_service_1 = require("./modules/payments/webhook.service");
|
|
const gallery_ads_public_routes_1 = require("./modules/gallery-ads/gallery-ads-public.routes");
|
|
const gallery_ads_admin_routes_1 = require("./modules/gallery-ads/gallery-ads-admin.routes");
|
|
const effectiveness_routes_1 = require("./modules/influence/effectiveness/effectiveness.routes");
|
|
const docs_analytics_routes_1 = require("./modules/docs-analytics/docs-analytics.routes");
|
|
const docs_comments_routes_1 = require("./modules/docs-comments/docs-comments.routes");
|
|
const volunteer_invite_routes_1 = require("./modules/volunteer-invite/volunteer-invite.routes");
|
|
const docs_analytics_service_1 = require("./modules/docs-analytics/docs-analytics.service");
|
|
const sms_contacts_routes_1 = require("./modules/sms/contacts/sms-contacts.routes");
|
|
const sms_campaigns_routes_1 = require("./modules/sms/campaigns/sms-campaigns.routes");
|
|
const sms_conversations_routes_1 = require("./modules/sms/conversations/sms-conversations.routes");
|
|
const sms_messages_routes_1 = require("./modules/sms/messages/sms-messages.routes");
|
|
const sms_device_routes_1 = require("./modules/sms/device/sms-device.routes");
|
|
const sms_setup_routes_1 = require("./modules/sms/setup/sms-setup.routes");
|
|
const sms_templates_routes_1 = require("./modules/sms/templates/sms-templates.routes");
|
|
const sms_queue_service_1 = require("./services/sms-queue.service");
|
|
const sms_response_sync_service_1 = require("./services/sms-response-sync.service");
|
|
const sms_device_monitor_service_1 = require("./services/sms-device-monitor.service");
|
|
const reengagement_service_1 = require("./services/reengagement.service");
|
|
const social_digest_service_1 = require("./services/social-digest.service");
|
|
const termux_client_1 = require("./services/termux.client");
|
|
const user_provisioning_1 = require("./services/user-provisioning");
|
|
const people_routes_1 = require("./modules/people/people.routes");
|
|
const participant_needs_routes_1 = require("./modules/people/participant-needs.routes");
|
|
const profile_public_routes_1 = require("./modules/people/profile-public.routes");
|
|
const search_routes_1 = require("./modules/search/search.routes");
|
|
const activity_public_routes_1 = require("./modules/activity/activity-public.routes");
|
|
const newsletter_public_routes_1 = require("./modules/newsletter/newsletter-public.routes");
|
|
const events_public_routes_1 = require("./modules/events/events-public.routes");
|
|
const homepage_routes_1 = require("./modules/homepage/homepage.routes");
|
|
const og_routes_1 = require("./modules/og/og.routes");
|
|
const social_routes_1 = require("./modules/social/social.routes");
|
|
const error_report_routes_1 = require("./modules/reports/error-report.routes");
|
|
const calendar_routes_1 = __importDefault(require("./modules/calendar/calendar.routes"));
|
|
const feed_routes_1 = __importDefault(require("./modules/calendar/feed.routes"));
|
|
const shared_calendar_routes_1 = __importDefault(require("./modules/calendar/shared-calendar.routes"));
|
|
const admin_calendar_routes_1 = require("./modules/calendar/admin-calendar.routes");
|
|
const ticketed_events_public_routes_1 = require("./modules/ticketed-events/ticketed-events-public.routes");
|
|
const ticketed_events_admin_routes_1 = require("./modules/ticketed-events/ticketed-events-admin.routes");
|
|
const checkin_routes_1 = require("./modules/ticketed-events/checkin.routes");
|
|
const sse_service_1 = require("./modules/social/sse.service");
|
|
const presence_service_1 = require("./modules/social/presence.service");
|
|
const upgrade_service_1 = require("./modules/upgrade/upgrade.service");
|
|
const auto_upgrade_service_1 = require("./services/auto-upgrade.service");
|
|
const calendar_feed_queue_service_1 = require("./services/calendar-feed-queue.service");
|
|
const scheduled_jobs_queue_service_1 = require("./services/scheduled-jobs-queue.service");
|
|
const poll_auto_finalize_queue_service_1 = require("./services/poll-auto-finalize-queue.service");
|
|
const agenda_routes_1 = require("./modules/meetings/agenda.routes");
|
|
const action_items_routes_1 = require("./modules/meetings/action-items.routes");
|
|
const ws_1 = require("ws");
|
|
const docs_collab_service_1 = require("./modules/docs/docs-collab.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)());
|
|
// Stripe webhook — must receive raw body BEFORE express.json() parses it
|
|
app.post('/api/payments/webhook', express_1.default.raw({ type: 'application/json' }), async (req, res) => {
|
|
const signature = req.headers['stripe-signature'];
|
|
if (!signature || typeof signature !== 'string') {
|
|
res.status(400).json({ error: 'Missing stripe-signature header' });
|
|
return;
|
|
}
|
|
try {
|
|
const event = await webhook_service_1.webhookService.constructEvent(req.body, signature);
|
|
await webhook_service_1.webhookService.handleEvent(event);
|
|
res.json({ received: true });
|
|
}
|
|
catch (err) {
|
|
const message = err instanceof Error ? err.message : 'Webhook error';
|
|
res.status(400).json({ error: message });
|
|
}
|
|
});
|
|
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 (authenticated - SUPER_ADMIN only) ---
|
|
app.get('/api/metrics', auth_middleware_1.authenticate, (0, rbac_middleware_1.requireRole)('SUPER_ADMIN'), 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/users', provisioning_routes_1.provisioningRouter); // User provisioning management (ADMIN roles)
|
|
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', campaigns_user_routes_1.campaignUserRouter); // User campaign submission (auth, non-temp)
|
|
app.use('/api/campaigns', campaigns_moderation_routes_1.campaignModerationRouter); // Moderation queue (admin 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/area-import', area_import_routes_1.areaImportRouter); // Area import from multiple sources (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/map/events', events_routes_1.eventsPublicRouter); // Public map events from Gancio (no auth)
|
|
app.use('/api/meeting-planner', meeting_planner_routes_1.meetingPlannerPublicRouter); // Public poll viewing + voting (no auth)
|
|
app.use('/api/meeting-planner', meeting_planner_routes_1.meetingPlannerAdminRouter); // Admin poll CRUD (auth required)
|
|
app.use('/api/meetings/agendas', agenda_routes_1.agendaRouter); // Meeting agendas + minutes (EVENTS roles)
|
|
app.use('/api/meetings/action-items', action_items_routes_1.actionItemsRouter); // Action items CRUD (EVENTS roles / auth)
|
|
app.use('/api/qr', qr_routes_1.qrRouter); // QR code generation (public)
|
|
app.use('/api/listmonk', listmonk_webhook_routes_1.listmonkWebhookRouter); // Listmonk webhook (shared secret, no JWT)
|
|
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/canvass', canvass_export_routes_1.canvassExportRouter); // Canvass-to-campaign export (admin roles)
|
|
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/rocketchat', rocketchat_routes_1.rocketchatRouter); // Rocket.Chat SSO + status (auth required)
|
|
app.use('/api/jitsi', jitsi_routes_1.jitsiRouter); // Jitsi Meet JWT + status (auth required)
|
|
app.use('/api/observability', observability_routes_1.observabilityRouter); // Observability / monitoring (SUPER_ADMIN)
|
|
app.use('/api/upgrade', upgrade_routes_1.upgradeRouter); // System upgrade management (SUPER_ADMIN)
|
|
app.use('/api/registry', registry_routes_1.registryRouter); // Container registry management (SUPER_ADMIN)
|
|
app.use('/api/dashboard', dashboard_routes_1.dashboardRouter); // Dashboard summary (ADMIN roles)
|
|
app.use('/api/donation-pages', donation_pages_public_routes_1.donationPagesPublicRouter); // Public donation pages (no auth)
|
|
app.use('/api/payments', payments_public_routes_1.paymentsPublicRouter); // Public payment routes (plans, checkout, my subscription)
|
|
app.use('/api/payments/admin/donation-pages', donation_pages_admin_routes_1.donationPagesAdminRouter); // Admin donation page CRUD (SUPER_ADMIN)
|
|
app.use('/api/payments/admin', payments_admin_routes_1.paymentsAdminRouter); // Admin payment management (SUPER_ADMIN)
|
|
app.use('/api/influence/effectiveness', effectiveness_routes_1.effectivenessRouter); // Campaign effectiveness analytics (ADMIN)
|
|
app.use('/api/gallery-ads', gallery_ads_public_routes_1.galleryAdsPublicRouter); // Public gallery ads (optional auth)
|
|
app.use('/api/gallery-ads/admin', gallery_ads_admin_routes_1.galleryAdsAdminRouter); // Admin gallery ad CRUD (SUPER_ADMIN)
|
|
app.use('/api/docs-analytics', docs_analytics_routes_1.docsAnalyticsPublicRouter); // Public docs page view tracking (no auth)
|
|
app.use('/api/docs-analytics', docs_analytics_routes_1.docsAnalyticsAdminRouter); // Admin docs analytics (ADMIN roles)
|
|
app.use('/api/docs-comments', docs_comments_routes_1.docsCommentsPublicRouter); // Public docs comments (CORS override for docs origin)
|
|
app.use('/api/docs-comments', docs_comments_routes_1.docsCommentsAdminRouter); // Admin docs comment moderation (ADMIN roles)
|
|
app.use('/api/volunteer-invite', volunteer_invite_routes_1.volunteerInviteRouter); // Quick join invite (admin generate + public redeem)
|
|
app.use('/api/sms/contacts', sms_contacts_routes_1.smsContactsRouter); // SMS contact list CRUD + import (ADMIN roles)
|
|
app.use('/api/sms/campaigns', sms_campaigns_routes_1.smsCampaignsRouter); // SMS campaign CRUD + execution (ADMIN roles)
|
|
app.use('/api/sms/conversations', sms_conversations_routes_1.smsConversationsRouter); // SMS conversation threads (ADMIN roles)
|
|
app.use('/api/sms/messages', sms_messages_routes_1.smsMessagesRouter); // SMS message history + ad-hoc send (ADMIN roles)
|
|
app.use('/api/sms/device', sms_device_routes_1.smsDeviceRouter); // SMS device status + sync trigger (ADMIN roles)
|
|
app.use('/api/sms/templates', sms_templates_routes_1.smsTemplatesRouter); // SMS template CRUD (ADMIN roles)
|
|
app.use('/api/sms/setup', sms_setup_routes_1.smsSetupRouter); // SMS setup wizard (SUPER_ADMIN only)
|
|
app.use('/api/profile', profile_public_routes_1.profilePublicRouter); // Self-service contact profile (no auth, token-based)
|
|
app.use('/api/people/needs', participant_needs_routes_1.participantNeedsRouter); // Participant needs (self-service + EVENTS roles)
|
|
app.use('/api/people', people_routes_1.peopleRouter); // People CRM aggregation (ADMIN roles)
|
|
app.use('/api/search', search_routes_1.searchRouter); // Public unified search (no auth, rate-limited)
|
|
app.use('/api/activity', activity_public_routes_1.activityPublicRouter); // Public activity feed (no auth)
|
|
app.use('/api/newsletter', newsletter_public_routes_1.newsletterPublicRouter); // Public newsletter subscribe (no auth, rate-limited)
|
|
app.use('/api/events', events_public_routes_1.eventsListPublicRouter); // Public events list from Gancio (no auth, cached)
|
|
app.use('/api/homepage', homepage_routes_1.homepageRouter); // Public homepage aggregation (no auth, cached)
|
|
app.use('/api/og', og_routes_1.ogRouter); // OG meta tags for social sharing bots (no auth, cached)
|
|
app.use('/api/social', social_routes_1.socialRouter); // Social connections (auth required)
|
|
app.use('/api/ticketed-events/admin', ticketed_events_admin_routes_1.ticketedEventsAdminRouter); // Admin ticketed event CRUD (auth + permission) — MUST be before public /:slug
|
|
app.use('/api/ticketed-events/checkin', checkin_routes_1.checkinRouter); // Check-in scanner routes (auth required)
|
|
app.use('/api/ticketed-events', ticketed_events_public_routes_1.ticketedEventsPublicRouter); // Public ticketed event listing + checkout (no auth)
|
|
app.use('/api/public/error-report', error_report_routes_1.errorReportRouter); // Public 404 error reporting (rate-limited)
|
|
app.use('/api/admin/calendar/shared', admin_calendar_routes_1.adminCalendarRouter); // Admin calendar views (SUPER_ADMIN/MAP_ADMIN)
|
|
app.use('/api/calendar/shared', shared_calendar_routes_1.default); // Shared calendar views + collaboration (public share + auth)
|
|
app.use('/api/calendar', feed_routes_1.default); // ICS feed subscriptions + export (public .ics + auth)
|
|
app.use('/api/calendar', calendar_routes_1.default); // Personal calendar layers + items (auth required)
|
|
// --- API 404 Handler (catch unmatched /api/* routes) ---
|
|
app.use('/api/*', (_req, res) => {
|
|
res.status(404).json({ error: { message: 'Route not found', code: 'NOT_FOUND' } });
|
|
});
|
|
// --- 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)');
|
|
}
|
|
if (!env_1.env.ENCRYPTION_KEY) {
|
|
logger_1.logger.warn('ENCRYPTION_KEY not set — falling back to JWT_ACCESS_SECRET for encryption. Set ENCRYPTION_KEY in production.');
|
|
}
|
|
(0, crypto_1.initEncryption)(env_1.env.ENCRYPTION_KEY || env_1.env.JWT_ACCESS_SECRET);
|
|
// Warn if Listmonk sync is enabled but webhook secret is not configured
|
|
if (env_1.env.LISTMONK_SYNC_ENABLED === 'true' && !env_1.env.LISTMONK_WEBHOOK_SECRET) {
|
|
logger_1.logger.warn('LISTMONK_SYNC_ENABLED is true but LISTMONK_WEBHOOK_SECRET is not set. Unsubscribe events from Listmonk will not be processed.');
|
|
}
|
|
// Register user provisioning framework
|
|
(0, user_provisioning_1.registerProvisioners)();
|
|
// Rebuild SMTP transporter from DB settings (env fallback for empty fields)
|
|
await email_service_1.emailService.rebuildTransporter();
|
|
email_queue_service_1.emailQueueService.startWorker();
|
|
notification_queue_service_1.notificationQueueService.startWorker();
|
|
geocode_queue_service_1.geocodeQueueService.startWorker();
|
|
calendar_feed_queue_service_1.calendarFeedQueueService.startWorker();
|
|
scheduled_jobs_queue_service_1.scheduledJobsQueueService.startWorker();
|
|
poll_auto_finalize_queue_service_1.pollAutoFinalizeQueueService.startWorker();
|
|
(0, listmonk_proxy_service_1.startProxy)();
|
|
// Load SMS config from DB (env fallback for empty fields)
|
|
await termux_client_1.termuxClient.configureFromDb();
|
|
// Load Tailscale config from DB (for SMS setup device discovery)
|
|
const { tailscaleClient } = await Promise.resolve().then(() => __importStar(require('./services/tailscale.client')));
|
|
await tailscaleClient.configureFromDb();
|
|
// Start SMS services if enabled
|
|
if (termux_client_1.termuxClient.enabled) {
|
|
sms_queue_service_1.smsQueueService.startWorker();
|
|
sms_response_sync_service_1.smsResponseSyncService.start();
|
|
sms_device_monitor_service_1.smsDeviceMonitorService.start();
|
|
logger_1.logger.info('SMS integration enabled (Termux API)');
|
|
}
|
|
// One-time startup calls (recurring runs handled by scheduled-jobs queue)
|
|
verification_token_service_1.verificationTokenService.cleanupExpiredTokens().catch(() => { });
|
|
password_reset_token_service_1.passwordResetTokenService.cleanupExpiredTokens().catch(() => { });
|
|
canvass_service_1.canvassService.closeAbandonedSessions().catch(() => { });
|
|
tracking_service_1.trackingService.cleanupOldData(30).catch(() => { });
|
|
tracking_service_1.trackingService.closeStaleTrackingSessions(120).catch(() => { });
|
|
docs_analytics_service_1.docsAnalyticsService.cleanupOldData(90).catch(() => { });
|
|
reengagement_service_1.reengagementService.scan().catch(() => { });
|
|
social_digest_service_1.socialDigestService.scan().catch(() => { });
|
|
// SSE + Presence: mark all users offline on startup, start heartbeat + stale cleanup
|
|
presence_service_1.presenceService.markAllOffline().catch(() => { });
|
|
sse_service_1.sseService.startHeartbeat();
|
|
setInterval(() => presence_service_1.presenceService.cleanupStale().catch(() => { }), 60 * 1000); // every 1 min
|
|
// Challenge lifecycle: activate/complete/score every 5 minutes
|
|
Promise.resolve().then(() => __importStar(require('./services/challenge-scoring.service'))).then(({ challengeScoringService }) => {
|
|
challengeScoringService.processLifecycle().catch(() => { });
|
|
setInterval(() => challengeScoringService.processLifecycle().catch(() => { }), 5 * 60 * 1000);
|
|
logger_1.logger.info('Challenge lifecycle processor started (every 5min)');
|
|
}).catch(() => { });
|
|
// Clean up stale upgrade progress on startup
|
|
upgrade_service_1.upgradeService.clearStaleProgress();
|
|
// Archive any pending upgrade result to history + send notifications
|
|
auto_upgrade_service_1.autoUpgradeService.handlePostRestartResult().catch(() => { });
|
|
// Start auto-upgrade scheduler if enabled
|
|
auto_upgrade_service_1.autoUpgradeService.start().catch(() => { });
|
|
// Setup Rocket.Chat notification channels (non-blocking)
|
|
rocketchat_webhook_service_1.rocketchatWebhookService.setupChannels().catch(() => { });
|
|
// Sync CML site settings → Gancio (with retry for slow Gancio startup)
|
|
if (gancio_client_1.gancioClient.enabled) {
|
|
const maxAttempts = 3;
|
|
const retryDelay = 20000; // 20s — covers Gancio's 60s start_period
|
|
const attemptSync = async (attempt) => {
|
|
try {
|
|
const available = await gancio_client_1.gancioClient.isAvailable();
|
|
if (!available)
|
|
throw new Error('Gancio not available');
|
|
await gancio_settings_sync_service_1.gancioSettingsSyncService.syncAll();
|
|
}
|
|
catch {
|
|
if (attempt < maxAttempts) {
|
|
logger_1.logger.debug(`Gancio settings sync attempt ${attempt}/${maxAttempts} failed, retrying in ${retryDelay / 1000}s`);
|
|
setTimeout(() => attemptSync(attempt + 1).catch(() => { }), retryDelay);
|
|
}
|
|
else {
|
|
logger_1.logger.warn(`Gancio settings sync failed after ${maxAttempts} attempts — will sync on next settings update`);
|
|
}
|
|
}
|
|
};
|
|
attemptSync(1).catch(() => { });
|
|
}
|
|
// 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 (recurring runs handled by scheduled-jobs queue)
|
|
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));
|
|
const server = app.listen(env_1.env.PORT, () => {
|
|
logger_1.logger.info(`API server running on port ${env_1.env.PORT} [${env_1.env.NODE_ENV}]`);
|
|
});
|
|
// --- WebSocket upgrade handler for docs collaboration ---
|
|
const wss = new ws_1.WebSocketServer({ noServer: true });
|
|
server.on('upgrade', (request, socket, head) => {
|
|
const url = new URL(request.url || '', `http://${request.headers.host}`);
|
|
// HocuspocusProvider connects to the base URL; document name is sent via protocol
|
|
const collabPath = '/api/docs/collaborate';
|
|
if (url.pathname !== collabPath && !url.pathname.startsWith(collabPath + '/')) {
|
|
socket.destroy();
|
|
return;
|
|
}
|
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
// Document name may be in the URL path (y-websocket) or sent via protocol (HocuspocusProvider)
|
|
const pathSuffix = url.pathname.slice(collabPath.length + 1); // strip /api/docs/collaborate/
|
|
const documentName = pathSuffix ? decodeURIComponent(pathSuffix) : '';
|
|
const token = url.searchParams.get('token') || '';
|
|
docs_collab_service_1.docsCollabService.handleConnection(ws, request, { documentName, token });
|
|
});
|
|
});
|
|
// Clean stale collab states on startup (recurring runs handled by scheduled-jobs queue)
|
|
docs_collab_service_1.docsCollabService.cleanupStaleStates().catch(() => { });
|
|
}
|
|
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...`);
|
|
sse_service_1.sseService.closeAll();
|
|
await docs_collab_service_1.docsCollabService.shutdown();
|
|
await (0, listmonk_proxy_service_1.stopProxy)();
|
|
await email_queue_service_1.emailQueueService.close();
|
|
await notification_queue_service_1.notificationQueueService.close();
|
|
await geocode_queue_service_1.geocodeQueueService.close();
|
|
await sms_queue_service_1.smsQueueService.close();
|
|
await calendar_feed_queue_service_1.calendarFeedQueueService.close();
|
|
await scheduled_jobs_queue_service_1.scheduledJobsQueueService.close();
|
|
await database_1.prisma.$disconnect();
|
|
redis_1.redis.disconnect();
|
|
process.exit(0);
|
|
});
|
|
}
|
|
//# sourceMappingURL=server.js.map
|