feat(server): 完成 P0+P1 后端能力(工作流、RBAC、付款归档)
P0 MVP: - 认证:login/refresh/logout/forgot/reset + tokenVersion - RBAC:角色到权限映射 + requirePermission - 发票池:upload/bind/signed-url + 判重(VALID_003/VALID_005) - 报销:create/view/submit + 单号生成 - 工作流:approve + department_manager 解析 P1 扩展: - 动作:reject/return/withdraw(含 comment) - transfer:任务转审 + 链路上限 3 - pay:approved -> paid + 发票归档 used_complete - 动态审批链:approval_rules 驱动 回归脚本: - smoke-test.sh / regression-dedup.sh / regression-workflow-actions.sh
这个提交包含在:
@@ -0,0 +1,105 @@
|
||||
const fastifyJwt = require("@fastify/jwt");
|
||||
const fp = require("fastify-plugin");
|
||||
const { AppError } = require("../utils/AppError");
|
||||
const { resolvePermissionsForRoles, hasPermission } = require("../constants/permissions");
|
||||
|
||||
async function authPlugin(fastify) {
|
||||
const { jwtSecret, accessTokenTtl } = fastify.config;
|
||||
|
||||
await fastify.register(fastifyJwt, {
|
||||
secret: jwtSecret,
|
||||
});
|
||||
|
||||
fastify.decorateRequest("employee", null);
|
||||
|
||||
fastify.decorate("issueAccessToken", (employee) => {
|
||||
return fastify.jwt.sign(
|
||||
{
|
||||
sub: employee.id,
|
||||
tokenVersion: employee.token_version,
|
||||
},
|
||||
{ expiresIn: accessTokenTtl },
|
||||
);
|
||||
});
|
||||
|
||||
fastify.decorate("hasPermission", (employee, permissionCode) => {
|
||||
return hasPermission(employee?.permissionCodes ?? [], permissionCode);
|
||||
});
|
||||
|
||||
fastify.decorate("requirePermission", (permissionCode) => {
|
||||
return async (request) => {
|
||||
await fastify.authenticate(request);
|
||||
|
||||
if (!fastify.hasPermission(request.employee, permissionCode)) {
|
||||
throw new AppError({
|
||||
code: "PERM_001",
|
||||
message: "无权限",
|
||||
statusCode: 403,
|
||||
details: { permission: permissionCode },
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
fastify.decorate("authenticate", async (request) => {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
} catch {
|
||||
throw new AppError({
|
||||
code: "AUTH_004",
|
||||
message: "Access Token 已过期/无效",
|
||||
statusCode: 401,
|
||||
});
|
||||
}
|
||||
|
||||
const employeeId = request.user?.sub;
|
||||
const tokenVersion = request.user?.tokenVersion;
|
||||
if (typeof employeeId !== "string" || typeof tokenVersion !== "number") {
|
||||
throw new AppError({
|
||||
code: "AUTH_004",
|
||||
message: "Access Token 已过期/无效",
|
||||
statusCode: 401,
|
||||
});
|
||||
}
|
||||
|
||||
const employee = await fastify.prisma.employee.findUnique({
|
||||
where: { id: employeeId },
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
token_version: true,
|
||||
department_id: true,
|
||||
roles: { select: { role_code: true } },
|
||||
},
|
||||
});
|
||||
if (!employee || employee.status !== "active") {
|
||||
throw new AppError({
|
||||
code: "AUTH_003",
|
||||
message: "账号已离职/禁用",
|
||||
statusCode: 403,
|
||||
});
|
||||
}
|
||||
|
||||
if (tokenVersion < employee.token_version) {
|
||||
throw new AppError({
|
||||
code: "AUTH_004",
|
||||
message: "Access Token 已过期/无效",
|
||||
statusCode: 401,
|
||||
});
|
||||
}
|
||||
|
||||
const roleCodes = employee.roles.map((item) => item.role_code);
|
||||
const permissionCodes = resolvePermissionsForRoles(roleCodes);
|
||||
|
||||
request.employee = {
|
||||
id: employee.id,
|
||||
status: employee.status,
|
||||
token_version: employee.token_version,
|
||||
department_id: employee.department_id,
|
||||
roleCodes,
|
||||
permissionCodes,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = fp(authPlugin, { name: "auth" });
|
||||
@@ -0,0 +1,16 @@
|
||||
const { createPrismaClient } = require("../prismaClient");
|
||||
const fp = require("fastify-plugin");
|
||||
|
||||
async function prismaPlugin(fastify) {
|
||||
const { prisma, pool } = createPrismaClient();
|
||||
await prisma.$connect();
|
||||
|
||||
fastify.decorate("prisma", prisma);
|
||||
|
||||
fastify.addHook("onClose", async (instance) => {
|
||||
await instance.prisma.$disconnect();
|
||||
await pool.end();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = fp(prismaPlugin, { name: "prisma" });
|
||||
在新工单中引用
屏蔽一个用户