diff --git a/server/routes/admin-backup.js b/server/routes/admin-backup.js index d23c8b2..cf72d15 100644 --- a/server/routes/admin-backup.js +++ b/server/routes/admin-backup.js @@ -30,6 +30,54 @@ import { refreshContentCaches, } from '../data.js' +// Portable server settings captured into env-snapshot.env at export time so a +// backup carries the runtime configuration too (Docker deployments have no +// .env file — settings live in container env vars). Machine-specific vars +// (PORT, NODE_ENV, SITEFORGE_DATA_DIR, ALLOW_INSECURE_COOKIES, build info) +// are deliberately excluded. Values are NOT applied automatically on restore. +const ENV_SNAPSHOT_KEYS = [ + 'ADMIN_PASSWORD', + 'RESEND_API_KEY', + 'RESEND_CONTACTS_API_KEY', + 'RESEND_WEBHOOK_TOKEN', + 'INBOUND_EMAIL_SECRET', + 'RESEND_FROM', + 'RESEND_TO', + 'RESEND_REPLY_TO', + 'RESEND_AUTOMATION_WELCOME', + 'RESEND_SEGMENT_ID', + 'RESEND_REMINDER_SUBJECT', + 'RESEND_WELCOME_SUBJECT', + 'RESEND_WELCOME_IMAGE_URL', + 'RESEND_WELCOME_EPISODE_URL', + 'RESEND_WELCOME_WEBSITE_URL', + 'RESEND_WELCOME_SPOTIFY_URL', + 'RESEND_WELCOME_APPLE_URL', + 'RESEND_WELCOME_AMAZON_URL', + 'CACHE_PURGE_WEBHOOK_URL', + 'DEPLOY_WEBHOOK_URL', + 'CONTACT_EMAIL_COOLDOWN_MS', + 'TRUST_PROXY_HOPS', + 'TITUS_STUDY_FILE', + 'TITUS_STUDY_DOWNLOAD_NAME', +] + +async function writeEnvSnapshot() { + const lines = [ + '# Siteforge environment snapshot — regenerated on every full backup export.', + '# Contains secrets (admin password, API keys): keep this backup private.', + '# These values are NOT applied automatically on restore. Set them as', + '# container environment variables (or in .env) on the new server.', + `# Exported at ${new Date().toISOString()}`, + '', + ] + for (const key of ENV_SNAPSHOT_KEYS) { + const value = process.env[key] + if (typeof value === 'string' && value.trim() !== '') lines.push(`${key}=${value}`) + } + await writeFile(path.join(DATA_DIR, 'env-snapshot.env'), `${lines.join('\n')}\n`, 'utf8') +} + // 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 = [ @@ -106,6 +154,7 @@ export function register(app) { app.get('/api/admin-backup/export', requireAdminAuth, async (_req, res) => { try { await flushPendingWrites() + await writeEnvSnapshot() const entries = (await readdir(DATA_DIR)).filter(name => name !== 'backups') if (entries.length === 0) { res.status(500).json({ message: 'Data directory is empty — nothing to export.' }) diff --git a/src/components/AnalyticsPanel.tsx b/src/components/AnalyticsPanel.tsx index 91c7c69..248c59b 100644 --- a/src/components/AnalyticsPanel.tsx +++ b/src/components/AnalyticsPanel.tsx @@ -818,7 +818,7 @@ export function AnalyticsPanel({ {/* Full Data Backup (entire /data folder — server migration) */}

Full Data Backup

-

Download every persisted file — site content, uploads, study accounts, notes, progress, questions, analytics — as one archive, or restore that archive on this or a new server.

+

Download every persisted file — site content, uploads, study accounts, notes, progress, questions, analytics — plus a snapshot of server settings (env-snapshot.env) as one archive, or restore that archive on this or a new server. The archive contains secrets like the admin password and API keys, so keep it private. Restored settings are not applied automatically — copy them into the new server's environment.