373 行
10 KiB
JavaScript
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);
|
|
});
|