feat: 初始化项目结构并添加核心功能模块
- 新增文档模板和导航结构 - 实现服务器基础API路由和控制器 - 添加扩展插件配置和前端框架 - 引入多租户和权限管理模块 - 集成日志和数据库配置 - 添加核心业务模型和类型定义
This commit is contained in:
36
server/src/core/guards/mtls.guard.ts
Normal file
36
server/src/core/guards/mtls.guard.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
/**
|
||||
* [CORE_SEC_07] 全链路 mTLS 强制加密守卫
|
||||
* @description 校验请求是否携带合法的客户端证书。在生产环境下,由 Nginx/ALB 负责终止 mTLS 并通过 Header 传递证书信息。
|
||||
*/
|
||||
export const mtlsGuard = (req: Request, res: Response, next: NextFunction) => {
|
||||
// 1. 获取证书指纹或序列号 (假设由反向代理通过 Header 传递)
|
||||
const certFingerprint = req.header('X-Client-Cert-Fingerprint');
|
||||
const certSubject = req.header('X-Client-Cert-Subject');
|
||||
|
||||
// 2. 如果是开发环境且未配置 mTLS,则跳过 (可选)
|
||||
if (process.env.NODE_ENV === 'development' && !process.env.STRICT_MTLS) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// 3. 校验指纹是否存在
|
||||
if (!certFingerprint) {
|
||||
logger.error(`[mTLS] Request blocked: Missing client certificate fingerprint. IP: ${req.ip}`);
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'mTLS_REQUIRED',
|
||||
message: 'Secure connection via client certificate is required.'
|
||||
});
|
||||
}
|
||||
|
||||
// 4. 将证书信息挂载到请求上下文,供后续业务逻辑使用 (如 NodeIdentityService)
|
||||
(req as any).mtlsContext = {
|
||||
fingerprint: certFingerprint,
|
||||
subject: certSubject
|
||||
};
|
||||
|
||||
logger.info(`[mTLS] Validated client certificate: ${certSubject} (${certFingerprint.substring(0, 8)}...)`);
|
||||
next();
|
||||
};
|
||||
34
server/src/core/guards/rbac.guard.ts
Normal file
34
server/src/core/guards/rbac.guard.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { RBACEngine } from '../auth/RBACEngine';
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
/**
|
||||
* [BIZ_RBAC_01] RBAC 权限校验门禁
|
||||
* @param requiredPermission 所需权限点 (如 'product:write')
|
||||
*/
|
||||
export const requirePermission = (requiredPermission: string) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
const context = (req as any).traceContext;
|
||||
|
||||
if (!context || !context.userId || !context.tenantId) {
|
||||
logger.error(`[RBAC] Missing auth context for request ${req.path}`);
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Unauthorized: Missing security context'
|
||||
});
|
||||
}
|
||||
|
||||
// [V32.0] 使用 RBACEngine.authorize 进行异步权限校验
|
||||
const hasAccess = await RBACEngine.authorize(context.userId, context.tenantId, requiredPermission);
|
||||
|
||||
if (!hasAccess) {
|
||||
logger.warn(`[RBAC] User ${context.userId} denied permission [${requiredPermission}] for tenant ${context.tenantId}`);
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: `Forbidden: Missing required permission [${requiredPermission}]`
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
43
server/src/core/guards/sla.guard.ts
Normal file
43
server/src/core/guards/sla.guard.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import db from '../../config/database';
|
||||
import { SLAGovernanceService } from '../../domains/Billing/SLAGovernanceService';
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
/**
|
||||
* [CORE_TOB_06] SLA 熔断中间件
|
||||
* @description 在请求进入前检查租户状态,请求结束后记录指标
|
||||
*/
|
||||
export async function slaGuard(req: Request, res: Response, next: NextFunction) {
|
||||
// 公开路由白名单
|
||||
const PUBLIC_ROUTES = ['/api/v1/auth/login', '/health'];
|
||||
if (PUBLIC_ROUTES.includes(req.path)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const context = (req as any).traceContext;
|
||||
if (!context || !context.tenantId) return next();
|
||||
|
||||
const tenantId = context.tenantId;
|
||||
|
||||
// 1. 检查租户是否被熔断 (SUSPENDED)
|
||||
const tenant = await db('cf_tenants').where({ id: tenantId }).first();
|
||||
if (tenant && tenant.status === 'SUSPENDED') {
|
||||
logger.warn(`[SLA] Request blocked for suspended tenant: ${tenantId}`);
|
||||
return res.status(429).json({
|
||||
success: false,
|
||||
error: 'Tenant SLA breach: Service temporarily suspended. Please contact support.'
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 拦截响应以记录指标
|
||||
const originalJson = res.json;
|
||||
res.json = function(data: any) {
|
||||
const success = res.statusCode >= 200 && res.statusCode < 300;
|
||||
SLAGovernanceService.recordMetric(tenantId, success).catch(err =>
|
||||
logger.error(`[SLA] Failed to record metric: ${err.message}`)
|
||||
);
|
||||
return originalJson.call(this, data);
|
||||
};
|
||||
|
||||
next();
|
||||
}
|
||||
36
server/src/core/guards/state-transition.guard.ts
Normal file
36
server/src/core/guards/state-transition.guard.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
const PRODUCT_STATUS_TRANSITIONS: Record<string, string[]> = {
|
||||
DRAFTED: ['PENDING_REVIEW'],
|
||||
PENDING_REVIEW: ['APPROVED', 'FAILED_MANUAL'],
|
||||
APPROVED: ['EXECUTING'],
|
||||
EXECUTING: ['PUBLISHED', 'FAILED_RETRYABLE', 'FAILED_MANUAL'],
|
||||
PUBLISHED: ['RECONCILED'],
|
||||
RECONCILED: [],
|
||||
FAILED_RETRYABLE: ['EXECUTING', 'FAILED_MANUAL'],
|
||||
FAILED_MANUAL: ['PENDING_REVIEW'],
|
||||
};
|
||||
|
||||
const SYNC_STATUS_TRANSITIONS: Record<string, string[]> = {
|
||||
pending: ['published', 'failed'],
|
||||
failed: ['pending'],
|
||||
published: ['published'],
|
||||
};
|
||||
|
||||
export function isValidProductStatusTransition(fromStatus: string, toStatus: string) {
|
||||
const normalizedFrom = fromStatus.toUpperCase();
|
||||
const normalizedTo = toStatus.toUpperCase();
|
||||
const allowed = PRODUCT_STATUS_TRANSITIONS[normalizedFrom];
|
||||
if (!allowed) {
|
||||
return false;
|
||||
}
|
||||
return allowed.includes(normalizedTo);
|
||||
}
|
||||
|
||||
export function isValidSyncStatusTransition(fromStatus: string, toStatus: string) {
|
||||
const normalizedFrom = fromStatus.toLowerCase();
|
||||
const normalizedTo = toStatus.toLowerCase();
|
||||
const allowed = SYNC_STATUS_TRANSITIONS[normalizedFrom];
|
||||
if (!allowed) {
|
||||
return false;
|
||||
}
|
||||
return allowed.includes(normalizedTo);
|
||||
}
|
||||
85
server/src/core/guards/trace-context.guard.ts
Normal file
85
server/src/core/guards/trace-context.guard.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { AuthService } from '../../services/AuthService';
|
||||
|
||||
const REQUIRED_HEADERS = [
|
||||
'x-tenant-id',
|
||||
'x-trace-id',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* [CORE_GOV_05] 运行时强门禁: 租户上下文与身份验证
|
||||
* @description 优先从 JWT 提取上下文,否则从 Header 提取 (仅限系统/节点调用)
|
||||
*/
|
||||
export function requireTraceContext(req: Request, res: Response, next: NextFunction) {
|
||||
// 公开路由白名单
|
||||
const PUBLIC_ROUTES = ['/api/v1/auth/login', '/health'];
|
||||
if (PUBLIC_ROUTES.includes(req.path)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
let context: any = null;
|
||||
|
||||
// 1. 尝试从 Authorization: Bearer <Token> 提取 JWT
|
||||
const authHeader = req.headers.authorization;
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
const token = authHeader.substring(7);
|
||||
const payload = AuthService.verifyToken(token);
|
||||
if (payload) {
|
||||
context = {
|
||||
tenantId: payload.tenantId,
|
||||
shopId: payload.shopId,
|
||||
userId: payload.userId,
|
||||
roleCode: payload.role,
|
||||
taskId: String(req.headers['x-task-id'] || ''),
|
||||
traceId: String(req.headers['x-trace-id'] || `trace-${Date.now()}`),
|
||||
source: 'jwt'
|
||||
};
|
||||
} else {
|
||||
return res.status(401).json({ success: false, error: 'Invalid or expired authentication token' });
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 如果没有 JWT,检查 Header 门禁 (适用于 Node Agent / Extension 快速调用)
|
||||
if (!context) {
|
||||
const missing = REQUIRED_HEADERS.filter((header) => {
|
||||
const value = req.headers[header];
|
||||
return typeof value !== 'string' || value.trim().length === 0;
|
||||
});
|
||||
|
||||
if (missing.length > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: `Missing required trace context headers: ${missing.join(', ')}`,
|
||||
});
|
||||
}
|
||||
|
||||
context = {
|
||||
tenantId: String(req.headers['x-tenant-id']),
|
||||
shopId: String(req.headers['x-shop-id'] || ''),
|
||||
taskId: String(req.headers['x-task-id'] || ''),
|
||||
traceId: String(req.headers['x-trace-id']),
|
||||
userId: String(req.headers['x-user-id'] || 'system'),
|
||||
roleCode: String(req.headers['x-role-code'] || 'UNKNOWN'),
|
||||
source: 'header'
|
||||
};
|
||||
}
|
||||
|
||||
// 3. 挂载上下文
|
||||
(req as any).traceContext = context;
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* [CORE_AUTH_01] 仅限管理员或特定角色访问的门禁
|
||||
*/
|
||||
export function authorize(roles: string[]) {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
const context = (req as any).traceContext;
|
||||
if (!context || !roles.includes(context.roleCode)) {
|
||||
return res.status(403).json({ success: false, error: 'Forbidden: Insufficient permissions' });
|
||||
}
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user