383 行
11 KiB
JavaScript
383 行
11 KiB
JavaScript
require("dotenv/config");
|
||
|
||
const crypto = require("node:crypto");
|
||
|
||
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([
|
||
"erp_sync",
|
||
"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 },
|
||
});
|
||
}
|
||
|
||
function generateOneTimePassword() {
|
||
// 确保满足 passwordPolicy:>=12 + 小写 + 大写 + 数字
|
||
const suffix = crypto.randomBytes(6).toString("hex");
|
||
return `TrialrunA1${suffix}`;
|
||
}
|
||
|
||
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",
|
||
purchaseRequests: "admin",
|
||
travelRequests: "admin",
|
||
budgets: "admin",
|
||
paymentBatches: "admin",
|
||
apOperations: "admin",
|
||
cutover: "admin",
|
||
erpIntegrations: "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",
|
||
purchaseRequests: "admin",
|
||
travelRequests: "admin",
|
||
budgets: "admin",
|
||
paymentBatches: "admin",
|
||
apOperations: "admin",
|
||
cutover: "admin",
|
||
erpIntegrations: "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 };
|
||
}
|
||
|
||
const password = params.password || generateOneTimePassword();
|
||
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 ?? "T0000",
|
||
name: process.env.TRIALRUN_ADMIN_NAME ?? "Trialrun Admin",
|
||
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(`- email: ${adminEmail}`);
|
||
// eslint-disable-next-line no-console
|
||
console.log(`- phone: ${adminPhone}`);
|
||
// 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(`- one-time password: ${result.password}`);
|
||
// eslint-disable-next-line no-console
|
||
console.log(" (请立刻保存,并在首次登录后由管理员重置/更换密码)");
|
||
} 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);
|
||
});
|