require("dotenv/config"); const { createPrismaClient } = require("../prismaClient"); const { hashPassword } = require("../utils/crypto"); const { validatePassword } = require("../utils/passwordPolicy"); const DEFAULT_FORCE_ON_FLAGS = Object.freeze([ "portal_home_v1", "announcements_v1", "global_search_v1", "presence_board_v1", "attendance_v1", "attendance_unified_v1", "weekly_report_v1", "employee_lifecycle_v1", "todo_board_v1", "project_domain_v1", "project_linking_v1", "project_communication_log_v1", "project_closure_checklist_v1", "confidential_doc_refs_v1", ]); const OPTIONAL_FORCE_ON_FLAGS = new Set([ "budget_precheck_v1", "payment_batch_v1", "workflow_definition_admin_v1", "cutover_console_v1", ]); function requiredEnv(name) { const value = process.env[name]; if (typeof value !== "string" || !value.trim()) { throw new Error(`Missing required env: ${name}`); } return value.trim(); } function parseExtraForceOnFlags() { const raw = String(process.env.TRIALRUN_EXTRA_FORCE_ON_FLAGS ?? "").trim(); if (!raw) { return []; } const parts = raw .split(/[\s,]+/) .map((item) => item.trim()) .filter(Boolean); const unique = Array.from(new Set(parts)); const invalid = unique.filter((flagKey) => !OPTIONAL_FORCE_ON_FLAGS.has(flagKey)); if (invalid.length > 0) { throw new Error( `Unsupported TRIALRUN_EXTRA_FORCE_ON_FLAGS: ${invalid.join(", ")}. Allowed: ${Array.from( OPTIONAL_FORCE_ON_FLAGS, ).join(", ")}`, ); } return unique; } async function ensureDepartment(prisma, payload) { const now = new Date(); const existing = await prisma.department.findFirst({ where: { name: payload.name, parent_id: payload.parent_id ?? null, }, select: { id: true }, }); if (existing) { return prisma.department.update({ where: { id: existing.id }, data: { parent_id: payload.parent_id ?? null, archived_at: null, updated_at: now, }, select: { id: true }, }); } return prisma.department.create({ data: { name: payload.name, parent_id: payload.parent_id ?? null, created_at: now, updated_at: now, }, select: { id: true }, }); } async function ensureFeatureFlag(prisma, payload) { const now = new Date(); const existing = await prisma.featureFlag.findUnique({ where: { flag_key: payload.flag_key }, select: { id: true, version: true }, }); if (existing) { return prisma.featureFlag.update({ where: { id: existing.id }, data: { override_mode: payload.override_mode, rollout_type: payload.rollout_type, department_ids: payload.department_ids, note: payload.note, version: existing.version + 1, updated_at: now, }, }); } return prisma.featureFlag.create({ data: { ...payload, created_at: now, updated_at: now, }, }); } async function ensureWorkspaceSetting(prisma) { const current = await prisma.workspaceSetting.findFirst({ select: { id: true } }); if (current) { return prisma.workspaceSetting.update({ where: { id: current.id }, data: { profile: "small_team_internal", org_mode: "single_company", primary_entry_mode: "password", notification_strategy: "in_app", device_priority: "desktop", module_visibility_json: { portal: "main", team: "main", leaveRequests: "main", reimbursements: "main", assets: "main", weeklyReports: "main", todos: "main", announcements: "main", workspace: "admin", people: "admin", workflowDefinitions: "admin", systemSettings: "admin", featureFlags: "admin", auditLogs: "admin", contractsRegistry: "main", purchaseRequests: "main", travelRequests: "admin", budgets: "admin", paymentBatches: "admin", apOperations: "admin", cutover: "admin", }, }, }); } return prisma.workspaceSetting.create({ data: { profile: "small_team_internal", org_mode: "single_company", primary_entry_mode: "password", notification_strategy: "in_app", device_priority: "desktop", module_visibility_json: { portal: "main", team: "main", leaveRequests: "main", reimbursements: "main", assets: "main", weeklyReports: "main", todos: "main", announcements: "main", workspace: "admin", people: "admin", workflowDefinitions: "admin", systemSettings: "admin", featureFlags: "admin", auditLogs: "admin", contractsRegistry: "main", purchaseRequests: "main", travelRequests: "admin", budgets: "admin", paymentBatches: "admin", apOperations: "admin", cutover: "admin", }, }, }); } async function ensureAdminEmployee(prisma, params) { const now = new Date(); const existing = await prisma.employee.findFirst({ where: { OR: [{ email: params.email }, { phone: params.phone }] }, select: { id: true, employee_no: true, email: true, phone: true, status: true, archived_at: true }, }); if (existing) { if ((existing.status === "archived" || existing.archived_at) && process.env.TRIALRUN_ADMIN_REACTIVATE_ARCHIVED !== "1") { const fallbackAdmin = await findActiveAdminEmployee(prisma, existing.id); if (fallbackAdmin) { return { employeeId: fallbackAdmin.id, created: false, password: null, skippedArchived: true, fallbackEmployeeNo: fallbackAdmin.employee_no, }; } } await prisma.employee.update({ where: { id: existing.id }, data: { status: "active", archived_at: null, updated_at: now, }, }); await prisma.employeeRole.deleteMany({ where: { employee_id: existing.id }, }); await prisma.employeeRole.createMany({ data: [ { employee_id: existing.id, role_code: "admin", created_at: now, created_by: null }, { employee_id: existing.id, role_code: "employee", created_at: now, created_by: null }, ], skipDuplicates: true, }); return { employeeId: existing.id, created: false, password: null }; } if (!params.password) { throw new Error('TRIALRUN_ADMIN_PASSWORD is required when creating the trialrun admin; credentials are never generated into deployment logs'); } const password = params.password; const errors = validatePassword(password); if (errors.length) { throw new Error(`Invalid TRIALRUN_ADMIN_PASSWORD: ${errors.join("; ")}`); } const passwordHash = await hashPassword(password); const employee = await prisma.employee.create({ data: { employee_no: params.employeeNo, name: params.name, phone: params.phone, email: params.email, department_id: params.departmentId, title: params.title, direct_manager_id: null, hire_date: params.hireDate, status: "active", bank_account_encrypted: "ENC_TRIALRUN", bank_name: "TRIALRUN_BANK", password_hash: passwordHash, must_change_password: true, created_at: now, created_by: null, updated_at: now, updated_by: null, }, select: { id: true }, }); await prisma.employeeRole.createMany({ data: [ { employee_id: employee.id, role_code: "admin", created_at: now, created_by: null }, { employee_id: employee.id, role_code: "employee", created_at: now, created_by: null }, ], skipDuplicates: true, }); return { employeeId: employee.id, created: true, password }; } async function findActiveAdminEmployee(prisma, excludedEmployeeId) { const where = { status: "active", archived_at: null, roles: { some: { role_code: "admin" } }, }; if (excludedEmployeeId) { where.id = { not: excludedEmployeeId }; } return prisma.employee.findFirst({ where, orderBy: { employee_no: "asc" }, select: { id: true, employee_no: true }, }); } async function main() { const adminEmail = requiredEnv("TRIALRUN_ADMIN_EMAIL"); const adminPhone = requiredEnv("TRIALRUN_ADMIN_PHONE"); const adminPassword = process.env.TRIALRUN_ADMIN_PASSWORD?.trim() || ""; const { prisma, pool } = createPrismaClient(); await prisma.$connect(); try { const root = await ensureDepartment(prisma, { name: "公司", parent_id: null }); const management = await ensureDepartment(prisma, { name: "管理部", parent_id: root.id }); const result = await ensureAdminEmployee(prisma, { email: adminEmail, phone: adminPhone, password: adminPassword || null, employeeNo: process.env.TRIALRUN_ADMIN_EMPLOYEE_NO ?? "HYD000006", name: process.env.TRIALRUN_ADMIN_NAME ?? "王顺利", title: process.env.TRIALRUN_ADMIN_TITLE ?? "系统管理员", departmentId: management.id, hireDate: new Date(process.env.TRIALRUN_ADMIN_HIRE_DATE ?? "2020-01-01"), }); await ensureWorkspaceSetting(prisma); const extraForceOnFlags = parseExtraForceOnFlags(); const forceOnFlags = [...DEFAULT_FORCE_ON_FLAGS, ...extraForceOnFlags]; for (const flagKey of forceOnFlags) { await ensureFeatureFlag(prisma, { flag_key: flagKey, override_mode: "force_on", rollout_type: "global", department_ids: [], note: "trialrun default: force_on", }); } // eslint-disable-next-line no-console console.log("Seeded trialrun admin:"); // eslint-disable-next-line no-console console.log("- login identifiers: configured"); // eslint-disable-next-line no-console console.log(`- created: ${result.created}`); // eslint-disable-next-line no-console console.log(`- force_on flags: ${forceOnFlags.join(", ")}`); if (result.skippedArchived) { // eslint-disable-next-line no-console console.log(`- archived trialrun admin: kept archived; active admin fallback: ${result.fallbackEmployeeNo}`); } if (result.password) { // eslint-disable-next-line no-console console.log("- initial password: supplied by approved configuration; not logged"); } else { // eslint-disable-next-line no-console console.log("- password: unchanged"); } } finally { await prisma.$disconnect(); await pool.end(); } } main().catch((error) => { // eslint-disable-next-line no-console console.error(error); process.exit(1); });