feat(P15A-1,P15A-5): 解耦请假审批权限

这个提交包含在:
2026-03-07 07:15:33 +08:00
父节点 637fd24d8e
当前提交 03c5a78485
共修改 10 个文件,包含 230 行新增和 46 行删除
+63 -10
查看文件
@@ -1,4 +1,4 @@
const EMPLOYEE_BASE_PERMISSIONS = [
const EXPENSE_SELF_SERVICE_PERMISSIONS = [
"expense.create",
"expense.view_own",
"expense.edit_own",
@@ -10,6 +10,50 @@ const EMPLOYEE_BASE_PERMISSIONS = [
"expense.proxy_manage",
];
const LEAVE_SELF_SERVICE_PERMISSIONS = [
"leave.create",
"leave.view_own",
"leave.submit",
];
const EMPLOYEE_BASE_PERMISSIONS = [
...EXPENSE_SELF_SERVICE_PERMISSIONS,
...LEAVE_SELF_SERVICE_PERMISSIONS,
];
const ENTITY_PERMISSION_MAP = {
reimbursement: {
create: "expense.create",
view_own: "expense.view_own",
view_all: "expense.view_all",
edit_own: "expense.edit_own",
delete_own: "expense.delete_own",
submit: "expense.submit",
withdraw: "expense.withdraw",
approve: "expense.approve",
transfer: "expense.transfer",
},
expense: {
create: "expense.create",
view_own: "expense.view_own",
view_all: "expense.view_all",
edit_own: "expense.edit_own",
delete_own: "expense.delete_own",
submit: "expense.submit",
withdraw: "expense.withdraw",
approve: "expense.approve",
transfer: "expense.transfer",
},
leave: {
create: "leave.create",
view_own: "leave.view_own",
view_all: "leave.view_all",
submit: "leave.submit",
approve: "leave.approve",
manage: "leave.manage",
},
};
const ROLE_PERMISSIONS = {
employee: EMPLOYEE_BASE_PERMISSIONS,
manager: [
@@ -17,6 +61,7 @@ const ROLE_PERMISSIONS = {
"expense.approve",
"expense.transfer",
"expense.view_all",
"leave.approve",
"report.dashboard.view",
"budget.view",
"budget.manage",
@@ -26,6 +71,8 @@ const ROLE_PERMISSIONS = {
"expense.approve",
"expense.transfer",
"expense.view_all",
"leave.approve",
"leave.view_all",
"report.dashboard.view",
"budget.view",
],
@@ -57,16 +104,17 @@ const ROLE_PERMISSIONS = {
],
administration: [
"expense.proxy_submit",
"expense.create",
"expense.view_own",
"expense.edit_own",
"expense.delete_own",
"expense.submit",
"expense.withdraw",
"invoice.upload",
"invoice.view_own",
...EMPLOYEE_BASE_PERMISSIONS,
],
hr: [
...LEAVE_SELF_SERVICE_PERMISSIONS,
"leave.view_all",
"leave.manage",
"system.users",
"system.users_sensitive",
"expense.view_all",
"report.dashboard.view",
],
hr: ["system.users", "system.users_sensitive", "expense.view_all", "report.dashboard.view"],
admin: ["*"],
};
@@ -90,8 +138,13 @@ function hasPermission(permissionCodes, permission) {
return permissionCodes.includes("*") || permissionCodes.includes(permission);
}
function resolveEntityPermission(entityType, action) {
return ENTITY_PERMISSION_MAP[entityType]?.[action] ?? null;
}
module.exports = {
ROLE_PERMISSIONS,
resolvePermissionsForRoles,
hasPermission,
resolveEntityPermission,
};
+39 -9
查看文件
@@ -1,7 +1,11 @@
const fastifyJwt = require("@fastify/jwt");
const fp = require("fastify-plugin");
const { AppError } = require("../utils/AppError");
const { resolvePermissionsForRoles, hasPermission } = require("../constants/permissions");
const {
resolvePermissionsForRoles,
hasPermission,
resolveEntityPermission,
} = require("../constants/permissions");
async function authPlugin(fastify) {
const { jwtSecret, accessTokenTtl } = fastify.config;
@@ -26,18 +30,44 @@ async function authPlugin(fastify) {
return hasPermission(employee?.permissionCodes ?? [], permissionCode);
});
fastify.decorate("assertPermission", (employee, permissionCode, details = {}) => {
if (!permissionCode || !fastify.hasPermission(employee, permissionCode)) {
throw new AppError({
code: "PERM_001",
message: "无权限",
statusCode: 403,
details: { permission: permissionCode, ...details },
});
}
return permissionCode;
});
fastify.decorate("assertEntityPermission", (employee, entityType, action) => {
const permissionCode = resolveEntityPermission(entityType, action);
if (!permissionCode) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 409,
details: { entityType, action, permission: "unsupported_entity_action" },
});
}
return fastify.assertPermission(employee, permissionCode, { entityType, action });
});
fastify.decorate("requirePermission", (permissionCode) => {
return async (request) => {
await fastify.authenticate(request);
fastify.assertPermission(request.employee, permissionCode);
};
});
if (!fastify.hasPermission(request.employee, permissionCode)) {
throw new AppError({
code: "PERM_001",
message: "无权限",
statusCode: 403,
details: { permission: permissionCode },
});
}
fastify.decorate("requireEntityPermission", (entityType, action) => {
return async (request) => {
await fastify.authenticate(request);
fastify.assertEntityPermission(request.employee, entityType, action);
};
});
+6 -3
查看文件
@@ -95,7 +95,8 @@ async function leaveRoutes(fastify) {
fastify.post(
"/leave-requests",
{
preHandler: fastify.authenticate,
schema: { tags: ["Leave"], summary: "Create Leave Request" },
preHandler: fastify.requireEntityPermission("leave", "create"),
},
async (request, reply) => {
const body = parseCreateBody(request.body);
@@ -131,6 +132,7 @@ async function leaveRoutes(fastify) {
fastify.get(
"/leave-requests/:id",
{
schema: { tags: ["Leave"], summary: "Get Leave Request" },
preHandler: fastify.authenticate,
},
async (request, reply) => {
@@ -159,7 +161,7 @@ async function leaveRoutes(fastify) {
});
}
const canViewAll = fastify.hasPermission(request.employee, "expense.view_all");
const canViewAll = fastify.hasPermission(request.employee, "leave.view_all");
if (!canViewAll && leave.owner_id !== request.employee.id) {
throw new AppError({
code: "PERM_001",
@@ -185,7 +187,8 @@ async function leaveRoutes(fastify) {
fastify.post(
"/leave-requests/:id/submit",
{
preHandler: fastify.authenticate,
schema: { tags: ["Leave"], summary: "Submit Leave Request" },
preHandler: fastify.requireEntityPermission("leave", "submit"),
},
async (request, reply) => {
const leaveId = parseLeaveId(request.params?.id);
+13 -5
查看文件
@@ -1,7 +1,7 @@
const { z } = require("zod");
const { AppError } = require("../utils/AppError");
const { sendData } = require("../utils/reply");
const { resolvePermissionsForRoles, hasPermission, ROLE_PERMISSIONS } = require("../constants/permissions");
const { resolvePermissionsForRoles, hasPermission, ROLE_PERMISSIONS, resolveEntityPermission } = require("../constants/permissions");
const { getWorkflowStepsSnapshot, resolveActionableStep, toMoneyString } = require("../utils/workflow");
const {
createNotifications,
@@ -199,7 +199,8 @@ function listAllowedActionsForTask(request, task, actionable) {
}
const actions = [];
if (hasPermission(request.employee.permissionCodes, "expense.approve")) {
const approvePermission = resolveEntityPermission(task.workflow_instance?.entity_type, "approve");
if (approvePermission && hasPermission(request.employee.permissionCodes, approvePermission)) {
actions.push("approve", "reject", "return");
}
@@ -731,7 +732,7 @@ async function workflowRoutes(fastify) {
fastify.post(
"/workflow/tasks/:taskId/approve",
{
preHandler: fastify.requirePermission("expense.approve"),
preHandler: fastify.authenticate,
},
async (request, reply) => {
const taskId = parseTaskId(request.params?.taskId);
@@ -744,6 +745,7 @@ async function workflowRoutes(fastify) {
const assignment = await ensureTaskAssignableToCurrentEmployee(tx, task, request);
const workflowInstance = await loadWorkflowInstanceForTask(tx, task.instance_id);
fastify.assertEntityPermission(request.employee, workflowInstance.entity_type, "approve");
const steps = getWorkflowStepsSnapshot(workflowInstance);
const currentStepIndex = steps.findIndex((step) => step.id === workflowInstance.current_step_id);
if (currentStepIndex < 0) {
@@ -966,7 +968,7 @@ async function workflowRoutes(fastify) {
fastify.post(
"/workflow/tasks/:taskId/reject",
{
preHandler: fastify.requirePermission("expense.approve"),
preHandler: fastify.authenticate,
},
async (request, reply) => {
const taskId = parseTaskId(request.params?.taskId);
@@ -980,6 +982,7 @@ async function workflowRoutes(fastify) {
const assignment = await ensureTaskAssignableToCurrentEmployee(tx, task, request);
const workflowInstance = await loadWorkflowInstanceForTask(tx, task.instance_id);
fastify.assertEntityPermission(request.employee, workflowInstance.entity_type, "approve");
const now = new Date();
await tx.workflowTask.update({
@@ -1095,7 +1098,7 @@ async function workflowRoutes(fastify) {
fastify.post(
"/workflow/tasks/:taskId/return",
{
preHandler: fastify.requirePermission("expense.approve"),
preHandler: fastify.authenticate,
},
async (request, reply) => {
const taskId = parseTaskId(request.params?.taskId);
@@ -1109,6 +1112,7 @@ async function workflowRoutes(fastify) {
const assignment = await ensureTaskAssignableToCurrentEmployee(tx, task, request);
const workflowInstance = await loadWorkflowInstanceForTask(tx, task.instance_id);
fastify.assertEntityPermission(request.employee, workflowInstance.entity_type, "approve");
const now = new Date();
await tx.workflowTask.update({
@@ -1353,3 +1357,7 @@ async function workflowRoutes(fastify) {
}
module.exports = workflowRoutes;
module.exports.__testables = {
listAllowedActionsForTask,
};