forked from EduCraft/curriculum-project-hub
8a13e455fb
.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 原先按路径引用 这两份文档,现改为说明出处与取回方式。
158 lines
8.4 KiB
Markdown
158 lines
8.4 KiB
Markdown
# 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 /database`、`GET /database/*`、`GET /app`、`GET /app/*` —— SPA shell /
|
||
客户端路由 fallback(`static.ts` 的 `registerDatabaseSpa`)。
|
||
- `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` 里设:
|
||
|
||
```sh
|
||
HUB_DEV_LOGIN_BYPASS="true"
|
||
```
|
||
|
||
改完重启服务(`npm run dev`,或本地手动 `npx tsx src/server.ts`)。启动日志会
|
||
打印一行 `DEV login bypass enabled: /database/dev-login ...` 作为确认。
|
||
|
||
开启后:
|
||
|
||
- `/database/config` 返回 `devLoginEnabled: true`,SPA 登录页据此显示
|
||
「⚡ 一键登录管理员」按钮
|
||
- 后端注册 `/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.ts` 调 `registerDatabasePlugin()` |
|
||
| `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.ts` 加 `app.get("/database/api/...")`,
|
||
要么新建 `routes/xxxRoutes.ts` 并在 `databaseRoutes.ts` 里 `registerXxxRoutes(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 watch`(`npm run dev`)加载。放在
|
||
`src/` 之外的目录不会被构建,外部识别不到。
|