Files
ln-bi/src/server/auth/login.ts
kkfluous 2575778293 feat: 后端用户认证和权限过滤
- 新增 auth 模块:jumpToken 代理交换、用户信息获取、JWT 签发
- 三级权限:full(所有权限/数智中心/BI-Leader)、department(BI-Leader-Dep)、personal
- 添加 managerId 到车辆数据模型,支持个人级别按 userId 精确过滤
- auth 中间件保护所有 /api/* 端点(跳过 /api/health 和 /api/auth/*)
- 所有路由集成 filterByPermission 权限过滤

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:35:29 +08:00

103 lines
3.3 KiB
TypeScript

import { Hono } from 'hono';
import jwt from 'jsonwebtoken';
import pool from '../db.js';
import type { AuthUser, JwtPayload, PermissionLevel } from './types.js';
import { FULL_ACCESS_ROLES, DEPT_ACCESS_ROLES } from './types.js';
const app = new Hono();
const EXTERNAL_API_BASE = process.env.EXTERNAL_API_BASE || 'https://beta.lnh2e.com';
const JWT_SECRET = process.env.JWT_SECRET || 'ln-bi-default-secret';
/** GET /api/auth/exchange?jumpToken=xxx — 代理 jumpToken 换取 sessionToken */
app.get('/exchange', async (c) => {
const jumpToken = c.req.query('jumpToken');
if (!jumpToken) return c.json({ error: 'Missing jumpToken' }, 400);
try {
const res = await fetch(
`${EXTERNAL_API_BASE}/api/lingniu-manager-v1/v1/auth/issueTokenByJump?jumpToken=${encodeURIComponent(jumpToken)}`
);
const data = await res.json() as { code: number; data: string | null; message: string };
if (data.code !== 0 || !data.data) {
return c.json({ error: 'Token exchange failed', message: data.message }, 401);
}
return c.json({ token: data.data });
} catch (e: unknown) {
console.error('jumpToken exchange error:', e);
return c.json({ error: 'Token exchange failed' }, 500);
}
});
/** POST /api/auth/login — 用外部 sessionToken 获取用户信息,签发 JWT */
app.post('/login', async (c) => {
const body = await c.req.json<{ token: string }>().catch(() => null);
if (!body?.token) return c.json({ error: 'Missing token' }, 400);
try {
// 调用外部 API 获取用户信息
const res = await fetch(
`${EXTERNAL_API_BASE}/api/lingniu-manager-v1/v1/auth/getLoginUserInfo`,
{ headers: { g7litegtoken: body.token } }
);
const data = await res.json() as {
code: number;
data: {
id: string;
userName: string;
loginName: string;
depCode: string;
orgId: string;
roles: { roleName: string; id: string }[];
} | null;
};
if (data.code !== 0 || !data.data) {
return c.json({ error: 'Failed to get user info' }, 401);
}
const userInfo = data.data;
const roleNames = userInfo.roles.map(r => r.roleName);
// 确定权限级别
let permissionLevel: PermissionLevel = 'personal';
if (roleNames.some(r => FULL_ACCESS_ROLES.includes(r))) {
permissionLevel = 'full';
} else if (roleNames.some(r => DEPT_ACCESS_ROLES.includes(r))) {
permissionLevel = 'department';
}
// 查询 depCode 对应的部门名称
let depName = '';
if (userInfo.depCode) {
const [rows] = await pool.execute(
'SELECT dep_name FROM tab_department WHERE dep_code = ? AND is_deleted = 0 LIMIT 1',
[userInfo.depCode]
) as [{ dep_name: string }[], unknown];
depName = rows[0]?.dep_name || '';
}
const payload: JwtPayload = {
userId: userInfo.id,
userName: userInfo.userName,
loginName: userInfo.loginName,
depCode: userInfo.depCode,
depName,
permissionLevel,
};
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: '8h' });
const authUser: AuthUser = { ...payload };
return c.json({ token, user: authUser });
} catch (e: unknown) {
console.error('login error:', e);
return c.json({ error: 'Login failed' }, 500);
}
});
export default app;