diff --git a/.memory/worklog.json b/.memory/worklog.json index 27f422e..c07444c 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -307,6 +307,13 @@ "message": "auto-save 2026-04-19 22:40 (~1)", "hash": "7adecd9", "files_changed": 1 + }, + { + "ts": "2026-04-19T22:45:43+08:00", + "type": "commit", + "message": "auto-save 2026-04-19 22:45 (~1)", + "hash": "81e2710", + "files_changed": 1 } ] } diff --git a/orchestrator/src/admin.ts b/orchestrator/src/admin.ts index a53f06d..721ad79 100644 --- a/orchestrator/src/admin.ts +++ b/orchestrator/src/admin.ts @@ -7,11 +7,53 @@ import { containerName, incus } from './incus.ts'; export const admin = new Hono(); // 鉴权中间件 — token 走 query(首屏 iframe/跳转友好)或 header +// 生成用户范围 token(供 LobeChat 内嵌 iframe 用,只能访问自己的沙箱) +export const scopedToken = async (userId: string): Promise => { + const key = await crypto.subtle.importKey( + 'raw', + new TextEncoder().encode(env.orchSecret), + { hash: 'SHA-256', name: 'HMAC' }, + false, + ['sign'], + ); + const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(`view:${userId}`)); + return Buffer.from(sig).toString('base64url').slice(0, 32); +}; + +const tokenForUser = (token: string, userId: string) => + scopedToken(userId).then((t) => t === token); + admin.use('*', async (c, next) => { - if (!env.adminToken) return c.text('Admin UI disabled: set ADMIN_TOKEN env', 503); const token = c.req.query('token') ?? c.req.header('Admin-Token'); - if (token !== env.adminToken) return c.text('Unauthorized', 401); + if (!token) return c.text('Unauthorized', 401); + + // 1. admin token (全局) + if (env.adminToken && token === env.adminToken) { + await next(); + return; + } + + // 2. scoped token 针对单用户路由,验证 userId 匹配 + const userId = + c.req.param('userId') ?? + c.req.query('userId') ?? + (c.req.path.match(/\/user\/([^\/]+)/)?.[1]); + if (userId && (await tokenForUser(token, userId))) { + c.set('scopedUserId', userId); + await next(); + return; + } + return c.text('Unauthorized', 401); +}); + +// 允许 ai.milejoy.com / lobehub.kang-kang.com iframe 引用 +admin.use('*', async (c, next) => { await next(); + c.header( + 'Content-Security-Policy', + "frame-ancestors 'self' https://ai.milejoy.com https://lobehub.kang-kang.com", + ); + c.header('X-Frame-Options', ''); }); // 全局用户列表 @@ -37,6 +79,13 @@ admin.get('/users', async (c) => { return c.json({ users }); }); +// 给一个 userId 生成 scoped token(admin-token only;LobeChat 服务器端也要实现同样算法) +admin.get('/token/:userId', async (c) => { + const userId = c.req.param('userId'); + const token = await scopedToken(userId); + return c.json({ userId, token, viewUrl: `/admin/user/${userId}?token=${token}` }); +}); + // 单用户 detail admin.get('/user/:userId', async (c) => { const userId = c.req.param('userId');