feat(P15A-1,P15A-5): 解耦请假审批权限
这个提交包含在:
@@ -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);
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
在新工单中引用
屏蔽一个用户