3d381944d2
Next.js + Express event management app for Hope Family Church.
197 lines
7.5 KiB
JavaScript
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);
|
|
}); |