Files
ln-bi/src/server/auth/login.ts
T
dsh-agent b28ca49491 refactor(stage7): 后端目录分层、架构守护测试与文档
后端目录
- 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 全绿。
2026-09-11 10:20:47 +08:00

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;