6562208b13
- 新增 users 表(scrypt 口令哈希)与持久化层,启动兜底种子账号 - 登录改为后端用户表校验账号密码;JWT 带角色;保留无DB演示回退 - 新增系统管理员角色 + 用户管理页(增删改/改角色/启停/重置密码) - 用户管理端点按 系统管理员 角色强制校验(RBAC) - 各角色可建任意多个账号(多销售/多风控/多管理) - 更新登录页快速登录与首屏快照
97 lines
3.3 KiB
TypeScript
97 lines
3.3 KiB
TypeScript
/**
|
|
* 认证与权限模块(生产级基础)。
|
|
*
|
|
* 当前实现:基于 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();
|
|
};
|
|
}
|