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 66c27c7b62
commit d899cd793b
14 files changed
+1172 -139

No files matched your search

+41 -8
View File
@@ -41,27 +41,59 @@ export function makeGit(cfg) {
'-c', 'commit.gpgsign=false',
];
async function status() {
// core.quotepath=false:不把中文文件名转成 \346\265\213 那种八进制转义,
// 否则发布弹层里满屏都是看不懂的编码
const st = await run(repoRoot, ['-c', 'core.quotepath=false', 'status', '--porcelain', '--', ...paths]);
/**
* scope 语义(编辑角色 = 只提交自己的文章):
* undefined → 全量 cfg.paths(content / static),管理员用
* string[] → 只用这些 pathspec(编辑自己的文章目录)
* [] → 编辑名下还没有文章目录:什么都不做(不能退化成全量!)
*
* ★ 空数组必须显式拦掉。`git add -- ` 后面不跟 pathspec 就等于 `git add -A`,
* 会把仓库里所有人未提交的改动一起提交进去 —— 那正是要避免的事。
*/
function resolvePaths(scope) {
if (Array.isArray(scope)) return scope;
return paths;
}
async function status(scope) {
const usePaths = resolvePaths(scope);
const scoped = Array.isArray(scope);
const head = await run(repoRoot, ['log', '-1', '--pretty=%h%x09%ad%x09%s', '--date=format:%Y-%m-%d %H:%M']);
const br = await run(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD']);
const base = {
branch: br.out.trim(),
scoped,
lastCommit: head.out.trim(),
};
if (scoped && !usePaths.length) {
return { ...base, dirty: false, changed: 0, files: [] };
}
// core.quotepath=false:不把中文文件名转成 \346\265\213 那种八进制转义,
// 否则发布弹层里满屏都是看不懂的编码
const st = await run(repoRoot, ['-c', 'core.quotepath=false', 'status', '--porcelain', '--', ...usePaths]);
const files = st.out.split('\n').map((l) => l.trim()).filter(Boolean);
return {
branch: br.out.trim(),
...base,
dirty: files.length > 0,
changed: files.length,
files: files.slice(0, 50),
lastCommit: head.out.trim(),
};
}
async function publish(message) {
async function publish(message, scope) {
return serial(async () => {
const log = [];
const usePaths = resolvePaths(scope);
const scoped = Array.isArray(scope);
const add = await run(repoRoot, ['add', '--', ...paths]);
if (scoped && !usePaths.length) {
return { ok: true, pushed: [], scoped: true, warning: null, log: '(你名下还没有文章目录,没有需要发布的内容)' };
}
const add = await run(repoRoot, ['add', '--', ...usePaths]);
log.push(add.out.trim());
// 先提交(没有暂存内容就跳过)
@@ -100,6 +132,7 @@ export function makeGit(cfg) {
return {
ok: true,
pushed,
scoped,
// 备份远端推失败不算发布失败(主仓成了就算成),但如实说明
warning: failed.length ? '以下远端推送失败(不影响上线): ' + failed.join(' | ') : null,
log: log.join('\n'),