文件
hyb-oa/server/src/scripts/seed-trialrun.js
T

373 行
10 KiB
JavaScript

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);
});