446 lines
14 KiB
JavaScript
446 lines
14 KiB
JavaScript
#!/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();
|
|
});
|