import express from 'express' import { hasVisitorConsent } from './server/helpers.js' import { isValidAdminSession } from './server/auth.js' import { BACKUP_INTERVAL_MS } from './server/config.js' import { state } from './server/state.js' import { loadHitStatsFromDisk, loadVisitorStatsFromDisk, loadContactSubmissionsFromDisk, loadReplyTemplatesFromDisk, loadReplyHistoryFromDisk, loadQuestionsFromDisk, loadDraftQuestionsFromDisk, loadStudyUsersFromDisk, loadStudyCommunityFromDisk, loadStudyRemindersFromDisk, loadStudyCommentsFromDisk, loadStudyCertificatesFromDisk, loadEpisodeScriptsFromDisk, migrateStudyNotesIfNeeded, loadDownloadCountsFromDisk, loadQrCodesFromDisk, loadEpisodePlaysFromDisk, loadPodcastChecklistFromDisk, loadAnalyticsEventsFromDisk, createBackupSnapshot, refreshContentCaches, queueHitStatsWrite, } from './server/data.js' import { logResendEmailAlignmentWarnings, sendStudyReminderEmail } from './server/email.js' import { detectBot, sanitizeUserAgent, shouldCountHit, recordHit, scheduleStudyReminders, } from './server/study-helpers.js' import { recordVisitor } from './server/routes/analytics.js' // Route registrars import { register as registerAdminAuth } from './server/routes/admin-auth.js' import { register as registerAdminContent } from './server/routes/admin-content.js' import { register as registerAdminAssets } from './server/routes/admin-assets.js' import { register as registerStudyAuth } from './server/routes/study-auth.js' import { register as registerStudyData } from './server/routes/study-data.js' import { register as registerStudyAccount } from './server/routes/study-account.js' import { register as registerStudyComments } from './server/routes/study-comments.js' import { register as registerStudyCertificate } from './server/routes/study-certificate.js' import { register as registerEpisodeScripts } from './server/routes/episode-scripts.js' import { register as registerContact } from './server/routes/contact.js' import { register as registerQuestions } from './server/routes/questions.js' import { register as registerAnalytics } from './server/routes/analytics.js' import { register as registerDownloads } from './server/routes/downloads.js' import { register as registerEpisodes } from './server/routes/episodes.js' import { register as registerQrCodes } from './server/routes/qr-codes.js' import { register as registerPublic } from './server/routes/public.js' const app = express() app.use(express.json({ limit: '10mb' })) const trustProxyHops = Number(process.env.TRUST_PROXY_HOPS ?? 1) app.set('trust proxy', Number.isFinite(trustProxyHops) && trustProxyHops >= 0 ? trustProxyHops : 1) app.use((_req, res, next) => { res.setHeader('X-Content-Type-Options', 'nosniff') res.setHeader('X-Frame-Options', 'SAMEORIGIN') res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin') res.setHeader( 'Content-Security-Policy', [ "default-src 'self'", "script-src 'self' 'unsafe-inline' https://www.truthforlife.org https://ajax.googleapis.com", "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", "font-src 'self' https://fonts.gstatic.com", "img-src 'self' data: https:", "media-src 'self' https:", "frame-src https:", "connect-src 'self' https:", ].join('; '), ) next() }) // Register API routes registerAdminAuth(app) registerAdminContent(app) registerAdminAssets(app) registerStudyAuth(app) registerStudyData(app) registerStudyAccount(app) registerStudyComments(app) registerStudyCertificate(app) registerEpisodeScripts(app) registerContact(app) registerQuestions(app) registerAnalytics(app) registerDownloads(app) registerEpisodes(app) registerQrCodes(app) // Hit-counting middleware (must come before public routes) app.use((req, res, next) => { if (shouldCountHit(req)) { const ua = sanitizeUserAgent(req.get('user-agent')) const botDetection = detectBot(ua) recordHit(req.path, botDetection.isBot, botDetection.reason) queueHitStatsWrite() if (hasVisitorConsent(req) && !botDetection.isBot && !isValidAdminSession(req)) { recordVisitor(req, res).catch(err => { console.error('[visitor-stats] failed to record visitor:', err) }) } } next() }) // Public routes (robots, sitemap, static, SPA fallback) registerPublic(app) const PORT = Number(process.env.PORT ?? 4173) Promise.all([ loadHitStatsFromDisk(), loadVisitorStatsFromDisk(), loadContactSubmissionsFromDisk(), loadReplyTemplatesFromDisk(), loadReplyHistoryFromDisk(), loadQuestionsFromDisk(), loadDraftQuestionsFromDisk(), loadStudyUsersFromDisk(), loadStudyCommunityFromDisk(), loadStudyRemindersFromDisk(), loadStudyCommentsFromDisk(), loadStudyCertificatesFromDisk(), loadEpisodeScriptsFromDisk(), migrateStudyNotesIfNeeded(), loadDownloadCountsFromDisk(), loadQrCodesFromDisk(), loadEpisodePlaysFromDisk(), loadPodcastChecklistFromDisk(), loadAnalyticsEventsFromDisk(), refreshContentCaches(), ]) .catch(err => { console.error('[stats] failed to load persisted stats:', err) }) .finally(() => { createBackupSnapshot('startup').catch(() => {}) setInterval(() => { createBackupSnapshot('scheduled').catch(() => {}) }, BACKUP_INTERVAL_MS) // Purge expired study sessions every hour setInterval(() => { const now = Date.now() for (const [token, session] of state.studySessions) { if (session.expiresAt <= now) state.studySessions.delete(token) } }, 60 * 60 * 1000) // Send study reminder emails for newly released lessons every hour setInterval(() => { scheduleStudyReminders(sendStudyReminderEmail).catch(err => { console.error('[study-reminders] failed to schedule reminders:', err) }) }, 60 * 60 * 1000) void scheduleStudyReminders(sendStudyReminderEmail) app.listen(PORT, () => { logResendEmailAlignmentWarnings() console.log(`Portfolio app listening on http://localhost:${PORT}`) }) })