This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-TMP-REPO/apps/api/src/app/routes/workbench.ts
T

65 lines
2.6 KiB
TypeScript

/**
* 工作台路由组装 —— 路由契约在 @contracts/api,实现在此(编码规范 §3 / §9.2)。
* 登录 / 登出属鉴权面:中间件在 app 层,身份判定读 `org` 出口(§4 / ADR-010)。
*/
import type { Hono } from 'hono';
import type { ApiError } from '@contracts';
import { CONTRACTS_VERSION, healthRoute, loginRoute, logoutRoute, sessionRoute } from '@contracts';
import type { Container } from '../container.ts';
import { SESSION_COOKIE, clearSessionCookie, setSessionCookie } from '../session-cookie.ts';
import type { AppEnv } from '../http-env.ts';
export function registerWorkbenchRoutes(app: Hono<AppEnv>, container: Container): void {
app.get(healthRoute.path, async (c) => {
let db: 'up' | 'down' = 'down';
try {
await container.pool.query('select 1');
db = 'up';
} catch {
db = 'down';
}
return c.json({ ok: db === 'up', version: CONTRACTS_VERSION, db });
});
/** 我是谁 —— 会话由中间件解好放进上下文,这里只回投影 */
app.get(sessionRoute.path, (c) => {
const session = c.get('session');
if (session === undefined) {
const body: ApiError = { code: 'unauthenticated', message: '尚未登录' };
return c.json(body, 401);
}
return c.json(session);
});
app.post(loginRoute.path, async (c) => {
const parsed = loginRoute.body.safeParse(await c.req.json().catch(() => ({})));
if (!parsed.success) {
const body: ApiError = { code: 'validation-failed', message: '请填写账号与口令' };
return c.json(body, 400);
}
const issued = await container.org.login(parsed.data.account, parsed.data.password);
// 失败只有一句话:不区分账号不存在 / 已停用 / 口令错,不外泄账号是否存在
if (issued === undefined) {
const body: ApiError = { code: 'unauthenticated', message: '账号或口令不对' };
return c.json(body, 401);
}
const session = await container.org.resolveSession(issued.token);
if (session === undefined) {
const body: ApiError = { code: 'internal', message: '会话签发后立即失效' };
return c.json(body, 500);
}
setSessionCookie(c, issued.token, new Date(issued.expiresAt));
return c.json(session);
});
app.post(logoutRoute.path, async (c) => {
const token = c.req.header('cookie')?.match(new RegExp(`(?:^|;\\s*)${SESSION_COOKIE}=([^;]+)`))?.[1];
// 服务端表先作废,再清 cookie —— 顺序反了会留下「cookie 没了但会话还活着」的窗口
if (token !== undefined) await container.org.logout(token);
clearSessionCookie(c);
return c.json({ ok: true });
});
}