OAPERM1 收紧启用前权限冻结

这个提交包含在:
2026-05-24 00:57:11 +08:00
父节点 a3f13eab57
当前提交 65513a5291
共修改 27 个文件,包含 231 行新增和 149 行删除
-13
查看文件
@@ -44,7 +44,6 @@ const EMPLOYEE_BASE_PERMISSIONS = [
...EXPENSE_SELF_SERVICE_PERMISSIONS,
...LEAVE_SELF_SERVICE_PERMISSIONS,
...ATTENDANCE_SELF_SERVICE_PERMISSIONS,
...TRAVEL_SELF_SERVICE_PERMISSIONS,
...PROJECT_SELF_SERVICE_PERMISSIONS,
];
@@ -154,8 +153,6 @@ const ROLE_PERMISSIONS = {
"attendance.view_all",
"procurement.approve",
"procurement.view_all",
"travel.approve",
"travel.view_all",
"asset.view_all",
"report.dashboard.view",
"project.view_all",
@@ -176,8 +173,6 @@ const ROLE_PERMISSIONS = {
"attendance.view_all",
"procurement.approve",
"procurement.view_all",
"travel.approve",
"travel.view_all",
"asset.view_all",
"report.dashboard.view",
"budget.view",
@@ -194,8 +189,6 @@ const ROLE_PERMISSIONS = {
"payment.batch.manage",
"expense.view_all",
"procurement.view_all",
"travel.view_all",
"travel.payment.manage",
"asset.view_all",
"attendance.view_all",
],
@@ -217,9 +210,6 @@ const ROLE_PERMISSIONS = {
"expense.finance.override_duplicate",
"procurement.view_all",
"procurement.manage",
"travel.view_all",
"travel.manage",
"travel.payment.manage",
"asset.view_all",
"asset.manage",
"asset.inventory",
@@ -237,9 +227,6 @@ const ROLE_PERMISSIONS = {
...EMPLOYEE_BASE_PERMISSIONS,
"procurement.view_all",
"procurement.manage",
"travel.view_all",
"travel.manage",
"travel.payment.manage",
"asset.view_all",
"asset.manage",
"asset.transfer",
+2 -1
查看文件
@@ -844,7 +844,8 @@ async function employeesRoutes(fastify) {
const stats = await buildEmployeeStats(fastify.prisma, {
timeZone: fastify.config.appTimezone,
includeSalarySummary: fastify.hasPermission(request.employee, "system.users"),
includeSalarySummary: fastify.hasPermission(request.employee, "system.settings"),
includePerformanceSummary: fastify.hasPermission(request.employee, "system.settings"),
});
return sendData(reply, request, stats);
});
+1 -1
查看文件
@@ -1762,7 +1762,7 @@ async function exportsRoutes(fastify) {
fastify.get(
"/exports/payslips",
{
preHandler: fastify.requirePermission("system.users"),
preHandler: fastify.requirePermission("system.settings"),
},
async (request, reply) => {
const query = parseQuery(PAYSLIP_EXPORT_QUERY_SCHEMA, request.query);
+12 -11
查看文件
@@ -21,6 +21,7 @@ const DATE_SCHEMA = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
const PAY_MONTH_SCHEMA = z.string().regex(/^\d{4}-\d{2}$/);
const MONEY_INPUT_SCHEMA = z.union([z.number(), z.string()]);
const SALARY_ITEM_TYPE_SCHEMA = z.enum(["earning", "deduction"]);
const PAYROLL_MANAGE_PERMISSION = "system.settings";
const SALARY_ITEM_CREATE_SCHEMA = z
.object({
@@ -232,14 +233,14 @@ async function ensurePayslipExists(prisma, payslipId) {
}
function canViewEmployeePayslips(fastify, request, employeeId) {
return request.employee.id === employeeId || fastify.hasPermission(request.employee, "system.users");
return request.employee.id === employeeId || fastify.hasPermission(request.employee, PAYROLL_MANAGE_PERMISSION);
}
async function payrollRoutes(fastify) {
fastify.get(
"/payroll/items",
{
preHandler: fastify.requirePermission("system.users"),
preHandler: fastify.requirePermission(PAYROLL_MANAGE_PERMISSION),
},
async (request, reply) => {
const items = await fastify.prisma.salaryItem.findMany({
@@ -255,7 +256,7 @@ async function payrollRoutes(fastify) {
fastify.post(
"/payroll/items",
{
preHandler: fastify.requirePermission("system.users"),
preHandler: fastify.requirePermission(PAYROLL_MANAGE_PERMISSION),
},
async (request, reply) => {
const body = parseBody(SALARY_ITEM_CREATE_SCHEMA, request.body);
@@ -300,7 +301,7 @@ async function payrollRoutes(fastify) {
fastify.patch(
"/payroll/items/:itemId",
{
preHandler: fastify.requirePermission("system.users"),
preHandler: fastify.requirePermission(PAYROLL_MANAGE_PERMISSION),
},
async (request, reply) => {
const itemId = parseId(request.params?.itemId);
@@ -347,7 +348,7 @@ async function payrollRoutes(fastify) {
fastify.get(
"/payroll/employees/:employeeId/profile",
{
preHandler: fastify.requirePermission("system.users"),
preHandler: fastify.requirePermission(PAYROLL_MANAGE_PERMISSION),
},
async (request, reply) => {
const employeeId = parseId(request.params?.employeeId);
@@ -375,7 +376,7 @@ async function payrollRoutes(fastify) {
fastify.put(
"/payroll/employees/:employeeId/profile",
{
preHandler: fastify.requirePermission("system.users"),
preHandler: fastify.requirePermission(PAYROLL_MANAGE_PERMISSION),
},
async (request, reply) => {
const employeeId = parseId(request.params?.employeeId);
@@ -461,7 +462,7 @@ async function payrollRoutes(fastify) {
}
await ensureEmployeeExists(fastify.prisma, employeeId);
const isAdmin = fastify.hasPermission(request.employee, "system.users");
const isAdmin = fastify.hasPermission(request.employee, PAYROLL_MANAGE_PERMISSION);
const payslips = await fastify.prisma.payslip.findMany({
where: {
employee_id: employeeId,
@@ -496,7 +497,7 @@ async function payrollRoutes(fastify) {
fastify.get(
"/payroll/payslips",
{
preHandler: fastify.requirePermission("system.users"),
preHandler: fastify.requirePermission(PAYROLL_MANAGE_PERMISSION),
},
async (request, reply) => {
const query = parseQuery(PAYSLIP_QUERY_SCHEMA, request.query);
@@ -519,7 +520,7 @@ async function payrollRoutes(fastify) {
fastify.post(
"/payroll/payslips/generate",
{
preHandler: fastify.requirePermission("system.users"),
preHandler: fastify.requirePermission(PAYROLL_MANAGE_PERMISSION),
},
async (request, reply) => {
const body = parseBody(PAYSLIP_GENERATE_SCHEMA, request.body);
@@ -679,7 +680,7 @@ async function payrollRoutes(fastify) {
fastify.patch(
"/payroll/payslips/:payslipId",
{
preHandler: fastify.requirePermission("system.users"),
preHandler: fastify.requirePermission(PAYROLL_MANAGE_PERMISSION),
},
async (request, reply) => {
const payslipId = parseId(request.params?.payslipId);
@@ -753,7 +754,7 @@ async function payrollRoutes(fastify) {
fastify.post(
"/payroll/payslips/issue",
{
preHandler: fastify.requirePermission("system.users"),
preHandler: fastify.requirePermission(PAYROLL_MANAGE_PERMISSION),
},
async (request, reply) => {
const body = parseBody(PAYSLIP_ISSUE_SCHEMA, request.body);
+10 -15
查看文件
@@ -25,6 +25,7 @@ const SCORE_SCHEMA = z
.refine((value) => Number.isFinite(value) && Math.abs(value * 2 - Math.round(value * 2)) < 1e-9, {
message: "invalid_score_step",
});
const PERFORMANCE_MANAGE_PERMISSION = "system.settings";
const CYCLE_CREATE_SCHEMA = z
.object({
@@ -212,15 +213,9 @@ function mapReview(review) {
}
function resolveViewerRole(fastify, request, employee) {
if (fastify.hasPermission(request.employee, "system.users")) {
if (fastify.hasPermission(request.employee, PERFORMANCE_MANAGE_PERMISSION)) {
return "admin";
}
if (request.employee.id === employee.id) {
return "self";
}
if (employee.direct_manager_id && employee.direct_manager_id === request.employee.id) {
return "manager";
}
return null;
}
@@ -386,8 +381,8 @@ async function buildEmployeeTimeline(prisma, employeeId, viewerRole) {
async function performanceRoutes(fastify) {
fastify.get("/performance/cycles", { preHandler: fastify.authenticate }, async (request, reply) => {
await ensureFeatureEnabled(fastify, request);
if (!fastify.hasPermission(request.employee, "system.users")) {
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "system.users" } });
if (!fastify.hasPermission(request.employee, PERFORMANCE_MANAGE_PERMISSION)) {
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: PERFORMANCE_MANAGE_PERMISSION } });
}
const cycles = await fastify.prisma.performanceReviewCycle.findMany();
@@ -398,8 +393,8 @@ async function performanceRoutes(fastify) {
fastify.post("/performance/cycles", { preHandler: fastify.authenticate }, async (request, reply) => {
await ensureFeatureEnabled(fastify, request);
if (!fastify.hasPermission(request.employee, "system.users")) {
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "system.users" } });
if (!fastify.hasPermission(request.employee, PERFORMANCE_MANAGE_PERMISSION)) {
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: PERFORMANCE_MANAGE_PERMISSION } });
}
const body = parseBody(CYCLE_CREATE_SCHEMA, request.body);
@@ -431,8 +426,8 @@ async function performanceRoutes(fastify) {
fastify.patch("/performance/cycles/:cycleId", { preHandler: fastify.authenticate }, async (request, reply) => {
await ensureFeatureEnabled(fastify, request);
if (!fastify.hasPermission(request.employee, "system.users")) {
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "system.users" } });
if (!fastify.hasPermission(request.employee, PERFORMANCE_MANAGE_PERMISSION)) {
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: PERFORMANCE_MANAGE_PERMISSION } });
}
const cycleId = parseId(request.params?.cycleId);
@@ -508,8 +503,8 @@ async function performanceRoutes(fastify) {
fastify.get("/performance/cycles/:cycleId/reviews", { preHandler: fastify.authenticate }, async (request, reply) => {
await ensureFeatureEnabled(fastify, request);
if (!fastify.hasPermission(request.employee, "system.users")) {
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "system.users" } });
if (!fastify.hasPermission(request.employee, PERFORMANCE_MANAGE_PERMISSION)) {
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: PERFORMANCE_MANAGE_PERMISSION } });
}
const cycleId = parseId(request.params?.cycleId);
+11 -3
查看文件
@@ -65,6 +65,14 @@ async function isFlagEnabled(fastify, request, flagKey) {
return gate.enabled;
}
function canSearchSensitiveRegistry(fastify, request, isAdmin) {
return isAdmin || fastify.hasPermission(request.employee, "system.settings");
}
function canSearchFrozenTravel(fastify, request, isAdmin) {
return isAdmin || fastify.hasPermission(request.employee, "system.settings");
}
async function searchRoutes(fastify) {
fastify.get("/search", { preHandler: fastify.authenticate }, async (request, reply) => {
await ensureFeatureEnabled(fastify, request);
@@ -307,7 +315,7 @@ async function searchRoutes(fastify) {
);
}
if (featureEnabled.projectLinking && (fastify.hasPermission(request.employee, "project.view_all") || fastify.hasPermission(request.employee, "project.manage") || fastify.hasPermission(request.employee, "system.settings") || isAdmin)) {
if (featureEnabled.projectLinking && canSearchSensitiveRegistry(fastify, request, isAdmin)) {
tasks.push(
fastify.prisma.contractRegistry.findMany({
where: {
@@ -348,7 +356,7 @@ async function searchRoutes(fastify) {
);
}
if (featureEnabled.confidentialDocRefs && (fastify.hasPermission(request.employee, "project.view_all") || fastify.hasPermission(request.employee, "project.manage") || fastify.hasPermission(request.employee, "system.settings") || isAdmin)) {
if (featureEnabled.confidentialDocRefs && canSearchSensitiveRegistry(fastify, request, isAdmin)) {
tasks.push(
fastify.prisma.confidentialDocumentRef.findMany({
where: {
@@ -449,7 +457,7 @@ async function searchRoutes(fastify) {
);
}
if (fastify.hasPermission(request.employee, "travel.view_own") || fastify.hasPermission(request.employee, "travel.view_all") || fastify.hasPermission(request.employee, "travel.manage")) {
if (canSearchFrozenTravel(fastify, request, isAdmin)) {
tasks.push(
fastify.prisma.travelRequest.findMany({
where: {
+28 -3
查看文件
@@ -561,6 +561,29 @@ async function buildTaskVisibilityFilters(tx, request) {
return assigneeFilters;
}
function canViewWorkflowEntityType(request, entityType) {
const normalizedEntityType = normalizeWorkflowEntityType(entityType);
if (normalizedEntityType === "travel_request") {
return hasPermission(request.employee.permissionCodes ?? [], "*")
|| hasPermission(request.employee.permissionCodes ?? [], "system.settings");
}
return true;
}
function buildWorkflowEntityVisibilityWhere(request, entityType = null) {
if (entityType) {
return canViewWorkflowEntityType(request, entityType)
? { entity_type: normalizeWorkflowEntityType(entityType) }
: { entity_type: "__forbidden__" };
}
const hiddenEntityTypes = [];
if (!canViewWorkflowEntityType(request, "travel_request")) {
hiddenEntityTypes.push("travel_request");
}
return hiddenEntityTypes.length ? { entity_type: { notIn: hiddenEntityTypes } } : {};
}
function listAllowedActionsForTask(request, task, actionable) {
if (task.status !== "pending" || !actionable) {
return [];
@@ -1957,6 +1980,7 @@ async function workflowRoutes(fastify) {
const where = {
status: "pending",
...(visibilityFilters ? { OR: visibilityFilters } : {}),
workflow_instance: { is: buildWorkflowEntityVisibilityWhere(request) },
};
const tasks = await fastify.prisma.workflowTask.findMany({
@@ -1998,9 +2022,7 @@ async function workflowRoutes(fastify) {
const where = {
status: query.status,
OR: assigneeFilters,
...(query.entityType
? { workflow_instance: { is: { entity_type: query.entityType } } }
: {}),
workflow_instance: { is: buildWorkflowEntityVisibilityWhere(request, query.entityType ?? null) },
};
const [tasks, total] = await Promise.all([
@@ -2151,6 +2173,7 @@ async function workflowRoutes(fastify) {
where: {
id: taskId,
OR: assigneeFilters,
workflow_instance: { is: buildWorkflowEntityVisibilityWhere(request) },
},
select: {
id: true,
@@ -3199,7 +3222,9 @@ module.exports.__testables = {
buildReturnedCopy,
buildWorkflowTaskSummaryPayload,
buildWorkflowEntityMeta,
buildWorkflowEntityVisibilityWhere,
calculateWorkflowTaskDueAt,
canViewWorkflowEntityType,
handlePurchaseApprovalErpSync,
isWorkflowTaskOverdue,
listAllowedActionsForTask,
+14 -9
查看文件
@@ -29,6 +29,7 @@ function buildPerformanceDistribution(rows) {
async function buildEmployeeStats(prisma, options = {}) {
const timeZone = options.timeZone ?? "Asia/Shanghai";
const includeSalarySummary = options.includeSalarySummary ?? true;
const includePerformanceSummary = options.includePerformanceSummary ?? true;
const todayString = currentDateString(timeZone);
const today = parseDateOnlyString(todayString);
const last30DateFrom = addDays(today, -29);
@@ -114,14 +115,18 @@ async function buildEmployeeStats(prisma, options = {}) {
},
},
}),
prisma.performanceReviewCycle.findFirst({
where: { status: "active" },
orderBy: [{ activated_at: "desc" }, { start_date: "desc" }],
}),
prisma.performanceReviewCycle.findFirst({
where: { status: "completed" },
orderBy: [{ completed_at: "desc" }, { end_date: "desc" }],
}),
includePerformanceSummary
? prisma.performanceReviewCycle.findFirst({
where: { status: "active" },
orderBy: [{ activated_at: "desc" }, { start_date: "desc" }],
})
: Promise.resolve(null),
includePerformanceSummary
? prisma.performanceReviewCycle.findFirst({
where: { status: "completed" },
orderBy: [{ completed_at: "desc" }, { end_date: "desc" }],
})
: Promise.resolve(null),
includeSalarySummary
? prisma.payslip.aggregate({
where: {
@@ -155,7 +160,7 @@ async function buildEmployeeStats(prisma, options = {}) {
distribution: [],
};
if (performanceCycle) {
if (includePerformanceSummary && performanceCycle) {
const [reviewAggregate, reviewDistribution] = await Promise.all([
prisma.performanceReview.aggregate({
where: { cycle_id: performanceCycle.id },
+1 -1
查看文件
@@ -46,7 +46,7 @@ const NAVIGATION_REGISTRY = Object.freeze({
workflowTasks: { key: "workflowTasks", label: "审批待办", icon: "fact_check", href: "/workflow/tasks", anyOf: ["expense.approve", "leave.approve", "attendance.approve", "procurement.approve", "travel.approve"] },
dashboard: { key: "dashboard", label: "仪表盘", icon: "dashboard", href: "/dashboard", anyOf: ["report.dashboard.view"] },
purchaseRequests: { key: "purchaseRequests", label: "采购申请", icon: "shopping_cart", href: "/purchase-requests", anyOf: ["procurement.create", "procurement.view_own", "procurement.view_all", "procurement.manage"] },
travelRequests: { key: "travelRequests", label: "差旅申请", icon: "flight", href: "/travel-requests", anyOf: ["travel.view_all", "travel.manage", "travel.payment.manage", "integration.manage"] },
travelRequests: { key: "travelRequests", label: "差旅申请", icon: "flight", href: "/travel-requests", anyOf: ["travel.view_all", "travel.manage", "travel.payment.manage"] },
budgets: { key: "budgets", label: "预算管理", icon: "account_balance", href: "/budgets", anyOf: ["budget.view", "budget.manage"] },
paymentBatches: { key: "paymentBatches", label: "支付批次", icon: "payments", href: "/payment-batches", anyOf: ["payment.batch.manage", "risk.review.manage"], featureFlagKey: FEATURE_FLAG_KEYS.paymentBatch },
apOperations: { key: "apOperations", label: "AP 运营", icon: "account_balance_wallet", href: "/ap-operations", anyOf: ["payment.batch.manage", "risk.review.manage"], featureFlagKey: FEATURE_FLAG_KEYS.paymentBatch },