feat: 添加汇率服务和缓存服务,优化数据源和日志服务
refactor: 重构数据源工厂和类型定义,提升代码可维护性 fix: 修复类型转换和状态机文档中的错误 docs: 更新服务架构文档,添加新的服务闭环流程 test: 添加汇率服务单元测试 chore: 清理无用代码和注释,优化代码结构
This commit is contained in:
359
server/src/middleware/HierarchyAuthMiddleware.ts
Normal file
359
server/src/middleware/HierarchyAuthMiddleware.ts
Normal file
@@ -0,0 +1,359 @@
|
||||
/**
|
||||
* [BE-MT002] 层级权限认证中间件
|
||||
* 负责验证用户层级权限、构建数据隔离上下文
|
||||
* AI注意: 所有需要数据隔离的路由必须使用此中间件
|
||||
*/
|
||||
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { logger } from '../utils/logger';
|
||||
import { DataIsolationService, DataIsolationContext, HierarchyLevel } from './DataIsolationService';
|
||||
import { RedisService } from './RedisService';
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
isolationContext?: DataIsolationContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 权限级别
|
||||
export type PermissionLevel = 'TENANT' | 'DEPARTMENT' | 'SHOP' | 'OWN';
|
||||
|
||||
// 中间件配置
|
||||
interface HierarchyAuthConfig {
|
||||
requiredLevel?: PermissionLevel;
|
||||
resourceType?: string;
|
||||
action?: 'read' | 'write' | 'delete';
|
||||
allowAdmin?: boolean;
|
||||
}
|
||||
|
||||
export class HierarchyAuthMiddleware {
|
||||
private static readonly CONTEXT_CACHE_PREFIX = 'hierarchy_context:';
|
||||
private static readonly CONTEXT_CACHE_TTL = 1800;
|
||||
|
||||
/**
|
||||
* [BE-MT002-01] 创建层级认证中间件
|
||||
*/
|
||||
static create(config: HierarchyAuthConfig = {}) {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: '未授权访问' });
|
||||
}
|
||||
|
||||
const context = await this.buildContext(req.user.id);
|
||||
if (!context) {
|
||||
return res.status(403).json({ error: '无法构建权限上下文' });
|
||||
}
|
||||
|
||||
req.isolationContext = context;
|
||||
|
||||
if (config.requiredLevel) {
|
||||
const hasPermission = await this.checkPermissionLevel(
|
||||
context,
|
||||
config.requiredLevel,
|
||||
config.allowAdmin !== false
|
||||
);
|
||||
|
||||
if (!hasPermission) {
|
||||
logger.warn(`[HierarchyAuth] Permission denied for user ${req.user.id}`, {
|
||||
requiredLevel: config.requiredLevel,
|
||||
userLevel: context.role,
|
||||
});
|
||||
return res.status(403).json({ error: '权限不足' });
|
||||
}
|
||||
}
|
||||
|
||||
if (config.resourceType && config.action) {
|
||||
const resourceId = req.params.id || req.body.id;
|
||||
if (resourceId) {
|
||||
const hasAccess = await DataIsolationService.validateDataAccess(
|
||||
config.resourceType,
|
||||
resourceId,
|
||||
context
|
||||
);
|
||||
|
||||
if (!hasAccess) {
|
||||
logger.warn(`[HierarchyAuth] Access denied to resource ${resourceId}`, {
|
||||
userId: req.user.id,
|
||||
resourceType: config.resourceType,
|
||||
});
|
||||
return res.status(403).json({ error: '无权访问此资源' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
logger.error('[HierarchyAuth] Middleware error:', error);
|
||||
return res.status(500).json({ error: '权限验证失败' });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* [BE-MT002-02] 构建数据隔离上下文
|
||||
*/
|
||||
private static async buildContext(userId: string): Promise<DataIsolationContext | null> {
|
||||
const cacheKey = `${this.CONTEXT_CACHE_PREFIX}${userId}`;
|
||||
const cached = await RedisService.get(cacheKey);
|
||||
|
||||
if (cached) {
|
||||
return JSON.parse(cached);
|
||||
}
|
||||
|
||||
const user = await this.getUserHierarchy(userId);
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const permissions = await this.getUserPermissions(userId);
|
||||
|
||||
const context: DataIsolationContext = {
|
||||
tenantId: user.tenant_id,
|
||||
departmentId: user.department_id,
|
||||
shopId: user.shop_id,
|
||||
userId: user.id,
|
||||
role: user.role,
|
||||
permissions,
|
||||
hierarchyPath: user.hierarchy_path || '',
|
||||
};
|
||||
|
||||
await RedisService.set(cacheKey, JSON.stringify(context), this.CONTEXT_CACHE_TTL);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* [BE-MT002-03] 获取用户层级信息
|
||||
*/
|
||||
private static async getUserHierarchy(userId: string): Promise<any> {
|
||||
const user = await require('../config/database').default('cf_user')
|
||||
.where('id', userId)
|
||||
.whereNull('deleted_at')
|
||||
.first(
|
||||
'id',
|
||||
'tenant_id',
|
||||
'department_id',
|
||||
'shop_id',
|
||||
'role',
|
||||
'hierarchy_path'
|
||||
);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* [BE-MT002-04] 获取用户权限列表
|
||||
*/
|
||||
private static async getUserPermissions(userId: string): Promise<string[]> {
|
||||
const permissions = await require('../config/database').default('cf_user_permission')
|
||||
.where('user_id', userId)
|
||||
.pluck('permission_code');
|
||||
|
||||
return permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* [BE-MT002-05] 检查权限级别
|
||||
*/
|
||||
private static async checkPermissionLevel(
|
||||
context: DataIsolationContext,
|
||||
requiredLevel: PermissionLevel,
|
||||
allowAdmin: boolean
|
||||
): Promise<boolean> {
|
||||
if (allowAdmin && context.role === 'ADMIN') {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (requiredLevel) {
|
||||
case 'TENANT':
|
||||
return !!context.tenantId;
|
||||
|
||||
case 'DEPARTMENT':
|
||||
return !!context.departmentId || context.role === 'MANAGER';
|
||||
|
||||
case 'SHOP':
|
||||
return !!context.shopId || context.role === 'MANAGER' || context.role === 'OPERATOR';
|
||||
|
||||
case 'OWN':
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [BE-MT002-06] 验证层级选择器
|
||||
*/
|
||||
static validateHierarchySelector(requiredLevel: PermissionLevel) {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
if (!req.isolationContext) {
|
||||
return res.status(401).json({ error: '未授权访问' });
|
||||
}
|
||||
|
||||
const { tenantId, departmentId, shopId } = req.body;
|
||||
|
||||
if (requiredLevel === 'SHOP' && !shopId) {
|
||||
return res.status(400).json({ error: '必须指定店铺' });
|
||||
}
|
||||
|
||||
if (requiredLevel === 'DEPARTMENT' && !departmentId) {
|
||||
return res.status(400).json({ error: '必须指定部门' });
|
||||
}
|
||||
|
||||
if (tenantId && tenantId !== req.isolationContext.tenantId) {
|
||||
return res.status(403).json({ error: '无权访问此租户数据' });
|
||||
}
|
||||
|
||||
if (departmentId && req.isolationContext.role !== 'ADMIN') {
|
||||
if (req.isolationContext.departmentId !== departmentId) {
|
||||
return res.status(403).json({ error: '无权访问此部门数据' });
|
||||
}
|
||||
}
|
||||
|
||||
if (shopId && req.isolationContext.role !== 'ADMIN') {
|
||||
if (req.isolationContext.shopId && req.isolationContext.shopId !== shopId) {
|
||||
return res.status(403).json({ error: '无权访问此店铺数据' });
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
logger.error('[HierarchyAuth] Selector validation error:', error);
|
||||
return res.status(500).json({ error: '权限验证失败' });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* [BE-MT002-07] 清除用户上下文缓存
|
||||
*/
|
||||
static async clearUserContextCache(userId: string): Promise<void> {
|
||||
await RedisService.delete(`${this.CONTEXT_CACHE_PREFIX}${userId}`);
|
||||
logger.info(`[HierarchyAuth] Cleared context cache for user ${userId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* [BE-MT002-08] 数据隔离查询包装器
|
||||
*/
|
||||
static withIsolation(tableName: string) {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
if (!req.isolationContext) {
|
||||
return res.status(401).json({ error: '未授权访问' });
|
||||
}
|
||||
|
||||
req.isolatedQuery = DataIsolationService.buildIsolationQuery(
|
||||
tableName,
|
||||
req.isolationContext
|
||||
);
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
logger.error('[HierarchyAuth] Isolation wrapper error:', error);
|
||||
return res.status(500).json({ error: '数据隔离失败' });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* [BE-MT002-09] 批量数据访问验证
|
||||
*/
|
||||
static batchAccessCheck(tableName: string, idParam: string = 'ids') {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
if (!req.isolationContext) {
|
||||
return res.status(401).json({ error: '未授权访问' });
|
||||
}
|
||||
|
||||
const ids = req.body[idParam] || req.query[idParam];
|
||||
if (!ids || !Array.isArray(ids)) {
|
||||
return res.status(400).json({ error: '必须提供资源ID列表' });
|
||||
}
|
||||
|
||||
const accessMap = await DataIsolationService.batchValidateAccess(
|
||||
tableName,
|
||||
ids,
|
||||
req.isolationContext
|
||||
);
|
||||
|
||||
const deniedIds = ids.filter(id => !accessMap[id]);
|
||||
if (deniedIds.length > 0) {
|
||||
logger.warn(`[HierarchyAuth] Batch access denied for user ${req.user?.id}`, {
|
||||
deniedIds,
|
||||
tableName,
|
||||
});
|
||||
return res.status(403).json({
|
||||
error: '部分资源无权访问',
|
||||
deniedIds,
|
||||
});
|
||||
}
|
||||
|
||||
req.accessMap = accessMap;
|
||||
next();
|
||||
} catch (error) {
|
||||
logger.error('[HierarchyAuth] Batch access check error:', error);
|
||||
return res.status(500).json({ error: '权限验证失败' });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* [BE-MT002-10] 角色检查中间件
|
||||
*/
|
||||
static requireRole(...roles: string[]) {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.isolationContext) {
|
||||
return res.status(401).json({ error: '未授权访问' });
|
||||
}
|
||||
|
||||
if (!roles.includes(req.isolationContext.role)) {
|
||||
logger.warn(`[HierarchyAuth] Role check failed for user ${req.user?.id}`, {
|
||||
requiredRoles: roles,
|
||||
userRole: req.isolationContext.role,
|
||||
});
|
||||
return res.status(403).json({ error: '角色权限不足' });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* [BE-MT002-11] 权限检查中间件
|
||||
*/
|
||||
static requirePermission(permission: string) {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.isolationContext) {
|
||||
return res.status(401).json({ error: '未授权访问' });
|
||||
}
|
||||
|
||||
if (req.isolationContext.role === 'ADMIN') {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!req.isolationContext.permissions.includes(permission)) {
|
||||
logger.warn(`[HierarchyAuth] Permission check failed for user ${req.user?.id}`, {
|
||||
requiredPermission: permission,
|
||||
userPermissions: req.isolationContext.permissions,
|
||||
});
|
||||
return res.status(403).json({ error: '缺少必要权限' });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
isolatedQuery?: any;
|
||||
accessMap?: Record<string, boolean>;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user