/** * Idempotent Super Administrator provisioning, run on every container * start (see docker/entrypoint.sh) — NOT part of the installation wizard, * per the brief's explicit requirement that this account come from * environment variables. Safe to run repeatedly: re-hashes and syncs the * password/name from the env vars every run, so rotating * SUPER_ADMIN_PASSWORD and restarting the container is how you change it. * * Deliberately does NOT create a StaffProfile — that's what keeps the * Super Administrator out of the Team list, the customer-facing barber * picker, and off the Appointment.staffId foreign key entirely, by * construction rather than a filter. See src/lib/auth/permissions.ts * (hasPermission's isSuperAdmin short-circuit) and * src/lib/business/get-acting-business-context.ts. * * Uses relative imports (not the "@/..." alias) and constructs its own * PrismaClient, matching prisma/seed.ts — both scripts run under `tsx` * outside Next's own module resolution/bundling. */ import "dotenv/config"; import bcrypt from "bcryptjs"; import { PrismaClient } from "../generated/prisma/client"; import { PrismaMariaDb } from "@prisma/adapter-mariadb"; import { buildMysqlPoolConfig } from "../src/lib/db/mysql-pool-config"; const BCRYPT_COST = 12; // matches src/features/installation/installer.service.ts async function main() { const email = process.env.SUPER_ADMIN_EMAIL?.trim().toLowerCase(); if (!email) { console.log("[ensure-super-admin] SUPER_ADMIN_EMAIL not set — skipping (no Super Administrator configured)."); return; } const password = process.env.SUPER_ADMIN_PASSWORD; if (!password || password.length < 8) { console.error( "[ensure-super-admin] SUPER_ADMIN_EMAIL is set but SUPER_ADMIN_PASSWORD is missing or shorter than 8 characters. Refusing to provision a Super Administrator with no usable password.", ); process.exitCode = 1; return; } const firstName = process.env.SUPER_ADMIN_FIRST_NAME?.trim() ?? ""; const lastName = process.env.SUPER_ADMIN_LAST_NAME?.trim() ?? ""; const name = [firstName, lastName].filter(Boolean).join(" ") || null; const adapter = new PrismaMariaDb(buildMysqlPoolConfig()); const prisma = new PrismaClient({ adapter }); try { const passwordHash = await bcrypt.hash(password, BCRYPT_COST); const existing = await prisma.user.findUnique({ where: { email } }); if (existing && !existing.isSuperAdmin) { console.error( `[ensure-super-admin] A user with email ${email} already exists and is not a Super Administrator. Refusing to overwrite it — use a different SUPER_ADMIN_EMAIL or remove the conflicting account first.`, ); process.exitCode = 1; return; } if (existing) { await prisma.user.update({ where: { id: existing.id }, data: { name, passwordHash, emailVerified: existing.emailVerified ?? new Date() }, }); console.log(`[ensure-super-admin] Synced credentials for existing Super Administrator (${email}).`); } else { await prisma.user.create({ data: { email, name, passwordHash, isSuperAdmin: true, emailVerified: new Date() }, }); console.log(`[ensure-super-admin] Created Super Administrator (${email}).`); } } finally { await prisma.$disconnect(); } } main().catch((error) => { console.error("[ensure-super-admin] Failed:", error); process.exitCode = 1; });