feat(编辑角色): editor 只能写并发布自己的文章

- D1 users 加 role 列(''/editor/admin),与 is_admin 成对写入
- Worker routes/editor.ts 放行 admin+editor,反代注入 X-Editor-Uid/User/Role(昵称 encodeURIComponent)
- editor-api 引入 identify():身份取自注入头或本机会话;文章归属记 frontmatter author_id
- 编辑发布改精确 pathspec(只提交自己文章目录),管理员保持全量;空 pathspec 显式拦截
- 国内机直连登录改为转发 CF /user/access_token 校验,CF 不可达回退本机管理员
- handoff 签名覆盖身份(ts/uid/name/role),防编辑一键跳转变管理员
- admin.js:编辑只渲染「文章编辑」tab、作者框只读;用户管理加角色下拉 + 新建用户
This commit is contained in:
zqlit committed 2026-10-05 14:34:56 +08:00
1 parent 57dbfe6968
commit 39677e7b40
14 files changed
+1172 -139

No files matched your search

+2
View File
@@ -13,6 +13,7 @@ import type {
import { findPagesByKeys, findUsersByIds, siteFirstUrl } from './db';
import { renderMarkdown } from './md';
import { md5Lower } from './md5';
import { roleOf } from './role';
import { formatDateCN } from './util';
// ============================================================================
@@ -126,6 +127,7 @@ export function cookUser(u: UserRow): CookedUser {
badge_color: u.badge_color || '',
title_name: u.title_name || '',
is_admin: !!u.is_admin,
role: roleOf(u),
receive_email: !!u.receive_email,
};
}
+59
View File
@@ -0,0 +1,59 @@
/**
* 后台角色。
*
* 为什么单独一个小模块:`cook.ts`(输出用户结构)、`session.ts`(会话)、
* `routes/editor.ts`(编辑器鉴权)、`routes/admin.ts`(用户管理)都要判角色,
* 谁 import 谁都不合适 —— 放这里谁都能用,也不会绕出循环依赖。
*
* 两处事实源的关系:
* · `users.is_admin` 是历史字段,评论/通知/设置等一大堆既有逻辑都在用它 —— 不动。
* · `users.role` 是这次新增的('' 普通评论用户 | 'editor' 编辑 | 'admin' 管理员),
* 只有它才能表达「编辑」这一级。
* · 两者必须同步:role='admin' ⇔ is_admin=1。写入走 `normalizeRole()`,
* 读取走 `roleOf()`(以 is_admin 优先,兜住没同步过的历史行)。
*/
import type { UserRole } from '../types';
/** 判角色只需要这两个字段,别把整个 UserRow 拖进来 */
export interface RoleCarrier {
is_admin?: number | boolean | null;
role?: string | null;
}
/**
* 归一化读取:只认三种结果。
* ★ is_admin 优先 —— 历史行可能没有 role 列的值(ALTER 之前建的账号),
* 但 is_admin=1 是确定的,不能因为 role 是空串就把老管理员降级。
*/
export function roleOf(u: RoleCarrier | null | undefined): UserRole {
if (!u) return 'user';
if (u.is_admin) return 'admin';
return String(u.role || '') === 'editor' ? 'editor' : 'user';
}
/** 写入方向:把前端传来的一对(role / is_admin)折叠成一个规范值 */
export function normalizeRole(role: unknown, isAdmin: unknown): 'admin' | 'editor' | '' {
const r = String(role ?? '').trim().toLowerCase();
if (r === 'admin' || r === 'editor') return r;
if (r === '' && isAdmin === undefined) return '';
// 没给 role(老前端只给 is_admin)时按 is_admin 推
if (role === undefined || role === null) return isAdmin ? 'admin' : '';
// 给了个不认识的值:不猜,退回 is_admin 的语义
return isAdmin ? 'admin' : '';
}
/** 能进写作后台的角色(管理员 + 编辑) */
export function canWritePosts(u: RoleCarrier | null | undefined): boolean {
const r = roleOf(u);
return r === 'admin' || r === 'editor';
}
/** 编辑角色在文章维度上只能碰自己的(管理员不受限) */
export function isScopedEditor(u: RoleCarrier | null | undefined): boolean {
return roleOf(u) === 'editor';
}
/** 给前端展示的角色名(后台用户管理列表用) */
export function roleLabel(role: UserRole): string {
return role === 'admin' ? '管理员' : role === 'editor' ? '编辑' : '';
}
+2 -1
View File
@@ -1,6 +1,7 @@
import type { Env, SessionUser, UserRow } from '../types';
import { findUserByEmail, findUserById, getAdminUsers } from './db';
import { md5 } from './md5';
import { roleOf } from './role';
import { now } from './util';
// ================================================================= 编解码
@@ -166,7 +167,7 @@ export async function userFromToken(env: Env, authHeader: string | null): Promis
export function sessionOf(user: UserRow | null): SessionUser | null {
if (!user) return null;
return { id: user.id, name: user.name, email: user.email, isAdmin: !!user.is_admin };
return { id: user.id, name: user.name, email: user.email, isAdmin: !!user.is_admin, role: roleOf(user) };
}
/**