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:
@@ -1,4 +1,4 @@
|
||||
import { mkdir, mkdtemp, readdir, rm, cp, writeFile } from 'node:fs/promises'
|
||||
import { mkdir, mkdtemp, readdir, rm, cp, writeFile, unlink } from 'node:fs/promises'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import express from 'express'
|
||||
@@ -62,6 +62,8 @@ const ENV_SNAPSHOT_KEYS = [
|
||||
'TITUS_STUDY_DOWNLOAD_NAME',
|
||||
]
|
||||
|
||||
// Write env snapshot into DATA_DIR for inclusion in the archive, then delete
|
||||
// it immediately after the stream finishes so secrets don't linger on disk.
|
||||
async function writeEnvSnapshot() {
|
||||
const lines = [
|
||||
'# Siteforge environment snapshot — regenerated on every full backup export.',
|
||||
@@ -78,6 +80,10 @@ async function writeEnvSnapshot() {
|
||||
await writeFile(path.join(DATA_DIR, 'env-snapshot.env'), `${lines.join('\n')}\n`, 'utf8')
|
||||
}
|
||||
|
||||
async function deleteEnvSnapshot() {
|
||||
await unlink(path.join(DATA_DIR, 'env-snapshot.env')).catch(() => {})
|
||||
}
|
||||
|
||||
// Files that identify an archive as a Siteforge data backup. At least one
|
||||
// must be present at the top level of an uploaded archive before we restore.
|
||||
const KNOWN_DATA_FILES = [
|
||||
@@ -170,9 +176,12 @@ export function register(app) {
|
||||
console.error('[admin-backup] export stream failed:', err)
|
||||
res.destroy(err)
|
||||
})
|
||||
archive.on('end', () => { deleteEnvSnapshot() })
|
||||
res.on('close', () => { deleteEnvSnapshot() })
|
||||
archive.pipe(res)
|
||||
} catch (err) {
|
||||
console.error('[admin-backup] export failed:', err)
|
||||
deleteEnvSnapshot()
|
||||
if (!res.headersSent) res.status(500).json({ message: 'Full backup export failed.' })
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user