外包风险评估系统:领域引擎+前端+服务端持久化与生产部署

- 确定性领域引擎(分类/评分/分级/红线/费用/裁决)+LLM(通义千问)语言理解
- 6步评估向导、服务端草稿持久化(跨设备/编辑草稿保护)
- 工作流(草稿→风控→管理层)、RBAC、报告导出、校准、客户/费率/红线/最低工资管理
- 专业图标体系替换全部emoji、看板美化
- 生产化:API_BASE可配置(同源反代)、auth密钥惰性读取修复RBAC
- 444单测+204前端测试+51 e2e
This commit is contained in:
freedakgmail
2026-06-13 01:06:39 +08:00
commit c670b9e454
404 changed files with 61820 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
/**
* 认证与权限模块(生产级基础)。
*
* 当前实现:基于 JWT 的无状态认证 + 角色权限校验中间件。
* 密钥取自环境变量 AUTH_SECRET,未配置时降级为无校验(演示模式)。
*/
import type { Context, Next } from 'hono';
import { createHmac } from 'node:crypto';
export type AuthRole = '商务/销售' | '风控' | '管理层';
export interface AuthPayload {
username: string;
role: AuthRole;
iat: number;
exp: number;
}
const SECRET = (): string => process.env.AUTH_SECRET ?? '';
/** 简易 HMAC-SHA256 签名(不依赖外部库,生产建议替换为 jose)。 */
function base64url(buf: Buffer): string {
return buf.toString('base64url');
}
function sign(payload: object): string {
if (SECRET() === '') return ''; // 演示模式不签发
const header = base64url(Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })));
const body = base64url(Buffer.from(JSON.stringify(payload)));
const sig = base64url(createHmac('sha256', SECRET()).update(`${header}.${body}`).digest());
return `${header}.${body}.${sig}`;
}
function verify(token: string): AuthPayload | null {
if (SECRET() === '') return null;
try {
const [header, body, sig] = token.split('.');
if (!header || !body || !sig) return null;
const expected = base64url(createHmac('sha256', SECRET()).update(`${header}.${body}`).digest());
if (sig !== expected) return null;
const payload = JSON.parse(Buffer.from(body, 'base64url').toString()) as AuthPayload;
if (payload.exp < Date.now() / 1000) return null;
return payload;
} catch {
return null;
}
}
/** 签发 JWT(登录成功后调用)。 */
export function issueToken(username: string, role: AuthRole): string {
const now = Math.floor(Date.now() / 1000);
return sign({ username, role, iat: now, exp: now + 86400 });
}
/** 无需鉴权的公共路径(登录与健康检查)。 */
const PUBLIC_PATHS = new Set(['/api/health', '/api/auth/login', '/api/llm/status']);
/**
* Hono 中间件:从 Authorization Bearer token 解析并注入当前用户(供 requireRole 使用)。
*
* 设计:本中间件**只负责识别身份、不负责拦截**——读操作保持开放,敏感写操作由
* {@link requireRole} 按角色拦截。AUTH_SECRET 未配置时为演示模式(不识别身份)。
* 这样开启鉴权后既能强制敏感操作的角色校验,又不破坏只读接口与看板。
*/
export function authMiddleware() {
return async (c: Context, next: Next): Promise<void | Response> => {
if (SECRET() === '' || PUBLIC_PATHS.has(c.req.path)) {
await next();
return;
}
const auth = c.req.header('Authorization');
if (auth !== undefined && auth.startsWith('Bearer ')) {
const payload = verify(auth.slice(7));
if (payload !== null) {
c.set('user', payload);
}
}
await next();
};
}
/** 角色权限校验中间件。 */
export function requireRole(...roles: AuthRole[]) {
return async (c: Context, next: Next): Promise<void | Response> => {
if (SECRET() === '') {
await next();
return;
}
const user = c.get('user') as AuthPayload | undefined;
if (user === undefined || !roles.includes(user.role)) {
return c.json({ error: '权限不足' }, 403);
}
await next();
};
}