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 全绿。
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import type { Context, Next } from 'hono';
|
||||
import type { AuthUser } from '../auth/types.js';
|
||||
import { authMode, verifyAuthToken } from '../auth/config.js';
|
||||
|
||||
|
||||
// 临时:跳过所有认证(保留完整逻辑便于快速恢复)
|
||||
const BYPASS_AUTH = false;
|
||||
|
||||
export async function authMiddleware(c: Context, next: Next) {
|
||||
const path = c.req.path;
|
||||
|
||||
if (BYPASS_AUTH) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// 本地开发免登录开关:.env 里设 DEV_BYPASS_AUTH=1 启用
|
||||
if (process.env.NODE_ENV !== 'production' && process.env.DEV_BYPASS_AUTH === '1' && authMode() === 'sso') {
|
||||
const devUser: AuthUser = {
|
||||
userId: 'dev-local',
|
||||
userName: '本地开发',
|
||||
loginName: 'dev-local',
|
||||
depCode: '',
|
||||
depName: '',
|
||||
permissionLevel: 'full',
|
||||
roles: ['所有权限', 'BI-SCHEDULE-OPT', 'BI-ADMIN-FEEDBACK', 'BI-LEADER-ENERGY'],
|
||||
};
|
||||
c.set('user', devUser);
|
||||
return next();
|
||||
}
|
||||
|
||||
// 跳过不需要认证的路径
|
||||
if (path === '/api/health' || path.startsWith('/api/auth/')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const authHeader = c.req.header('Authorization');
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return c.json({ error: 'Unauthorized' }, 401);
|
||||
}
|
||||
|
||||
const token = authHeader.slice(7);
|
||||
|
||||
try {
|
||||
const payload = verifyAuthToken(token);
|
||||
if (payload.authMethod === 'password' && !['GET', 'HEAD', 'OPTIONS'].includes(c.req.method)) {
|
||||
return c.json({ error: 'Password account is read-only' }, 403);
|
||||
}
|
||||
const user: AuthUser = {
|
||||
userId: payload.userId,
|
||||
userName: payload.userName,
|
||||
loginName: payload.loginName,
|
||||
depCode: payload.depCode,
|
||||
depName: payload.depName,
|
||||
permissionLevel: payload.permissionLevel,
|
||||
roles: payload.roles ?? [],
|
||||
};
|
||||
c.set('user', user);
|
||||
return next();
|
||||
} catch {
|
||||
return c.json({ error: 'Invalid or expired token' }, 401);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { Hono } from 'hono';
|
||||
import { readOnlyMiddleware } from './read-only.js';
|
||||
|
||||
test('read-only preview blocks write handlers but permits reads; normal mode is unchanged', async () => {
|
||||
const previous = process.env.DB_READ_ONLY;
|
||||
try {
|
||||
let calls = 0;
|
||||
const app = new Hono();
|
||||
app.use('*', readOnlyMiddleware);
|
||||
app.all('*', (c) => { calls++; return c.text('ok'); });
|
||||
process.env.DB_READ_ONLY = '1';
|
||||
for (const method of ['POST', 'PUT', 'PATCH', 'DELETE']) {
|
||||
assert.equal((await app.request('/api/example', { method })).status, 403);
|
||||
}
|
||||
assert.equal(calls, 0);
|
||||
for (const method of ['GET', 'HEAD', 'OPTIONS']) {
|
||||
assert.equal((await app.request('/api/example', { method })).status, 200);
|
||||
}
|
||||
delete process.env.DB_READ_ONLY;
|
||||
assert.equal((await app.request('/api/example', { method: 'POST' })).status, 200);
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.DB_READ_ONLY;
|
||||
else process.env.DB_READ_ONLY = previous;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { MiddlewareHandler } from 'hono';
|
||||
|
||||
/** Guard preview write endpoints before auth/route handlers can perform work. */
|
||||
export const readOnlyMiddleware: MiddlewareHandler = async (context, next) => {
|
||||
if (process.env.DB_READ_ONLY === '1'
|
||||
&& context.req.path !== '/api/auth/password'
|
||||
&& !['GET', 'HEAD', 'OPTIONS'].includes(context.req.method)) {
|
||||
return context.json({ error: '当前为只读预览环境,不允许写入操作' }, 403);
|
||||
}
|
||||
return next();
|
||||
};
|
||||
Reference in New Issue
Block a user