Fix 11 bugs: restore crash, draft leak, email failures, memory leaks

Critical fixes:
- sanitizeLoadedHitStats/VisitorStats: restore full state shape so a
  snapshot restore no longer crashes hit-counting middleware (missing
  byPathReal, byPathBot, byDayReal, byDayBot, botReasons, ipHashIndex)
- /questions/share/🆔 read state.questions only, not draft questions
- inbound-email: validate date with Number.isFinite before toISOString
- study-reminders: wrap each send in try/catch so one failure doesn't
  block remaining users; persist sent-markers after each success

Security:
- getClientIp: use req.ip (trust-proxy-resolved) instead of raw
  x-forwarded-for header to prevent IP spoofing
- env-snapshot.env: delete immediately after backup tar stream ends
  so secrets don't linger on disk between exports

Correctness / UX:
- contact form: email failures no longer 500 the user after the
  submission is already saved; log and fall through instead
- study-account profile: cap data URI avatar at 6 MB
- admin enrollment PATCH: validate slug against study catalog
- signup: return 503 at MAX_STUDY_USERS instead of silently dropping
  oldest accounts

Memory leaks:
- contactHits, downloadHits Maps: prune stale entries at 5000 entries
- resendEmailSubmissionIndex: trim to 2000 entries (oldest first)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmert
2026-07-16 07:48:54 -04:00
parent f5e98826be
commit 1d43875e5a
11 changed files with 76 additions and 15 deletions
+8 -1
View File
@@ -650,9 +650,16 @@ export async function scheduleStudyReminders(sendStudyReminderEmail) {
const canonical = state.cachedSiteContent?.seo?.canonicalUrl || 'https://versebyversewithnate.us/'
const base = canonical.endsWith('/') ? canonical.slice(0, -1) : canonical
const sectionUrl = `${base}/study/${study.slug}/${section.id}`
await sendStudyReminderEmail(email, displayName, study.title, section.title, section.reference, sectionUrl)
try {
await sendStudyReminderEmail(email, displayName, study.title, section.title, section.reference, sectionUrl)
} catch (err) {
console.error(`[study-reminders] failed to send reminder to ${email} for ${studySlug}/${sectionId}:`, err)
continue
}
userSent[studySlug] = [...sentForStudy, sectionId]
state.studyReminders.users[user.id] = userSent
// Persist after each successful send so a later crash doesn't re-send
queueStudyRemindersWrite()
}
}
}