diff --git a/blog-admin/src/routes/editor.ts b/blog-admin/src/routes/editor.ts index 9d00da4f..f61d0e4a 100644 --- a/blog-admin/src/routes/editor.ts +++ b/blog-admin/src/routes/editor.ts @@ -12,10 +12,35 @@ */ import type { Ctx } from '../router'; import { fail, json } from '../lib/util'; -import { isAdminRequest } from '../lib/session'; +import { getAdminUsers } from '../lib/db'; +import { userFromToken } from '../lib/session'; + +/** + * ★ 鉴权只认「真实登录会话(Bearer token)」,**绝不能**用 isAdminRequest。 + * + * isAdminRequest 里有一条 Artalk 为老客户端留的兜底:请求带 + * `?name=<管理员名>&email=<管理员邮箱>` 就视为管理员。而 ADMIN_NAME / + * ADMIN_EMAIL 是写死在 wrangler.toml、并暴露在后台页面里的公开信息—— + * 一旦这条兜底被用在编辑器路由上,等于把「读全部文章源码 + 改文件 + + * 删文章 + 传图 + 触发 git 发布(真推到 CNB/GitHub)」的能力裸露在公网, + * 任何人拼一个 query 就能删你的博客。这里必须硬性要求会话。 + * + * 已登录用户的判定与 isAdminRequest 一致(is_admin 或邮箱命中配置管理员), + * 只是砍掉了「匿名 + query 参数」那一条。 + */ +async function requireAdminSession(ctx: Ctx) { + const user = ctx.user ?? (await userFromToken(ctx.env, ctx.req.headers.get('Authorization'))); + if (!user) return null; + if (user.is_admin) return user; + // 已登录但没打 is_admin 标的老账号:邮箱命中配置里的管理员也算 + const admins = await getAdminUsers(ctx.env); + return admins.some((a) => a.email && a.email.toLowerCase() === String(user.email || '').toLowerCase()) + ? user + : null; +} async function guard(ctx: Ctx): Promise { - if (!(await isAdminRequest(ctx.env, ctx.req, ctx.user))) { + if (!(await requireAdminSession(ctx))) { return fail(403, '需要管理员权限'); } if (!ctx.env.EDITOR_API_BASE) { @@ -136,9 +161,9 @@ export const publish = (ctx: Ctx) => relayJson(ctx, '/git/publish', 'POST'); export const sync = (ctx: Ctx) => relay(ctx, '/git/sync', { method: 'POST' }); -/** 健康检查:让后台能显示「编辑后端是否在线」 */ +/** 健康检查:让后台能显示「编辑后端是否在线」(同样只认真实会话) */ export async function health(ctx: Ctx): Promise { - if (!(await isAdminRequest(ctx.env, ctx.req, ctx.user))) return fail(403, '需要管理员权限'); + if (!(await requireAdminSession(ctx))) return fail(403, '需要管理员权限'); if (!ctx.env.EDITOR_API_BASE) return json({ ok: false, reason: '缺少 EDITOR_API_BASE' }); try { const res = await fetch(target(ctx, '/health'), { method: 'GET' });