Files
curriculum-project-hub/hub/src/database
8a13e455fb chore: 删除 .omo/ 与文件库-接口契约.md
.omo/ 下 12 个 run-continuation/ses_*.json 是 agent 会话续跑状态,
机器生成,本不该进版本库;文件库-开工计划.md 一并删除。

《文件库-接口契约.md》(C/D 编号)同时删除。两份文档的内容都可从 git
历史取回。

代码注释里的 C/D 编号(契约 8.1、C2、C4、D11–D19 等)因此不再有在库
文档可查,分布在 filelib 的 model / grantService / treeService /
guards、prisma schema 与迁移、以及 ADR-0028。README 原先按路径引用
这两份文档,现改为说明出处与取回方式。
2026-07-26 20:32:38 +08:00
..

src/database/

/database/* HTTP 面。代码写在这个目录里,hub.ts 通过 plugin.ts 挂载它, 所以服务器启动时能正确识别这些路由。

前后端分离:页面全部在 SvelteKit 静态 SPA hub/filelib-web/(与 hub/admin-web/ 同一套框架)。老师端 /app 与管理后台 /database 共用这一份工程和这一份构建产物 —— 两个挂载前缀,一个 SPA。本目录的后端只保留三件事:鉴权透传、JSON 数据端点、 以及把构建产物托管出去。服务端不渲染任何 HTML。

后端路由:

  • GET /database/config —— 免鉴权。返回 { orgSlug, devLoginEnabled }, 给 SPA 登录页拼飞书链接、决定是否显示 dev 按钮用。不含任何敏感数据。 (/database/api/login-info 是同形状的既有端点,由 routes/teacherApp.ts 注册。)
  • GET /database/api/stats —— 概览页统计。需登录 是 silo org OWNER/ADMIN。
  • GET /database/dev-login —— 仅开发。见下。
  • GET /databaseGET /database/*GET /appGET /app/* —— SPA shell / 客户端路由 fallbackstatic.tsregisterDatabaseSpa)。
  • GET /_filelib/* —— 构建产物资源。SvelteKit 的 appDir 改名为 _filelib 以避开 admin-web 在根上注册的 /_app/*(同名会让 Fastify 启动即抛重复路由)。

SPA 页面(filelib-web,真 URL 路由、无 hash):

  • /app —— 老师端文件库。未登录显示登录卡片。
  • /database/admin —— 管理员飞书登录页。按钮指向 /auth/feishu/<orgSlug> 回调由 src/admin/routes/authRoutes.ts 处理并种 session cookie。
  • /database/dashboard —— 后台外壳(侧栏 + 权限门)。未登录跳登录页; 登录但非 OWNER/ADMIN 显示无权提示。六个 tab 都是子路由: /database/dashboard(概览)、/library/users/groups/search/settings

注册顺序要点concrete 路由(/database/config/database/api/*/database/dev-login/app/dev-login-teacher)必须在 registerDatabaseSpa/database/*/app/* fallback 之前注册(已在 plugin.ts 保证), 否则通配会 shadow 它们。

开发模式:用环境变量开启一键登录

本地开发没有真实飞书 app 时,可以用环境变量开启一键登录,跳过飞书 OAuth, 直接以现有 OWNER/ADMIN 身份登入后台。仅限开发,不是生产登录路径。

怎么开

hub/.env 里设:

HUB_DEV_LOGIN_BYPASS="true"

改完重启服务(npm run dev,或本地手动 npx tsx src/server.ts)。启动日志会 打印一行 DEV login bypass enabled: /database/dev-login ... 作为确认。

开启后:

  • /database/config 返回 devLoginEnabled: trueSPA 登录页据此显示 「 一键登录管理员」按钮
  • 后端注册 /database/dev-login 端点:按钮就是打它,它签发一个和飞书 OAuth 回调完全一样的 session,然后跳到 /database/dashboard

怎么关

把值设成 false(或 0 / no / off),或删掉这一行。关闭后按钮消失、 /database/dev-login 返回 404 —— 按钮和端点同进同退。

双重门禁(重要)

真正的开关是两个条件的(判断在 plugin.ts):

allowDevLoginBypass = (NODE_ENV !== "production") && HUB_DEV_LOGIN_BYPASS 为真

即:只要 NODE_ENV=production,无论 HUB_DEV_LOGIN_BYPASS 设成什么,一键登录 都强制关闭。 生产始终只能走真实飞书 OAuth。

提醒:HUB_DEV_LOGIN_BYPASS 是敏感开关,别把开着它的 .env 带到任何联网 / 共享环境。整个旁路逻辑自包含在本目录(plugin.ts + routes/databaseRoutes.ts), src/admin 的登录路由未受影响。

文件

文件 职责
plugin.ts 模块对外入口,hub.tsregisterDatabasePlugin()
routes/databaseRoutes.ts /database/config/database/api/stats、dev 旁路 + 各子路由装配点
routes/filelibRoutes.ts 文件库 树/授权 API
routes/fileRoutes.ts 文件库 文件内容/导出 API
routes/memberGroupRoutes.ts 成员组管理 API + /groups/search + /users/search(ADR-0028)
routes/teacherApp.ts /database/api/login-info + 老师端 DEV 一键登录
static.ts filelib-web 构建产物托管:/_filelib/* 资源 + /app/database 两个 SPA 回退
filelib/ 文件库领域层(见下)

新增一类数据端点时:要么直接往 databaseRoutes.tsapp.get("/database/api/...") 要么新建 routes/xxxRoutes.ts 并在 databaseRoutes.tsregisterXxxRoutes(app, {...}) 注册一次。不要在后端拼 HTML —— 页面一律加在 hub/filelib-web/src/routes/ 下。

文件库(filelib/)

独立文件库模块。代码注释里的 C/D 编号(契约 8.1、C2、C4、D11–D19 等) 出自两份已删除的文档:《文件库-接口契约.md》与 .omo/文件库-开工计划.md, 内容可从 git 历史取回。其中 D19(网站管理员 = silo org OWNER/ADMIN) 另见 ADR-0028。与 hub 自己的 Folder/Project(ADR-0021 explorer)是 两套体系,不复用。

文件 职责
filelib/model.ts 角色秩(MANAGE>EDIT>VIEW)、D14 命名规则、FileLibError
filelib/permission.ts 纯权限 reducer(取最高/不降权/祖先继承/D11 冻结),不碰 IO
filelib/treeService.ts 树增删改查;每个写操作同事务落审计
filelib/grantService.ts 授权管理 + 契约 8.1 矩阵强制 + force_adjust
filelib/fileService.ts 文件路径安全 + 版本化读写(先 git 后审计的顺序铁律)
filelib/exportService.ts 导出 job 状态机(D10 异步)+ ExportAdapter port
filelib/versionStore.ts 契约 C1 port + 内存实现(版本团队 npm 包到位后替换)
filelib/groupResolver.ts 契约 C2 port(+ 已弃用的 Team 过渡实现,ADR-0028)
filelib/memberGroupResolver.ts 默认 C2 实现:读 in-hub MemberGroup 闭包(ADR-0028)
filelib/memberGroupService.ts 成员组 CRUD(含改名)+ 成员增删 + 闭包维护 + 搜索(ADR-0028)
filelib/groupResolverHttp.ts C2 HTTP 实现(HUB_GROUP_SERVICE_URL 启用;失败 → 503)
filelib/audit.ts 审计动作词表(C3 §6.3)+ 同事务写入
filelib/guards.ts session → FileLibActor;网站管理员 = org OWNER/ADMIN(D19)
filelib/routeShared.ts 路由共享件(依赖装配/错误映射/请求体校验)

环境变量:

  • HUB_FILELIB_STORAGE_ROOT — 项目 git 仓库根目录(默认 ./.filelib-repos)
  • HUB_GROUP_SERVICE_URL — 外部 Group 服务地址(C2);未配置时读 in-hub MemberGroup 闭包(ADR-0028 起的默认;此前是扁平 hub Team)

⚠️ 开发期注意:当前 VersionStore 是进程内存实现,服务重启后仓库全失, 此前创建的项目再访问文件会报 repo_not_found(需重建项目)。版本团队的 持久化 git 包到位后此问题消失。

关键语义速查:

  • D8:无权限 → 404(不泄露存在性);越权 → 403;Group 服务故障 → 503
  • D11:creator 不可变 + 自动 MANAGE;独立权限关闭时项目级非创建者 grant 冻结
  • D12:move = 本节点 MANAGE + 目标父 EDIT+,事务 + pg 咨询锁
  • D15:删除只打标本节点,"任一祖先已删"即整支不可见
  • 8.1:MANAGE 仅创建者可授/收;creator grant 不可动
  • 审计:一切写操作在业务事务内写 AuditEntry(同事务,失败即回滚); 文件内容写先 versionStore.commit 再审计(宁多版本,不造假审计)

约定(与 admin 面一致)

  1. 路由用绝对路径 "/database/...",不用 Fastify prefix —— 每条路由 grep 得到。
  2. guard 前置、fail closed:凡碰数据的端点第一行先跑 requireSession / requireOrgRole / requireProjectPermission (都在 ../admin/auth/guards.js)。
  3. 租户隔离ADR-0020):每个 Prisma 查询都 scope 到 auth.organization.id, 不得跨 org。禁止无鉴权的数据路由。
  4. 数据库通过传入的 config.prisma 访问(全进程单例,见 ../db.ts); 不要在这里 new PrismaClient()

为什么代码在 src/

tsconfig.json 固定 rootDir: "src"include: ["src/**/*.ts"]。只有 src/ 下的 .ts 会被 tsc 编译、被 tsx watchnpm run dev)加载。放在 src/ 之外的目录不会被构建,外部识别不到。