后端目录 - db/:mysql / hydrogen / heatmap 连接与氢能只读 SQL 守卫收拢到一处。 - middleware/:auth(JWT → 注入 user)与 read-only 归位。 - 相关测试随文件移动(middleware/read-only.test.ts、db/hydrogen-read-only.test.ts)。 import 改写由一次性 codemod 完成,未改变任何逻辑。 架构守护 - 新增 src/architecture.test.ts,断言 6 条分层铁律: server 不依赖 modules、前端不依赖 server、shared 为叶子层、 无 vendor 引用、model.ts 不依赖 react、@ts-nocheck 仅限已登记的原型快照。 豁免清单只减不增。 测试基建 - npm test 的 glob 同时匹配 .test.ts 与 .test.tsx(此前 .tsx 测试会被静默漏掉)。 文档 - 新增根 README.md:入口、快速开始、命令、目录、部署注意事项 (JWT_SECRET 必须注入且 >=32 字符,否则拒绝启动)。 - 新增 docs/ARCHITECTURE.md:依赖方向、6 条硬规则、业务域形状、 中间件顺序、新增模块步骤,以及"已知未尽事项"的诚实清单。 lint / test(134) / build 全绿。
103 lines
3.2 KiB
TypeScript
103 lines
3.2 KiB
TypeScript
import { Hono } from 'hono';
|
|
import jwt from 'jsonwebtoken';
|
|
import pool from '../db/mysql.js';
|
|
import type { AuthUser, JwtPayload, PermissionLevel } from './types.js';
|
|
import { FULL_ACCESS_ROLES, DEPT_ACCESS_ROLES } from './types.js';
|
|
import { authMode, verifyAuthToken } from './config.js';
|
|
import { passwordRouter } from './password.js';
|
|
import { jwtSecret, serverConfig } from '../config.js';
|
|
|
|
const app = new Hono();
|
|
app.route('/', passwordRouter());
|
|
|
|
/** GET /api/auth/exchange?jumpToken=xxx — 一步完成:换取用户信息 + 签发 JWT */
|
|
app.get('/exchange', async (c) => {
|
|
if (authMode() !== 'sso') return c.json({ message: '当前使用固定密码登录' }, 403);
|
|
const jumpToken = c.req.query('jumpToken');
|
|
if (!jumpToken) return c.json({ error: 'Missing jumpToken' }, 400);
|
|
|
|
try {
|
|
const res = await fetch(
|
|
`${serverConfig.externalApiBase}/api/lingniu-manager-v1/v1/auth/issueTokenByJump?jumpToken=${encodeURIComponent(jumpToken)}`
|
|
);
|
|
const data = await res.json() as {
|
|
code: number;
|
|
data: {
|
|
userInfo: {
|
|
id: string;
|
|
userName: string;
|
|
loginName: string;
|
|
depCode: string;
|
|
orgId: string;
|
|
roles: { roleName: string; id: string }[];
|
|
};
|
|
token: string;
|
|
} | null;
|
|
message: string;
|
|
};
|
|
|
|
if (data.code !== 0 || !data.data?.userInfo) {
|
|
return c.json({ error: 'Token exchange failed', message: data.message }, 401);
|
|
}
|
|
|
|
const userInfo = data.data.userInfo;
|
|
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) {
|
|
try {
|
|
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 || '';
|
|
} catch (e: any) {
|
|
if (e?.code !== 'ER_NO_SUCH_TABLE') throw e;
|
|
}
|
|
}
|
|
|
|
const payload: JwtPayload = {
|
|
userId: userInfo.id,
|
|
userName: userInfo.userName,
|
|
loginName: userInfo.loginName,
|
|
depCode: userInfo.depCode,
|
|
depName,
|
|
permissionLevel,
|
|
roles: roleNames,
|
|
};
|
|
|
|
const token = jwt.sign(payload, jwtSecret(), { expiresIn: '8h' });
|
|
const authUser: AuthUser = { ...payload };
|
|
|
|
return c.json({ token, user: authUser });
|
|
} catch (e: unknown) {
|
|
console.error('auth exchange error:', e);
|
|
return c.json({ error: 'Authentication failed' }, 500);
|
|
}
|
|
});
|
|
|
|
/** GET /api/auth/me — 查看当前用户信息(调试用) */
|
|
app.get('/me', async (c) => {
|
|
const authHeader = c.req.header('Authorization');
|
|
if (!authHeader?.startsWith('Bearer ')) {
|
|
return c.json({ error: 'No token' }, 401);
|
|
}
|
|
try {
|
|
const payload = verifyAuthToken(authHeader.slice(7));
|
|
return c.json(payload);
|
|
} catch {
|
|
return c.json({ error: 'Invalid token' }, 401);
|
|
}
|
|
});
|
|
|
|
export default app;
|