Files
hope-events/backend/scripts/migrate-env-to-db.js
T
joshua 3d381944d2 Initial commit
Next.js + Express event management app for Hope Family Church.
2026-07-23 15:26:47 +02:00

197 lines
7.5 KiB
JavaScript

/**
* 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);
});