131 lines
4.4 KiB
JavaScript
131 lines
4.4 KiB
JavaScript
import express from 'express'
|
|
import { hasVisitorConsent } from './server/helpers.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,
|
|
migrateStudyNotesIfNeeded,
|
|
loadDownloadCountsFromDisk,
|
|
loadPodcastChecklistFromDisk,
|
|
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 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 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)
|
|
|
|
// Register API routes
|
|
registerAdminAuth(app)
|
|
registerAdminContent(app)
|
|
registerAdminAssets(app)
|
|
registerStudyAuth(app)
|
|
registerStudyData(app)
|
|
registerStudyAccount(app)
|
|
registerContact(app)
|
|
registerQuestions(app)
|
|
registerAnalytics(app)
|
|
registerDownloads(app)
|
|
registerEpisodes(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) {
|
|
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(),
|
|
migrateStudyNotesIfNeeded(),
|
|
loadDownloadCountsFromDisk(),
|
|
loadPodcastChecklistFromDisk(),
|
|
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}`)
|
|
})
|
|
})
|