# 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/`, 回调由 `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/` 之外的目录不会被构建,外部识别不到。