#!/usr/bin/env node const { PrismaClient } = require('@prisma/client'); const bcrypt = require('bcryptjs'); const { v4: uuidv4 } = require('uuid'); const prisma = new PrismaClient(); const DEFAULT_ROLES = [ { roleName: 'DBIS_Super_Admin', roleDescription: 'DBIS Super Admin - Full network control', accessLevel: 'tier_1', permissions: ['all'], }, { roleName: 'DBIS_Ops', roleDescription: 'DBIS Operations - Network monitoring and operations', accessLevel: 'tier_2', permissions: ['network_operations', 'gas_qps_control', 'incident_management'], }, { roleName: 'DBIS_Risk', roleDescription: 'DBIS Risk Officer - Risk monitoring and compliance', accessLevel: 'tier_2', permissions: ['risk_monitoring', 'compliance_audit', 'stress_testing'], }, { roleName: 'SCB_Admin', roleDescription: 'SCB Admin - Full control over jurisdiction', accessLevel: 'tier_3', permissions: ['scb_administration', 'fi_management', 'corridor_management'], }, { roleName: 'SCB_Risk', roleDescription: 'SCB Risk Officer - Local risk monitoring', accessLevel: 'tier_3', permissions: ['local_risk_monitoring', 'compliance_review'], }, { roleName: 'SCB_Tech', roleDescription: 'SCB Tech/API Owner - Technical operations', accessLevel: 'tier_3', permissions: ['api_management', 'technical_operations'], }, { roleName: 'SCB_Read_Only', roleDescription: 'SCB Read-Only - View-only access', accessLevel: 'tier_4', permissions: ['view_only'], }, ]; function getBootstrapEmployee() { return { employeeId: process.env.DBIS_BOOTSTRAP_EMPLOYEE_ID || 'dbis-core-ops-001', employeeName: process.env.DBIS_BOOTSTRAP_EMPLOYEE_NAME || 'Core Operations User', email: process.env.DBIS_BOOTSTRAP_EMPLOYEE_EMAIL || 'core.ops@d-bis.org', securityClearance: process.env.DBIS_BOOTSTRAP_EMPLOYEE_CLEARANCE || 'tier_1', roleName: process.env.DBIS_BOOTSTRAP_EMPLOYEE_ROLE || 'DBIS_Super_Admin', }; } function shouldBootstrapEmployee() { return Boolean( process.env.DBIS_BOOTSTRAP_EMPLOYEE_PASSWORD_HASH?.trim() || process.env.DBIS_BOOTSTRAP_EMPLOYEE_PASSWORD?.trim() || process.env.DBIS_PORTAL_SHARED_SECRET?.trim() ); } function getBootstrapMember() { return { memberId: process.env.DBIS_BOOTSTRAP_MEMBER_ID || 'member.test', memberName: process.env.DBIS_BOOTSTRAP_MEMBER_NAME || 'DBIS Member Test User', email: process.env.DBIS_BOOTSTRAP_MEMBER_EMAIL || 'member.test@members.d-bis.org', sovereignBankId: process.env.DBIS_BOOTSTRAP_MEMBER_SOVEREIGN_BANK_ID || null, }; } function shouldBootstrapMember() { return Boolean( process.env.DBIS_BOOTSTRAP_MEMBER_PASSWORD_HASH?.trim() || process.env.DBIS_BOOTSTRAP_MEMBER_PASSWORD?.trim() || process.env.DBIS_MEMBER_PORTAL_SHARED_SECRET?.trim() ); } function getBootstrapPasswordHash() { const explicitHash = process.env.DBIS_BOOTSTRAP_EMPLOYEE_PASSWORD_HASH?.trim(); if (explicitHash) { return explicitHash; } const explicitPassword = process.env.DBIS_BOOTSTRAP_EMPLOYEE_PASSWORD?.trim(); if (explicitPassword) { return bcrypt.hashSync(explicitPassword, 12); } const legacyBootstrapSecret = process.env.DBIS_PORTAL_SHARED_SECRET?.trim(); if (legacyBootstrapSecret) { return bcrypt.hashSync(legacyBootstrapSecret, 12); } throw new Error( 'Set DBIS_BOOTSTRAP_EMPLOYEE_PASSWORD_HASH or DBIS_BOOTSTRAP_EMPLOYEE_PASSWORD before bootstrapping portal auth' ); } function getBootstrapMemberPasswordHash() { const explicitHash = process.env.DBIS_BOOTSTRAP_MEMBER_PASSWORD_HASH?.trim(); if (explicitHash) { return explicitHash; } const explicitPassword = process.env.DBIS_BOOTSTRAP_MEMBER_PASSWORD?.trim(); if (explicitPassword) { return bcrypt.hashSync(explicitPassword, 12); } const legacyMemberSecret = process.env.DBIS_MEMBER_PORTAL_SHARED_SECRET?.trim(); if (legacyMemberSecret) { return bcrypt.hashSync(legacyMemberSecret, 12); } throw new Error( 'Set DBIS_BOOTSTRAP_MEMBER_PASSWORD_HASH or DBIS_BOOTSTRAP_MEMBER_PASSWORD before bootstrapping member portal auth' ); } async function ensureSchema() { await prisma.$executeRawUnsafe(` CREATE TABLE IF NOT EXISTS dbis_roles ( id TEXT PRIMARY KEY, "roleId" TEXT UNIQUE NOT NULL, "roleName" TEXT NOT NULL, "roleDescription" TEXT NOT NULL, "accessLevel" TEXT NOT NULL, permissions JSONB NOT NULL, status TEXT NOT NULL DEFAULT 'active', "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), "updatedAt" TIMESTAMPTZ NOT NULL ) `); await prisma.$executeRawUnsafe( `CREATE INDEX IF NOT EXISTS dbis_roles_access_level_idx ON dbis_roles ("accessLevel")` ); await prisma.$executeRawUnsafe( `CREATE INDEX IF NOT EXISTS dbis_roles_role_id_idx ON dbis_roles ("roleId")` ); await prisma.$executeRawUnsafe( `CREATE INDEX IF NOT EXISTS dbis_roles_role_name_idx ON dbis_roles ("roleName")` ); await prisma.$executeRawUnsafe(` CREATE TABLE IF NOT EXISTS employee_credentials ( id TEXT PRIMARY KEY, "employeeId" TEXT UNIQUE NOT NULL, "roleId" TEXT NOT NULL, "employeeName" TEXT NOT NULL, email TEXT NOT NULL, "securityClearance" TEXT NOT NULL, "portalPasswordHash" TEXT, "cryptographicBadgeId" TEXT, "hsmCredentialId" TEXT, status TEXT NOT NULL DEFAULT 'active', "issuedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), "expiresAt" TIMESTAMPTZ, "revokedAt" TIMESTAMPTZ, "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), "updatedAt" TIMESTAMPTZ NOT NULL, CONSTRAINT employee_credentials_role_fk FOREIGN KEY ("roleId") REFERENCES dbis_roles(id) ON DELETE CASCADE ) `); await prisma.$executeRawUnsafe( `CREATE INDEX IF NOT EXISTS employee_credentials_employee_id_idx ON employee_credentials ("employeeId")` ); await prisma.$executeRawUnsafe( `ALTER TABLE employee_credentials ADD COLUMN IF NOT EXISTS "portalPasswordHash" TEXT` ); await prisma.$executeRawUnsafe( `ALTER TABLE employee_credentials ADD COLUMN IF NOT EXISTS "mustRotatePassword" BOOLEAN NOT NULL DEFAULT FALSE` ); await prisma.$executeRawUnsafe( `ALTER TABLE employee_credentials ADD COLUMN IF NOT EXISTS "failedLoginAttempts" INTEGER NOT NULL DEFAULT 0` ); await prisma.$executeRawUnsafe( `ALTER TABLE employee_credentials ADD COLUMN IF NOT EXISTS "lockedUntil" TIMESTAMPTZ` ); await prisma.$executeRawUnsafe( `ALTER TABLE employee_credentials ADD COLUMN IF NOT EXISTS "lastLoginAt" TIMESTAMPTZ` ); await prisma.$executeRawUnsafe( `ALTER TABLE employee_credentials ADD COLUMN IF NOT EXISTS "passwordChangedAt" TIMESTAMPTZ` ); await prisma.$executeRawUnsafe( `ALTER TABLE employee_credentials ADD COLUMN IF NOT EXISTS "passwordResetTokenHash" TEXT` ); await prisma.$executeRawUnsafe( `ALTER TABLE employee_credentials ADD COLUMN IF NOT EXISTS "passwordResetTokenExpiresAt" TIMESTAMPTZ` ); await prisma.$executeRawUnsafe( `ALTER TABLE employee_credentials ADD COLUMN IF NOT EXISTS "mfaEnabled" BOOLEAN NOT NULL DEFAULT FALSE` ); await prisma.$executeRawUnsafe( `ALTER TABLE employee_credentials ADD COLUMN IF NOT EXISTS "mfaSecretCiphertext" TEXT` ); await prisma.$executeRawUnsafe( `ALTER TABLE employee_credentials ADD COLUMN IF NOT EXISTS "mfaSecretIv" TEXT` ); await prisma.$executeRawUnsafe( `ALTER TABLE employee_credentials ADD COLUMN IF NOT EXISTS "mfaSecretTag" TEXT` ); await prisma.$executeRawUnsafe( `ALTER TABLE employee_credentials ADD COLUMN IF NOT EXISTS "mfaEnrolledAt" TIMESTAMPTZ` ); await prisma.$executeRawUnsafe( `CREATE INDEX IF NOT EXISTS employee_credentials_role_id_idx ON employee_credentials ("roleId")` ); await prisma.$executeRawUnsafe( `CREATE INDEX IF NOT EXISTS employee_credentials_security_clearance_idx ON employee_credentials ("securityClearance")` ); await prisma.$executeRawUnsafe( `CREATE INDEX IF NOT EXISTS employee_credentials_status_idx ON employee_credentials (status)` ); await prisma.$executeRawUnsafe(` CREATE TABLE IF NOT EXISTS portal_member_accounts ( id TEXT PRIMARY KEY, "memberId" TEXT UNIQUE NOT NULL, "memberName" TEXT NOT NULL, email TEXT UNIQUE NOT NULL, "institutionName" TEXT, "institutionCountry" TEXT, "participantId" TEXT, lei TEXT, "sovereignBankId" TEXT, "portalPasswordHash" TEXT NOT NULL, "approvalStatus" TEXT NOT NULL DEFAULT 'pending', "approvedAt" TIMESTAMPTZ, "approvedByEmployeeId" TEXT, "mustRotatePassword" BOOLEAN NOT NULL DEFAULT FALSE, "failedLoginAttempts" INTEGER NOT NULL DEFAULT 0, "lockedUntil" TIMESTAMPTZ, "lastLoginAt" TIMESTAMPTZ, "passwordChangedAt" TIMESTAMPTZ, "passwordResetTokenHash" TEXT, "passwordResetTokenExpiresAt" TIMESTAMPTZ, status TEXT NOT NULL DEFAULT 'active', "issuedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), "expiresAt" TIMESTAMPTZ, "revokedAt" TIMESTAMPTZ, "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), "updatedAt" TIMESTAMPTZ NOT NULL ) `); await prisma.$executeRawUnsafe( `ALTER TABLE portal_member_accounts ADD COLUMN IF NOT EXISTS "institutionName" TEXT` ); await prisma.$executeRawUnsafe( `ALTER TABLE portal_member_accounts ADD COLUMN IF NOT EXISTS "institutionCountry" TEXT` ); await prisma.$executeRawUnsafe( `CREATE INDEX IF NOT EXISTS portal_member_accounts_member_id_idx ON portal_member_accounts ("memberId")` ); await prisma.$executeRawUnsafe( `CREATE INDEX IF NOT EXISTS portal_member_accounts_email_idx ON portal_member_accounts (email)` ); await prisma.$executeRawUnsafe( `CREATE INDEX IF NOT EXISTS portal_member_accounts_institution_country_idx ON portal_member_accounts ("institutionCountry")` ); await prisma.$executeRawUnsafe( `CREATE INDEX IF NOT EXISTS portal_member_accounts_participant_id_idx ON portal_member_accounts ("participantId")` ); await prisma.$executeRawUnsafe( `CREATE INDEX IF NOT EXISTS portal_member_accounts_lei_idx ON portal_member_accounts (lei)` ); await prisma.$executeRawUnsafe( `CREATE INDEX IF NOT EXISTS portal_member_accounts_approval_status_idx ON portal_member_accounts ("approvalStatus")` ); await prisma.$executeRawUnsafe( `CREATE INDEX IF NOT EXISTS portal_member_accounts_status_idx ON portal_member_accounts (status)` ); await prisma.$executeRawUnsafe( `CREATE INDEX IF NOT EXISTS portal_member_accounts_sovereign_bank_id_idx ON portal_member_accounts ("sovereignBankId")` ); } async function upsertRoles() { for (const role of DEFAULT_ROLES) { const existing = await prisma.dbis_roles.findFirst({ where: { roleName: role.roleName }, }); if (existing) { await prisma.dbis_roles.update({ where: { id: existing.id }, data: { roleDescription: role.roleDescription, accessLevel: role.accessLevel, permissions: role.permissions, status: 'active', updatedAt: new Date(), }, }); continue; } await prisma.dbis_roles.create({ data: { id: uuidv4(), roleId: uuidv4(), roleName: role.roleName, roleDescription: role.roleDescription, accessLevel: role.accessLevel, permissions: role.permissions, status: 'active', updatedAt: new Date(), }, }); } } async function upsertBootstrapEmployee() { const employee = getBootstrapEmployee(); const portalPasswordHash = getBootstrapPasswordHash(); const role = await prisma.dbis_roles.findFirst({ where: { roleName: employee.roleName }, }); if (!role) { throw new Error(`Role ${employee.roleName} was not created`); } await prisma.employee_credentials.upsert({ where: { employeeId: employee.employeeId }, update: { roleId: role.id, employeeName: employee.employeeName, email: employee.email, securityClearance: employee.securityClearance, portalPasswordHash, status: 'active', revokedAt: null, updatedAt: new Date(), }, create: { id: uuidv4(), employeeId: employee.employeeId, roleId: role.id, employeeName: employee.employeeName, email: employee.email, securityClearance: employee.securityClearance, portalPasswordHash, status: 'active', updatedAt: new Date(), }, }); return employee; } async function upsertBootstrapMember() { const member = getBootstrapMember(); const portalPasswordHash = getBootstrapMemberPasswordHash(); const existing = await prisma.portal_member_accounts.findFirst({ where: { OR: [{ memberId: member.memberId }, { email: member.email }], }, }); if (existing) { await prisma.portal_member_accounts.update({ where: { id: existing.id }, data: { memberId: member.memberId, memberName: member.memberName, email: member.email, sovereignBankId: member.sovereignBankId, portalPasswordHash, status: 'active', revokedAt: null, updatedAt: new Date(), }, }); return member; } await prisma.portal_member_accounts.create({ data: { id: uuidv4(), memberId: member.memberId, memberName: member.memberName, email: member.email, sovereignBankId: member.sovereignBankId, portalPasswordHash, status: 'active', updatedAt: new Date(), }, }); return member; } async function main() { await ensureSchema(); await upsertRoles(); const employee = shouldBootstrapEmployee() ? await upsertBootstrapEmployee() : null; const member = shouldBootstrapMember() ? await upsertBootstrapMember() : null; console.log( JSON.stringify( { ok: true, bootstrapEmployee: employee ? { employeeId: employee.employeeId, email: employee.email, employeeName: employee.employeeName, roleName: employee.roleName, } : null, bootstrapMember: member ? { memberId: member.memberId, email: member.email, memberName: member.memberName, } : null, }, null, 2 ) ); } main() .catch(async (error) => { console.error(error.stack || error.message || String(error)); process.exitCode = 1; }) .finally(async () => { await prisma.$disconnect(); });