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:
+20
-1
@@ -1058,21 +1058,40 @@ function sanitizeLoadedContactSubmissions(value) {
|
||||
function sanitizeLoadedHitStats(value) {
|
||||
return {
|
||||
totalHits: Number(value?.totalHits) || 0,
|
||||
realHits: Number(value?.realHits) || 0,
|
||||
botHits: Number(value?.botHits) || 0,
|
||||
firstHitAt: typeof value?.firstHitAt === 'string' ? value.firstHitAt : null,
|
||||
lastHitAt: typeof value?.lastHitAt === 'string' ? value.lastHitAt : null,
|
||||
byPath: value?.byPath && typeof value.byPath === 'object' ? value.byPath : {},
|
||||
byPathReal: value?.byPathReal && typeof value.byPathReal === 'object' ? value.byPathReal : {},
|
||||
byPathBot: value?.byPathBot && typeof value.byPathBot === 'object' ? value.byPathBot : {},
|
||||
byDay: value?.byDay && typeof value.byDay === 'object' ? value.byDay : {},
|
||||
byDayReal: value?.byDayReal && typeof value.byDayReal === 'object' ? value.byDayReal : {},
|
||||
byDayBot: value?.byDayBot && typeof value.byDayBot === 'object' ? value.byDayBot : {},
|
||||
botReasons: value?.botReasons && typeof value.botReasons === 'object' ? value.botReasons : {},
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeLoadedVisitorStats(value) {
|
||||
const loadedVisitors = value?.visitors && typeof value.visitors === 'object' ? value.visitors : {}
|
||||
|
||||
let ipHashIndex = value?.ipHashIndex && typeof value.ipHashIndex === 'object' ? value.ipHashIndex : {}
|
||||
if (Object.keys(ipHashIndex).length === 0 && Object.keys(loadedVisitors).length > 0) {
|
||||
for (const [vid, visitor] of Object.entries(loadedVisitors)) {
|
||||
if (visitor?.ipHash && typeof visitor.ipHash === 'string') {
|
||||
ipHashIndex[visitor.ipHash] = vid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalVisits: Number(value?.totalVisits) || 0,
|
||||
uniqueVisitors: Number(value?.uniqueVisitors) || 0,
|
||||
returningVisits: Number(value?.returningVisits) || 0,
|
||||
firstVisitAt: typeof value?.firstVisitAt === 'string' ? value.firstVisitAt : null,
|
||||
lastVisitAt: typeof value?.lastVisitAt === 'string' ? value.lastVisitAt : null,
|
||||
visitors: value?.visitors && typeof value.visitors === 'object' ? value.visitors : {},
|
||||
visitors: loadedVisitors,
|
||||
ipHashIndex,
|
||||
recentVisits: Array.isArray(value?.recentVisits) ? value.recentVisits.slice(0, MAX_RECENT_VISITS) : [],
|
||||
geoCacheByIp: value?.geoCacheByIp && typeof value.geoCacheByIp === 'object' ? value.geoCacheByIp : {},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user