Archived
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
65 lines
2.6 KiB
TypeScript
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 });
|
|
});
|
|
}
|