auto-save 2026-04-19 22:51 (~2)
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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<string> => {
|
||||
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');
|
||||
|
||||
Reference in New Issue
Block a user