统一 Node 22 与后端安全运行基线(07-15-production-trust-closeout-cleanup)

这个提交包含在:
2026-07-16 05:17:18 +08:00
父节点 dcf34d7ac6
当前提交 abb6a22f5c
共修改 28 个文件,包含 2250 行新增和 3012 行删除
+81
查看文件
@@ -0,0 +1,81 @@
require("dotenv/config");
const { Client } = require("pg");
const LOCAL_DATABASE_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
class DevSeedSafetyError extends Error {
constructor(code, message) {
super(message);
this.name = "DevSeedSafetyError";
this.code = code;
}
}
function refuse(code, message) {
throw new DevSeedSafetyError(code, message);
}
function validateDevSeedSafety(env = process.env) {
if (String(env.NODE_ENV || "").trim().toLowerCase() === "production") {
refuse("production_forbidden", "seed:dev is permanently disabled in production");
}
if (String(env.SEED_DEV_ALLOW || "").trim() !== "1") {
refuse("explicit_allow_required", "seed:dev requires the explicit local allow switch");
}
let databaseUrl;
try {
databaseUrl = new URL(String(env.DATABASE_URL || ""));
} catch {
refuse("database_url_invalid", "seed:dev requires a valid local PostgreSQL target");
}
if (!["postgres:", "postgresql:"].includes(databaseUrl.protocol)) {
refuse("database_protocol_invalid", "seed:dev requires a local PostgreSQL target");
}
let effectiveTarget;
try {
const client = new Client({ connectionString: databaseUrl.toString() });
effectiveTarget = {
host: String(client.host || "").toLowerCase(),
databaseName: String(client.database || "").trim(),
};
} catch {
refuse("database_url_invalid", "seed:dev requires a valid local PostgreSQL target");
}
if (!LOCAL_DATABASE_HOSTS.has(effectiveTarget.host)) {
refuse("database_host_not_local", "seed:dev only accepts a loopback database target");
}
const databaseName = decodeURIComponent(databaseUrl.pathname.replace(/^\/+/, "")).trim();
if (!databaseName || databaseName.includes("/") || effectiveTarget.databaseName !== databaseName) {
refuse("database_name_invalid", "seed:dev requires one explicit database name");
}
const confirmation = String(env.SEED_DEV_DATABASE_CONFIRM || "").trim();
if (!confirmation || confirmation !== databaseName) {
refuse("database_confirmation_mismatch", "seed:dev database confirmation does not match the target");
}
return {
databaseName,
resetPasswords: String(env.SEED_DEV_RESET_PASSWORDS || "").trim() === "1",
};
}
if (require.main === module) {
try {
const result = validateDevSeedSafety();
const passwordMode = result.resetPasswords ? "one_time_reset" : "preserve_existing";
console.log(`[seed-dev] safety check passed; passwordMode=${passwordMode}`);
} catch (error) {
const code = error instanceof DevSeedSafetyError ? error.code : "unexpected_error";
console.error(`[seed-dev][REFUSED] ${code}`);
process.exit(1);
}
}
module.exports = {
DevSeedSafetyError,
validateDevSeedSafety,
};
+49 -48
查看文件
@@ -2,6 +2,7 @@ require("dotenv/config");
const { createPrismaClient } = require("../prismaClient");
const { hashPassword } = require("../utils/crypto");
const { validateDevSeedSafety } = require("./dev-seed-safety");
async function ensureDepartment(prisma, payload) {
const now = new Date();
@@ -330,9 +331,8 @@ async function ensureWorkspaceSetting(prisma) {
return prisma.workspaceSetting.create({ data });
}
async function ensureEmployee(prisma, payload) {
async function ensureEmployee(prisma, payload, { resetPasswords = false } = {}) {
const now = new Date();
const passwordHash = await hashPassword(payload.password);
const existing = await prisma.employee.findFirst({
where: { email: payload.email },
@@ -340,6 +340,12 @@ async function ensureEmployee(prisma, payload) {
});
if (existing) {
const passwordData = resetPasswords
? {
password_hash: await hashPassword(payload.password),
must_change_password: true,
}
: {};
const employee = await prisma.employee.update({
where: { id: existing.id },
data: {
@@ -353,11 +359,10 @@ async function ensureEmployee(prisma, payload) {
status: "active",
bank_account_encrypted: "ENC_DEV_ONLY",
bank_name: "DEV_BANK",
password_hash: passwordHash,
must_change_password: false,
...passwordData,
updated_at: now,
},
select: { id: true, email: true },
select: { id: true },
});
await prisma.employeeRole.deleteMany({
@@ -373,9 +378,13 @@ async function ensureEmployee(prisma, payload) {
skipDuplicates: true,
});
return employee;
return {
...employee,
seedPasswordAction: resetPasswords ? "reset" : "preserved",
};
}
const passwordHash = await hashPassword(payload.password);
const employee = await prisma.employee.create({
data: {
employee_no: payload.employeeNo,
@@ -391,11 +400,11 @@ async function ensureEmployee(prisma, payload) {
bank_account_encrypted: "ENC_DEV_ONLY",
bank_name: "DEV_BANK",
password_hash: passwordHash,
must_change_password: false,
must_change_password: true,
created_at: now,
updated_at: now,
},
select: { id: true, email: true },
select: { id: true },
});
await prisma.employeeRole.createMany({
@@ -407,10 +416,11 @@ async function ensureEmployee(prisma, payload) {
skipDuplicates: true,
});
return employee;
return { ...employee, seedPasswordAction: "created" };
}
async function main() {
const safety = validateDevSeedSafety();
const { prisma, pool } = createPrismaClient();
await prisma.$connect();
@@ -451,6 +461,9 @@ async function main() {
});
await ensureWorkspaceSetting(prisma);
const ensureDevEmployee = (payload) => ensureEmployee(prisma, payload, {
resetPasswords: safety.resetPasswords,
});
for (const flagKey of [
"portal_home_v1",
@@ -477,7 +490,7 @@ async function main() {
});
}
const ceo = await ensureEmployee(prisma, {
const ceo = await ensureDevEmployee({
employeeNo: process.env.SEED_CEO_EMPLOYEE_NO ?? "HYD000001",
name: "张总",
phone: process.env.SEED_CEO_PHONE ?? "13900000000",
@@ -491,7 +504,7 @@ async function main() {
hireDate,
});
const admin = await ensureEmployee(prisma, {
const admin = await ensureDevEmployee({
employeeNo: process.env.SEED_ADMIN_EMPLOYEE_NO ?? "HYD000006",
name: "王顺利",
phone: process.env.SEED_ADMIN_PHONE ?? "13800000000",
@@ -505,7 +518,7 @@ async function main() {
hireDate,
});
const manager = await ensureEmployee(prisma, {
const manager = await ensureDevEmployee({
employeeNo: process.env.SEED_MANAGER_EMPLOYEE_NO ?? "HYD000002",
name: "李研发",
phone: process.env.SEED_MANAGER_PHONE ?? "13700000000",
@@ -519,7 +532,7 @@ async function main() {
hireDate,
});
const employee = await ensureEmployee(prisma, {
const employee = await ensureDevEmployee({
employeeNo: process.env.SEED_EMPLOYEE_EMPLOYEE_NO ?? "HYD000003",
name: "陈工程",
phone: process.env.SEED_EMPLOYEE_PHONE ?? "13600000000",
@@ -533,7 +546,7 @@ async function main() {
hireDate,
});
const finance = await ensureEmployee(prisma, {
const finance = await ensureDevEmployee({
employeeNo: process.env.SEED_FINANCE_EMPLOYEE_NO ?? "HYD000004",
name: "赵财务",
phone: process.env.SEED_FINANCE_PHONE ?? "13500000000",
@@ -547,7 +560,7 @@ async function main() {
hireDate,
});
const financeMgr = await ensureEmployee(prisma, {
const financeMgr = await ensureDevEmployee({
employeeNo: process.env.SEED_FINANCE_MGR_EMPLOYEE_NO ?? "HYD000005",
name: "钱主管",
phone: process.env.SEED_FINANCE_MGR_PHONE ?? "13400000000",
@@ -561,7 +574,7 @@ async function main() {
hireDate,
});
const hr = await ensureEmployee(prisma, {
const hr = await ensureDevEmployee({
employeeNo: process.env.SEED_HR_EMPLOYEE_NO ?? "HYD000007",
name: "孙人事",
phone: process.env.SEED_HR_PHONE ?? "13300000000",
@@ -575,7 +588,7 @@ async function main() {
hireDate,
});
const administration = await ensureEmployee(prisma, {
const administration = await ensureDevEmployee({
employeeNo: process.env.SEED_ADMINISTRATION_EMPLOYEE_NO ?? "HYD000008",
name: "周行政",
phone: process.env.SEED_ADMINISTRATION_PHONE ?? "13200000000",
@@ -659,40 +672,28 @@ async function main() {
warningThresholdPercent: 75,
});
// eslint-disable-next-line no-console
console.log("Seeded dev users:");
// eslint-disable-next-line no-console
console.log(`- 负责人: ${ceo.email} / ${process.env.SEED_CEO_PASSWORD ?? "CeoPassword1234"}`);
// eslint-disable-next-line no-console
console.log(`- 管理员: ${admin.email} / ${process.env.SEED_ADMIN_PASSWORD ?? "AdminPassword1234"}`);
// eslint-disable-next-line no-console
console.log(`- 研发负责人: ${manager.email} / ${process.env.SEED_MANAGER_PASSWORD ?? "ManagerPassword1234"}`);
// eslint-disable-next-line no-console
console.log(`- 员工: ${employee.email} / ${process.env.SEED_EMPLOYEE_PASSWORD ?? "EmployeePassword1234"}`);
// eslint-disable-next-line no-console
console.log(`- 财务: ${finance.email} / ${process.env.SEED_FINANCE_PASSWORD ?? "FinancePassword1234"}`);
// eslint-disable-next-line no-console
console.log(`- 财务主管: ${financeMgr.email} / ${process.env.SEED_FINANCE_MGR_PASSWORD ?? "FinanceMgrPassword1234"}`);
// eslint-disable-next-line no-console
console.log(`- 人事: ${hr.email} / ${process.env.SEED_HR_PASSWORD ?? "HrPassword1234"}`);
// eslint-disable-next-line no-console
console.log(`- 行政: ${administration.email} / ${process.env.SEED_ADMINISTRATION_PASSWORD ?? "AdministrationPassword1234"}`);
// eslint-disable-next-line no-console
console.log(`- Legal Entity: ${legalEntity.code} / ${legalEntity.name}`);
// eslint-disable-next-line no-console
console.log(`- HolidayCalendar: ${holidayCalendar.code} / ${holidayCalendar.name}`);
// eslint-disable-next-line no-console
console.log(`- Supplier: ${supplier.code} / ${supplier.name}`);
// eslint-disable-next-line no-console
console.log(`- Budget Pools: ${managementBudgetPool.code}, ${rdBudgetPool.code}`);
const users = [ceo, admin, manager, employee, finance, financeMgr, hr, administration];
const countAction = (action) => users.filter((item) => item.seedPasswordAction === action).length;
console.log(
`[seed-dev] completed; users=${users.length} created=${countAction("created")} ` +
`passwordsPreserved=${countAction("preserved")} oneTimeResets=${countAction("reset")}`,
);
console.log("[seed-dev] reference data synchronized; credential values were not printed");
} finally {
await prisma.$disconnect();
await pool.end();
}
}
main().catch((error) => {
// eslint-disable-next-line no-console
console.error(error);
process.exit(1);
});
if (require.main === module) {
main().catch((error) => {
const code = typeof error?.code === "string" ? error.code : error?.name || "unexpected_error";
console.error(`[seed-dev][FAIL] ${code}`);
process.exit(1);
});
}
module.exports = {
ensureEmployee,
main,
};