#!/usr/bin/env node /** * Seed four super_admin control-panel users from config/omnl-super-admins.v1.json. * Requires DATABASE_URL and bcrypt (token-aggregation service deps). * * Usage: * node scripts/deployment/seed-omnl-super-admin-users.mjs * node scripts/deployment/seed-omnl-super-admin-users.mjs --dry-run * node scripts/deployment/seed-omnl-super-admin-users.mjs --credentials-only * * Passwords: generated per user unless OMNL_SUPER_ADMIN__PASSWORD env is set. */ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import crypto from 'crypto'; import { createRequire } from 'module'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(__dirname, '../..'); const taDir = path.join(repoRoot, 'services/token-aggregation'); const require = createRequire(path.join(taDir, 'package.json')); const bcrypt = require('bcrypt'); const pg = require('pg'); const configPath = path.join(repoRoot, 'config/omnl-super-admins.v1.json'); const dryRun = process.argv.includes('--dry-run'); const credentialsOnly = process.argv.includes('--credentials-only'); const stopServices = process.argv.includes('--stop-services'); const envFile = process.env.OMNL_BANK_ENV || path.join(repoRoot, '.env'); if (fs.existsSync(envFile)) { for (const line of fs.readFileSync(envFile, 'utf8').split('\n')) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const eq = trimmed.indexOf('='); if (eq <= 0) continue; const key = trimmed.slice(0, eq).trim(); if (process.env[key] !== undefined) continue; let val = trimmed.slice(eq + 1).trim(); if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { val = val.slice(1, -1); } process.env[key] = val; } } const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8')); const databaseUrl = process.env.DATABASE_URL; function passwordFor(admin) { const envKey = `OMNL_SUPER_ADMIN_${admin.id.replace(/-/g, '_').toUpperCase()}_PASSWORD`; const fromEnv = process.env[envKey]?.trim(); if (fromEnv) return fromEnv; return crypto.randomBytes(18).toString('base64url'); } async function hashPassword(plain) { return bcrypt.hash(plain, 12); } const rows = []; for (const admin of cfg.superAdmins) { const password = passwordFor(admin); rows.push({ admin, password, hash: dryRun ? '(dry-run)' : await hashPassword(password) }); } if (dryRun) { console.log(JSON.stringify(rows.map(({ admin, password }) => ({ id: admin.id, username: admin.username, email: admin.email, portalCtid: admin.portalCtid, passwordWouldGenerate: !process.env[`OMNL_SUPER_ADMIN_${admin.id.replace(/-/g, '_').toUpperCase()}_PASSWORD`], })), null, 2)); process.exit(0); } if (credentialsOnly) { const out = []; for (const { admin, password } of rows) { out.push({ id: admin.id, displayName: admin.displayName, portalCtid: admin.portalCtid, products: admin.products, username: admin.username, email: admin.email, password, envKey: admin.envKey, }); } console.log(JSON.stringify({ generatedAt: new Date().toISOString(), superAdmins: out }, null, 2)); process.exit(0); } if (!databaseUrl) { console.error('Set DATABASE_URL (PostgreSQL) to seed control-panel super_admin users.'); process.exit(1); } if (stopServices) { const { execSync } = await import('child_process'); for (const pattern of ['token-aggregation', 'settlement-middleware', 'dbis-exchange']) { try { execSync(`pkill -f "smom-dbis-138/services/${pattern}" || true`, { stdio: 'ignore' }); } catch { /* ignore */ } } await new Promise((r) => setTimeout(r, 2000)); } const client = new pg.Client({ connectionString: databaseUrl }); try { await client.connect(); await client.query(` CREATE TABLE IF NOT EXISTS admin_users ( id SERIAL PRIMARY KEY, username VARCHAR(255) UNIQUE NOT NULL, email VARCHAR(255), password_hash TEXT NOT NULL, role VARCHAR(50) NOT NULL DEFAULT 'admin', is_active BOOLEAN DEFAULT true, last_login TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); `); for (const { admin, password, hash } of rows) { await client.query( `INSERT INTO admin_users (username, email, password_hash, role, is_active) VALUES ($1, $2, $3, 'super_admin', true) ON CONFLICT (username) DO UPDATE SET email = EXCLUDED.email, password_hash = EXCLUDED.password_hash, role = 'super_admin', is_active = true, updated_at = NOW()`, [admin.username, admin.email, hash], ); } console.log('=== OMNL super_admin accounts (one-time — distribute securely) ==='); for (const { admin, password } of rows) { console.log(`${admin.displayName}`); console.log(` Portal CT ${admin.portalCtid} · ${admin.products.join(', ')}`); console.log(` Username: ${admin.username}`); console.log(` Email: ${admin.email}`); console.log(` Password: ${password}`); console.log(''); } } finally { await client.end().catch(() => {}); }