Files
curriculum-project-hub/hub/src/database/filelib/guards.ts
T
82241afb56 feat(filelib)!: VersionStore 改为真 git,一项目一仓库
内存 store 换成 gitVersionStore:init 建目录并 git init,VersionId 是
commit hash,某文件的版本取 `git log -1 -- <path>`(D16 文件级版本不因
别的文件提交而失效)。删除也是一个 commit,旧版本仍可读。决策见 ADR-0030。

git 用 execFile 调系统二进制,不引依赖。每次调用钉死 --git-dir/--work-tree
并禁 hooks、隔离全局 gitconfig:项目仓库是老师上传的数据,而 storage root
默认就在本 repo 内,不钉死会让命令落到外层仓库上。

同时:
- 单文件上限改为 HUB_FILELIB_MAX_FILE_BYTES(缺省 10MiB),前端从
  /database/config 读,不再两处硬编码
- commit 身份 name=displayName、email=<userId>@filelib.paradigm-edu.net;
  message 缺省为「【用户名】修改了【路径】」,调用方显式传则优先
- 上传改走弹窗,路径与 commit 信息可手填(原先 prompt 只能填路径)

BREAKING CHANGE: VersionId 由计数器(v1/v2)变为 commit hash;
CommitRequest.author 由字符串变为 { userId, displayName? }。
旧 .version-store.json 不迁移,此前建的项目报 repo_not_found。
2026-07-27 15:55:01 +08:00

50 lines
1.8 KiB
TypeScript

/**
* 文件库 HTTP 门禁(契约 C4 的入驻适配)。
*
* 身份链:hub session(飞书 OAuth / dev bypass)→ silo org membership。
* 网站管理员 = org 的 OWNER/ADMIN(D19:仅 root 创建与 force_adjust 特权,
* 不给内容读旁路);普通成员 = 任何活跃 membership;非成员 = 403。
*/
import type { FastifyReply, FastifyRequest } from "fastify";
import type { OrganizationMemberRole, PrismaClient } from "@prisma/client";
import { requireSession, sendError } from "../../admin/auth/guards.js";
import type { FileLibActor } from "./treeService.js";
export interface FileLibGuardDeps {
readonly prisma: PrismaClient;
readonly sessionSecret: string;
/** 文件库归属的 silo org(ADR-0020/0025)。 */
readonly organizationId: string;
}
const WEBSITE_ADMIN_ROLES: readonly OrganizationMemberRole[] = ["OWNER", "ADMIN"];
/** 每个 /database/api/* 端点第一行调它;返回 null 时响应已发出,fail closed。 */
export async function requireFileLibActor(
request: FastifyRequest,
reply: FastifyReply,
deps: FileLibGuardDeps,
): Promise<FileLibActor | null> {
const auth = await requireSession(request, reply, {
prisma: deps.prisma,
sessionSecret: deps.sessionSecret,
});
if (auth === null) return null;
const membership = await deps.prisma.organizationMembership.findFirst({
where: { organizationId: deps.organizationId, userId: auth.user.id, revokedAt: null },
select: { role: true },
});
if (membership === null) {
await sendError(reply, 403, "forbidden", "not a member of this organization");
return null;
}
return {
userId: auth.user.id,
isWebsiteAdmin: WEBSITE_ADMIN_ROLES.includes(membership.role),
// 仅用于 commit message 的【用户名】;权限判定一律走 userId。
displayName: auth.user.displayName,
};
}