/** * migrate-env-to-db.js * * Reads settings from environment variables (.env) and writes them into the * AppSetting table — only for keys that are NOT already set in the database. * * Safe to run on a live site: it never overwrites an existing DB value. * Sensitive keys (smtp_user, smtp_pass) are AES-256-GCM encrypted before storage. * * Usage: * cd backend * node scripts/migrate-env-to-db.js * * Add --dry-run to preview what would be written without touching the DB. * Add --force to overwrite existing DB values (use with caution on live data). */ 'use strict'; const path = require('path'); const fs = require('fs'); const dotenv = require('dotenv'); // Load .env from the backend root dotenv.config({ path: path.resolve(__dirname, '../.env') }); const { PrismaClient } = require('@prisma/client'); const crypto = require('crypto'); const DRY_RUN = process.argv.includes('--dry-run'); const FORCE = process.argv.includes('--force'); // ─── Encryption (mirrors backend/src/utils/encryption.js) ──────────────────── const SENTINEL = 'enc:'; const ENCRYPTION_KEY = (() => { const secret = process.env.JWT_SECRET || 'default-unsafe-key-CHANGE-IN-PRODUCTION'; if (!process.env.JWT_SECRET) { console.warn('[warn] JWT_SECRET is not set — encrypted values will use an insecure fallback key.'); } return crypto.scryptSync(secret, 'hope-events-settings-aes256gcm-v1', 32); })(); function encrypt(plaintext) { if (!plaintext) return plaintext; if (plaintext.startsWith(SENTINEL)) return plaintext; const iv = crypto.randomBytes(12); const cipher = crypto.createCipheriv('aes-256-gcm', ENCRYPTION_KEY, iv); const enc = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]); const tag = cipher.getAuthTag(); return `${SENTINEL}${iv.toString('hex')}:${tag.toString('hex')}:${enc.toString('hex')}`; } const ENCRYPTED_KEYS = new Set(['smtp_user', 'smtp_pass']); // ─── Mapping: env var(s) → DB key ──────────────────────────────────────────── // Each entry: { key: DB key, envVars: [...candidates in priority order], encrypt?: bool } // The first non-empty env var found wins. const MAPPINGS = [ // Organisation { key: 'org_name', envVars: ['ORG_NAME'] }, { key: 'org_tagline', envVars: ['ORG_TAGLINE'] }, { key: 'org_email', envVars: ['EMAIL_FROM', 'EMAIL_USER', 'SMTP_FROM', 'SMTP_USER'] }, // Branding { key: 'accent_color', envVars: ['EMAIL_HEADER_COLOR', 'BRAND_COLOR'] }, // Notifications { key: 'reg_notification_emails',envVars: ['REGISTRATIONS_EMAIL'] }, // SMTP { key: 'smtp_host', envVars: ['SMTP_HOST', 'EMAIL_HOST'] }, { key: 'smtp_port', envVars: ['SMTP_PORT', 'EMAIL_PORT'] }, { key: 'smtp_secure', envVars: ['SMTP_SECURE', 'EMAIL_SECURE'] }, { key: 'smtp_from', envVars: ['MAIL_FROM', 'EMAIL_FROM'] }, { key: 'smtp_user', envVars: ['SMTP_USER', 'EMAIL_USER'], encrypt: true }, { key: 'smtp_pass', envVars: ['SMTP_PASS', 'EMAIL_PASS'], encrypt: true }, // App URLs (used in email templates) { key: 'app_base_url', envVars: ['FRONTEND_URL', 'APP_BASE_URL'] }, ]; // ─── Main ───────────────────────────────────────────────────────────────────── async function main() { const prisma = new PrismaClient(); console.log(`\n${'─'.repeat(60)}`); console.log(' Hope Events — .env → DB settings migration'); if (DRY_RUN) console.log(' MODE: DRY RUN (no changes will be made)'); if (FORCE) console.log(' MODE: FORCE (will overwrite existing DB values)'); console.log(`${'─'.repeat(60)}\n`); try { // Fetch all existing DB settings once const existing = await prisma.appSetting.findMany(); const dbMap = new Map(existing.map(r => [r.key, r.value])); console.log(`Found ${dbMap.size} existing setting(s) in the database.\n`); const toWrite = []; const skipped = []; const noEnvVal = []; for (const { key, envVars, encrypt: shouldEncrypt } of MAPPINGS) { // Find first non-empty env var value let rawValue = ''; let sourceVar = ''; for (const v of envVars) { if (process.env[v] && process.env[v].trim()) { rawValue = process.env[v].trim(); sourceVar = v; break; } } if (!rawValue) { noEnvVal.push({ key, envVars }); continue; } const dbHasValue = dbMap.has(key) && dbMap.get(key) !== ''; if (dbHasValue && !FORCE) { skipped.push({ key, reason: 'already set in DB (use --force to overwrite)' }); continue; } const storedValue = (shouldEncrypt || ENCRYPTED_KEYS.has(key)) ? encrypt(rawValue) : rawValue; toWrite.push({ key, rawValue, storedValue, sourceVar, encrypted: !!(shouldEncrypt || ENCRYPTED_KEYS.has(key)), overwriting: dbHasValue }); } // ── Report what will be written ─────────────────────────────────────── if (toWrite.length === 0) { console.log('Nothing to write.\n'); } else { console.log(`Will write ${toWrite.length} setting(s):\n`); for (const { key, rawValue, sourceVar, encrypted, overwriting } of toWrite) { const display = encrypted ? '(encrypted)' : `"${rawValue}"`; const flag = overwriting ? ' [OVERWRITING]' : ''; console.log(` ✓ ${key.padEnd(28)} ← ${sourceVar} = ${display}${flag}`); } console.log(''); } if (skipped.length > 0) { console.log(`Skipped ${skipped.length} setting(s) (already in DB):\n`); for (const { key, reason } of skipped) { console.log(` · ${key.padEnd(28)} ${reason}`); } console.log(''); } if (noEnvVal.length > 0) { console.log(`No env value found for ${noEnvVal.length} setting(s) (nothing to migrate):\n`); for (const { key, envVars: vars } of noEnvVal) { console.log(` - ${key.padEnd(28)} checked: ${vars.join(', ')}`); } console.log(''); } // ── Write ───────────────────────────────────────────────────────────── if (DRY_RUN) { console.log('Dry run complete — no changes made.\n'); return; } if (toWrite.length === 0) { console.log('Nothing to do — all settings are already configured in the DB.\n'); return; } const ops = toWrite.map(({ key, storedValue }) => prisma.appSetting.upsert({ where: { key }, update: { value: storedValue }, create: { key, value: storedValue }, }) ); await prisma.$transaction(ops); console.log(`\n✓ Successfully wrote ${toWrite.length} setting(s) to the database.\n`); } finally { await prisma.$disconnect(); } } main().catch(err => { console.error('\n[error]', err.message || err); process.exit(1); });