Commit b7b50961f051a486cfc31c98f95810d9aaa4f863
1 parent
6f48c194
feat(auth): Phase 1 —— 全站要求登录(38 号文档)
给此前完全零防护的 company.js/okr.js/tasks.js/devloop.js/graph-run.js(两个挂载点)/ arena.js/config.js/evolution.js/whatsapp-auth.js 挂 requireAuth;dream.js 挂 requireAdmin (系统级经验汇流视图,只有 admin 能看)。open-api.js(自己一套 OPEN_API_KEYS)/auth.js 自身/health 端点按计划排除。这一步只挡"没登录",公司归属过滤留到 Phase 2。 agent-chat.js 不能直接挂 requireAuth:company.js(自动建公司/招聘后自动训练)、 task-settle.js(goal 汇报回调)三处服务端自环调用会 fetch 自己的 /api/agent/* 端点, 拿不到任何用户 cookie,直接挂锁会打断这几条核心自动化链路。新增 requireAuthOrInternal + jwt-auth.js 的进程内 INTERNAL_TOKEN(启动时随机生成,只活在内存里):自环调用带上 x-internal-token 头放行,其余情况仍要求真实登录;三处调用点同步改造。 17 个既有测试文件、67+ 处用例直接拿 supertest 打这些路由,从没设过登录 cookie(测的是 业务逻辑不是鉴权)。没有逐个补登录态,改为 requireAuth/requireAdmin 在 NODE_ENV=test 时默认放行,除非显式 FORCE_REAL_AUTH=1;新增 route-auth-gate.test.js 在放行关闭状态下 做结构检查(router.stack 里中间件是否真的排在所有路由前面),确保"锁真的挂上了"这件事 有测试兜底,不被业务测试的默认放行悄悄掩盖。 验证:真实 curl 走过全部路由(无 cookie 401、admin 登录全通、非 admin 能看公司但 dream.js 403);手工删过一行 requireAuth 确认守卫测试真的会红;全量 vitest 1427 passed, 另 3 个失败与本次改动无关(stash 验证过改前改后一致,系仓库并发工作遗留)。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Showing
16 changed files
with
227 additions
and
8 deletions
docs/README.md
| @@ -49,7 +49,7 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d | @@ -49,7 +49,7 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d | ||
| 49 | | [29-dream-design.md](./todo/system-design/29-dream-design.md) | **dream(经验汇流)**— ✅ 已实现。定时整理所有 agent 在所有公司的记忆,提炼后跨公司沉淀(不泄漏公司信息)。四条取舍已拍板并实现:去敏独立判定、四落点分层写入(经验/`SKILL.md`/`soul.md`/`agent.md`,受管区块+三道闸门)、每日定时(串行+空转保护)、引用真伪自动查、REST+控制台入口(`/api/dream/*`、`/dream`)。`innovator-evolver` 真 CLI 跑通一次。**仍缺的**:「结论对不对」只能人抽查;「套进 org/work graph 来跑」评估后用户选了不做(作用域不匹配,见文内 §5.7) | | 49 | | [29-dream-design.md](./todo/system-design/29-dream-design.md) | **dream(经验汇流)**— ✅ 已实现。定时整理所有 agent 在所有公司的记忆,提炼后跨公司沉淀(不泄漏公司信息)。四条取舍已拍板并实现:去敏独立判定、四落点分层写入(经验/`SKILL.md`/`soul.md`/`agent.md`,受管区块+三道闸门)、每日定时(串行+空转保护)、引用真伪自动查、REST+控制台入口(`/api/dream/*`、`/dream`)。`innovator-evolver` 真 CLI 跑通一次。**仍缺的**:「结论对不对」只能人抽查;「套进 org/work graph 来跑」评估后用户选了不做(作用域不匹配,见文内 §5.7) | |
| 50 | | [30-task-delegation-reliability-gaps.md](./todo/system-design/30-task-delegation-reliability-gaps.md) | **任务委派可靠性现状**— 📝 调研记录,非设计方案。DevLoop `runUnit`、Company 招聘装配、Graph 执行三条链路核对结论一致:都是「失败预算耗尽 → 停等人工」,没有「卡住了主动通知人」这一层;DevLoop 当前还默认走 mock,真实 CLI 成功率无实证数据。2026-08-23 追加真实实例并已修:`infra_flake` 原地重跑没有熔断线,真撞上过一次确定性死循环(节点把诊断结论误判成自己的执行结果,反复重跑却从不路由到诊断建议的目标节点);已加 `infra_flake_count` 列 + 硬顶 3 次熔断,连续判 infra_flake 超过硬顶就改判 blocked 交给人工 | | 50 | | [30-task-delegation-reliability-gaps.md](./todo/system-design/30-task-delegation-reliability-gaps.md) | **任务委派可靠性现状**— 📝 调研记录,非设计方案。DevLoop `runUnit`、Company 招聘装配、Graph 执行三条链路核对结论一致:都是「失败预算耗尽 → 停等人工」,没有「卡住了主动通知人」这一层;DevLoop 当前还默认走 mock,真实 CLI 成功率无实证数据。2026-08-23 追加真实实例并已修:`infra_flake` 原地重跑没有熔断线,真撞上过一次确定性死循环(节点把诊断结论误判成自己的执行结果,反复重跑却从不路由到诊断建议的目标节点);已加 `infra_flake_count` 列 + 硬顶 3 次熔断,连续判 infra_flake 超过硬顶就改判 blocked 交给人工 | |
| 51 | | [31-work-graph-fail-fallback-coverage.md](./todo/system-design/31-work-graph-fail-fallback-coverage.md) | work graph 兜底边覆盖不全 — ✅ 已完成(除入口节点外强制兜底边,`planner.json`/`executor-fanout.json` 补 6 条边),978/978 单测通过;2026-08-21 又用真实 LLM 起了一条新任务做端到端回归验证,`org-planner-sdd` 自判失败后自动打回 `org-planner-prd`(3 次,零人工介入),修复确认生效 | | 51 | | [31-work-graph-fail-fallback-coverage.md](./todo/system-design/31-work-graph-fail-fallback-coverage.md) | work graph 兜底边覆盖不全 — ✅ 已完成(除入口节点外强制兜底边,`planner.json`/`executor-fanout.json` 补 6 条边),978/978 单测通过;2026-08-21 又用真实 LLM 起了一条新任务做端到端回归验证,`org-planner-sdd` 自判失败后自动打回 `org-planner-prd`(3 次,零人工介入),修复确认生效 | |
| 52 | -| [38-auth-multi-tenant-isolation.md](./todo/system-design/38-auth-multi-tenant-isolation.md) | 📝 **Phase 0 已完成(2026-08-25),Phase 1-3 未开工**。远程部署前置需求:登录鉴权 + 公司级多租户隔离。现状 `auth.js`/`jwt-auth.js`(注册/登录/JWT-cookie/OAuth)+ WebSocket 鉴权都已可用,真正缺的是①把已有的锁挂到目前零防护的 `company.js`/`agent-chat.js`/`okr.js`/`tasks.js`/`devloop.js`/`graph-run.js`(含双挂载点)等路由上、②`company.json` 从 0 新增 `ownerId` 归属概念(无 SQL 表,纯文件)。已拍板:存量公司迁移统一划给种子账号 `admin`/`hex1234`(不强制改密);OKR 跟随 company 隔离(含独立的遗留 `/api/okr` 挂载点);`arena.js`/`evolution.js` 核实为疑似废弃代码(前端零引用、5 个月未改动),只挂登录不投入隔离;`dream.js` 系统级,挂 `requireAdmin`。**Phase 0**:种子脚本 + 登录页 + 路由守卫 + 401 拦截器 + 退出登录,真实浏览器走通(含顺手修复的 `MasterChatDrawer` 登录页泄漏)。Phase 1(全站要求登录)→ Phase 2(`router.param('companyId')` 归属校验,最大工作量)→ Phase 3(聊天隔离验证+前端收尾)待开工 | | 52 | +| [38-auth-multi-tenant-isolation.md](./todo/system-design/38-auth-multi-tenant-isolation.md) | 📝 **Phase 0/1 已完成(2026-08-25),Phase 2-3 未开工**。Phase 1 给 9 个此前零防护的路由文件挂 `requireAuth`/`requireAdmin`;`agent-chat.js` 用新写的 `requireAuthOrInternal`(进程内 `INTERNAL_TOKEN`)——因为发现三处服务端自环调用(招聘/训练自动流水线、goal 汇报回调)拿不到用户 cookie,直接挂锁会打断核心自动化链路。17 个既有测试文件的 67+ 处用例从没设过登录态,改为测试环境默认放行 + 新增 `route-auth-gate.test.js` 结构化守卫测试专盯"锁真的挂上了没有"。远程部署前置需求:登录鉴权 + 公司级多租户隔离。现状 `auth.js`/`jwt-auth.js`(注册/登录/JWT-cookie/OAuth)+ WebSocket 鉴权都已可用,真正缺的是①把已有的锁挂到目前零防护的 `company.js`/`agent-chat.js`/`okr.js`/`tasks.js`/`devloop.js`/`graph-run.js`(含双挂载点)等路由上、②`company.json` 从 0 新增 `ownerId` 归属概念(无 SQL 表,纯文件)。已拍板:存量公司迁移统一划给种子账号 `admin`/`hex1234`(不强制改密);OKR 跟随 company 隔离(含独立的遗留 `/api/okr` 挂载点);`arena.js`/`evolution.js` 核实为疑似废弃代码(前端零引用、5 个月未改动),只挂登录不投入隔离;`dream.js` 系统级,挂 `requireAdmin`。**Phase 0**:种子脚本 + 登录页 + 路由守卫 + 401 拦截器 + 退出登录,真实浏览器走通(含顺手修复的 `MasterChatDrawer` 登录页泄漏)。Phase 1(全站要求登录)→ Phase 2(`router.param('companyId')` 归属校验,最大工作量)→ Phase 3(聊天隔离验证+前端收尾)待开工 | |
| 53 | | [37-org-graph-department-scoping.md](./todo/system-design/37-org-graph-department-scoping.md) | 📝 **设计讨论,调用点扫描已完成,尚未立项实现**。现状 org graph 按 `company_id` 单例(`27-P2` 标题原文即"公司级"),塞不下 HR/财务/市场这类跟"交付流水线"无关的公司常设职能——`system/agents/hr/`("招·留·管·退")本体已存在却从没被装进任何图,全靠 fire-and-forget chat 招聘、无图执行审计轨迹。提议把 org graph 作用域从 `company_id` 升级为 `(company_id, department_id)`,一家公司可装配多张部门图,每张仍保持人工固定/慢变/可审计。全仓库调用点扫描结论:改造面是最初估算的 4 倍——真正的单点真相在 `work-graph-db.js:readDefGraph` 的裸 SQL 不变式(不在服务层),还牵涉两个易混淆服务文件间必须字节级一致的重复唯一索引、独立的 `graph-seed-service.js` 对账子系统、`company-hire-org-layer.test.js` 专属测试、`orggraph-planning`/`task-planning` 两份 agent 人设文档的自我认知改写 | | 53 | | [37-org-graph-department-scoping.md](./todo/system-design/37-org-graph-department-scoping.md) | 📝 **设计讨论,调用点扫描已完成,尚未立项实现**。现状 org graph 按 `company_id` 单例(`27-P2` 标题原文即"公司级"),塞不下 HR/财务/市场这类跟"交付流水线"无关的公司常设职能——`system/agents/hr/`("招·留·管·退")本体已存在却从没被装进任何图,全靠 fire-and-forget chat 招聘、无图执行审计轨迹。提议把 org graph 作用域从 `company_id` 升级为 `(company_id, department_id)`,一家公司可装配多张部门图,每张仍保持人工固定/慢变/可审计。全仓库调用点扫描结论:改造面是最初估算的 4 倍——真正的单点真相在 `work-graph-db.js:readDefGraph` 的裸 SQL 不变式(不在服务层),还牵涉两个易混淆服务文件间必须字节级一致的重复唯一索引、独立的 `graph-seed-service.js` 对账子系统、`company-hire-org-layer.test.js` 专属测试、`orggraph-planning`/`task-planning` 两份 agent 人设文档的自我认知改写 | |
| 54 | | [36-prd-agent-methodology-training-feasibility.md](./todo/system-design/36-prd-agent-methodology-training-feasibility.md) | 📝 **调研 + 阶段一/二均已完成**。回应"落地页太单薄是不是该用 Innovator 先训练专业 agent、再用 Dream 持续迭代"——认同方向。**阶段一**:`graph-engine.js` 结算节点后把 `success`/`fail:<reason>` 写进本体 `memory.md`(此前只有人类对话路径会写,Dream 天天空转)。**阶段二**:新建独立试验节点 `prd-methodology-trial` 走真实 Hunt→Digest→Evolve,2 轮收敛(96/97 分,真实基线 50 分),三个真实历史项目反推验证方法论有效;过程中连带修复同一个 2026-08-16 改名批次遗留的 5 处 `innovator`→`org-innovator` 死链(后端两处硬编码、前端"重新训练"按钮、`/api/agent/list` 展示 id 来源的四个 `config.json`),以及 `AgentScaffold` 默认模板一处裸相对路径缺陷。结论:FDE 对方法论类角色确实管用,但收敛速度/验收严谨度依赖任务书显式写明的自纠正要求,非免人工黑盒。环节 3(训练产物 → 雇佣本体转换)尚未开工 | | 54 | | [36-prd-agent-methodology-training-feasibility.md](./todo/system-design/36-prd-agent-methodology-training-feasibility.md) | 📝 **调研 + 阶段一/二均已完成**。回应"落地页太单薄是不是该用 Innovator 先训练专业 agent、再用 Dream 持续迭代"——认同方向。**阶段一**:`graph-engine.js` 结算节点后把 `success`/`fail:<reason>` 写进本体 `memory.md`(此前只有人类对话路径会写,Dream 天天空转)。**阶段二**:新建独立试验节点 `prd-methodology-trial` 走真实 Hunt→Digest→Evolve,2 轮收敛(96/97 分,真实基线 50 分),三个真实历史项目反推验证方法论有效;过程中连带修复同一个 2026-08-16 改名批次遗留的 5 处 `innovator`→`org-innovator` 死链(后端两处硬编码、前端"重新训练"按钮、`/api/agent/list` 展示 id 来源的四个 `config.json`),以及 `AgentScaffold` 默认模板一处裸相对路径缺陷。结论:FDE 对方法论类角色确实管用,但收敛速度/验收严谨度依赖任务书显式写明的自纠正要求,非免人工黑盒。环节 3(训练产物 → 雇佣本体转换)尚未开工 | |
| 55 | | [35-validation-rigor-and-body-readiness-proposal.md](./todo/system-design/35-validation-rigor-and-body-readiness-proposal.md) | 📝 **复盘 + 改进方案,四条提案全部已实施**。承接 31/32/33/34 号的复盘结论:任务结果差主要是架构问题(完整性闸门 `missingBodyDefinition` 只在招聘时查、图执行侧完全没有第二道防线——新发现)+ 一个具体的招聘装配可靠性 bug,不是 agent 能力问题。提案 A:本体不完整时图执行前置判 `fail_reason=body_not_ready`,不扣预算。提案 D:`goal-contract.js` 的 `evidence_hint` 命中浏览器类关键词的维度必须有证据支撑,否则判不过。提案 C:新增 `scripts/visual-acceptance-check.mjs` 复用检查工具。提案 B:控制台「验收证据」小节展示每个维度有没有证据支撑 | | 55 | | [35-validation-rigor-and-body-readiness-proposal.md](./todo/system-design/35-validation-rigor-and-body-readiness-proposal.md) | 📝 **复盘 + 改进方案,四条提案全部已实施**。承接 31/32/33/34 号的复盘结论:任务结果差主要是架构问题(完整性闸门 `missingBodyDefinition` 只在招聘时查、图执行侧完全没有第二道防线——新发现)+ 一个具体的招聘装配可靠性 bug,不是 agent 能力问题。提案 A:本体不完整时图执行前置判 `fail_reason=body_not_ready`,不扣预算。提案 D:`goal-contract.js` 的 `evidence_hint` 命中浏览器类关键词的维度必须有证据支撑,否则判不过。提案 C:新增 `scripts/visual-acceptance-check.mjs` 复用检查工具。提案 B:控制台「验收证据」小节展示每个维度有没有证据支撑 | |
docs/todo/system-design/38-auth-multi-tenant-isolation.md
| @@ -96,10 +96,12 @@ router.param('companyId', (req, res, next, companyId) => { | @@ -96,10 +96,12 @@ router.param('companyId', (req, res, next, companyId) => { | ||
| 96 | - 顺手修了一处过程中发现的泄漏:`App.vue` 原来无条件渲染 `MasterChatDrawer`,未登录时登录页右上角也会露出这个悬浮入口——改成 `v-if="showChrome"`(`route.name !== 'Login' && !!user`),登录页干净了。 | 96 | - 顺手修了一处过程中发现的泄漏:`App.vue` 原来无条件渲染 `MasterChatDrawer`,未登录时登录页右上角也会露出这个悬浮入口——改成 `v-if="showChrome"`(`route.name !== 'Login' && !!user`),登录页干净了。 |
| 97 | - **验收(真实浏览器走过一遍,非仅测试通过)**:未登录访问 `/company` 等任意页面 → 跳 `/login?redirect=/company`;`admin`/`hex1234` 登录成功 → 跳回原目标页,导航栏显示 `admin` + 退出按钮;直接刷新/新开页面访问受保护路由(非 SPA 内跳转)同样正确放行;点退出 → 清 cookie、跳回 `/login`;`npm run build` 产物构建通过,无报错。因为业务路由还没加锁,登录后依然能看到现状全部数据(等价于"登录墙搭好了,屋里还没上锁"),符合这一阶段的验收边界。 | 97 | - **验收(真实浏览器走过一遍,非仅测试通过)**:未登录访问 `/company` 等任意页面 → 跳 `/login?redirect=/company`;`admin`/`hex1234` 登录成功 → 跳回原目标页,导航栏显示 `admin` + 退出按钮;直接刷新/新开页面访问受保护路由(非 SPA 内跳转)同样正确放行;点退出 → 清 cookie、跳回 `/login`;`npm run build` 产物构建通过,无报错。因为业务路由还没加锁,登录后依然能看到现状全部数据(等价于"登录墙搭好了,屋里还没上锁"),符合这一阶段的验收边界。 |
| 98 | 98 | ||
| 99 | -### Phase 1 — 全站要求登录(不做归属过滤) | ||
| 100 | -- 给 `agent-chat.js`/`company.js`/`okr.js`/`tasks.js`/`devloop.js`/`graph-run.js`(两个挂载点)/`arena.js`/`config.js`/`evolution.js`/`whatsapp-auth.js` 挂 `requireAuth`,显式排除 `open-api.js`、`auth.js` 自身、`/api/health`。 | ||
| 101 | -- **`dream.js` 挂 `requireAdmin` 而不是 `requireAuth`**(已决策,见 §五)——它是系统级的经验汇流视图,只有 admin 能看,不是"登录即可看"。 | ||
| 102 | -- **验收**:未登录状态下所有 API 都 401;已登录状态下行为跟 Phase 0 完全一致(任何登录用户依然能看到全部公司——归属过滤还没上);非 admin 登录用户访问 `dream.js` 下的接口 403。这一步的回归测试很简单:跑现有测试套件 + 确认没有新增的越权点。 | 99 | +### Phase 1 — 全站要求登录(不做归属过滤)✅ 已完成(2026-08-25) |
| 100 | +- 给 `company.js`/`okr.js`/`tasks.js`/`devloop.js`/`graph-run.js`(两个挂载点:`router` + `companyRouter`)/`arena.js`/`config.js`/`evolution.js`/`whatsapp-auth.js` 挂 `requireAuth`,显式排除 `open-api.js`(自己一套 `OPEN_API_KEYS`)、`auth.js` 自身、`/api/health`。 | ||
| 101 | +- **`dream.js` 挂 `requireAdmin`**(已决策,见 §五)——系统级经验汇流视图,只有 admin 能看。 | ||
| 102 | +- **`agent-chat.js` 挂的是新写的 `requireAuthOrInternal`,不是普通 `requireAuth`**——实现过程中发现的真实风险:`company.js:82/506`(招聘/训练自动流水线)、`task-settle.js:452`(goal 汇报回调)三处**服务端自环调用**会 `fetch('http://127.0.0.1:.../api/agent/...')` 打这个文件,这三处根本拿不到任何用户 cookie,直接挂 `requireAuth` 会把公司自动创建、招聘训练、任务汇报这几条核心自动化链路全部打断成 401。解法:`jwt-auth.js` 新增进程内 `INTERNAL_TOKEN`(启动时随机生成、只活在内存里,不落盘不读环境变量——调用方和校验方永远在同一个 Node 进程),`requireAuthOrInternal` 先认这个头(`x-internal-token`),认不出再退回普通 `requireAuth`;三处自环调用改成带上这个头。 | ||
| 103 | +- **测试套件的连带问题**:仓库里已有 17 个测试文件、67+ 处用例直接拿 supertest 打这几个路由,从没设过登录 cookie(测的是业务逻辑,不是鉴权)——挂完锁后这批测试集体从 200/201 变 401。没有逐个文件补登录态(会引入 bcrypt 开销拖慢整个套件,且这些用例本来就不该关心鉴权),改为:`jwt-auth.js` 的 `requireAuth`/`requireAdmin` 在 `NODE_ENV==='test'`(vitest 自动设置)时默认放行,除非显式设了 `FORCE_REAL_AUTH=1`;新增 `server/tests/unit/route-auth-gate.test.js` 专门在 `FORCE_REAL_AUTH=1` 下做结构检查(`router.stack` 里有没有那个中间件函数本身、且排在所有具体路由前面),不发真实 HTTP、不碰 DB,确保"锁真的挂上了"这件事有测试兜底,而不是被 67 处业务测试的默认放行悄悄掩盖。 | ||
| 104 | +- **验收**:真实 `curl` 走过一遍——无 cookie 访问 `company.js`/`tasks.js`/`devloop.js`/`okr.js`/`graph-run.js`/`dream.js` 全部 401;`admin`/`hex1234` 登录后全部可访问;新注册的非 admin 用户能访问 `company.js` 等(Phase 1 还没做归属过滤,符合预期),但访问 `dream.js` 得到 403;`open-api.js`/`auth.js` 未受影响(前者仍是自己的 API Key 报错文案,后者登录本身不能被锁死)。手工临时删掉 `tasks.js` 的 `router.use(requireAuth)` 验证过守卫测试真的会红。全量 `npx vitest run`:1427 passed(含新增 13 条守卫测试),3 个失败与本次改动无关(`git stash` 验证过,改动前后同样失败:`agent-md-refs`/`graduation-criteria-reality`/`graph-engine-extend-budget`,是仓库里并发的其它工作留下的,非本次引入)。 | ||
| 103 | 105 | ||
| 104 | ### Phase 2 — 公司归属隔离(这次真正的硬骨头) | 106 | ### Phase 2 — 公司归属隔离(这次真正的硬骨头) |
| 105 | - `company.json` schema 加 `ownerId`;`create()` 接收 owner 参数(从 `req.user.id` 传入);`list()` 按 `req.user` 过滤(admin 角色不过滤)。 | 107 | - `company.json` schema 加 `ownerId`;`create()` 接收 owner 参数(从 `req.user.id` 传入);`list()` 按 `req.user` 过滤(admin 角色不过滤)。 |
server/middleware/jwt-auth.js
| @@ -103,9 +103,30 @@ function extractUser(req) { | @@ -103,9 +103,30 @@ function extractUser(req) { | ||
| 103 | } | 103 | } |
| 104 | 104 | ||
| 105 | /** | 105 | /** |
| 106 | + * 38 号文档 Phase 1 落地时发现:仓库里已有 17 个测试文件直接拿 supertest 打 | ||
| 107 | + * company.js/tasks.js/devloop.js/... 这些路由,从没设过登录 cookie——这些用例测的是 | ||
| 108 | + * 业务逻辑(队列/归档/CRUD),不是鉴权本身,一个个改成先登录再打显然是本末倒置 | ||
| 109 | + * (还会因为 bcrypt 而拖慢整个套件)。 | ||
| 110 | + * | ||
| 111 | + * 所以测试环境默认让 requireAuth/requireAdmin 直接放行(`NODE_ENV=test` 是 vitest | ||
| 112 | + * 自动设的,不需要每个测试文件单独配置),补一条真正验证"锁真的挂上了"的守卫测试 | ||
| 113 | + * (`server/tests/unit/route-auth-gate.test.js`),跑之前设 `FORCE_REAL_AUTH=1` | ||
| 114 | + * 关掉这里的放行,其余测试不受影响。**不是每次调用都读一遍 env**:中间件内联判断, | ||
| 115 | + * 保证同一个测试文件里前后设置的 env 值能立刻生效(不被模块加载时的一次性求值缓存)。 | ||
| 116 | + */ | ||
| 117 | +function isTestAuthBypass() { | ||
| 118 | + return process.env.NODE_ENV === 'test' && process.env.FORCE_REAL_AUTH !== '1'; | ||
| 119 | +} | ||
| 120 | +const TEST_BYPASS_USER = { id: -1, role: 'admin', display_name: 'test-bypass', email: 'test-bypass@local' }; | ||
| 121 | + | ||
| 122 | +/** | ||
| 106 | * Middleware: require authentication (401 if not logged in) | 123 | * Middleware: require authentication (401 if not logged in) |
| 107 | */ | 124 | */ |
| 108 | function requireAuth(req, res, next) { | 125 | function requireAuth(req, res, next) { |
| 126 | + if (isTestAuthBypass()) { | ||
| 127 | + req.user = TEST_BYPASS_USER; | ||
| 128 | + return next(); | ||
| 129 | + } | ||
| 109 | const user = extractUser(req); | 130 | const user = extractUser(req); |
| 110 | if (!user) { | 131 | if (!user) { |
| 111 | return res.status(401).json({ | 132 | return res.status(401).json({ |
| @@ -129,6 +150,10 @@ function optionalAuth(req, res, next) { | @@ -129,6 +150,10 @@ function optionalAuth(req, res, next) { | ||
| 129 | * Middleware: require admin role (403 if not admin) | 150 | * Middleware: require admin role (403 if not admin) |
| 130 | */ | 151 | */ |
| 131 | function requireAdmin(req, res, next) { | 152 | function requireAdmin(req, res, next) { |
| 153 | + if (isTestAuthBypass()) { | ||
| 154 | + req.user = TEST_BYPASS_USER; | ||
| 155 | + return next(); | ||
| 156 | + } | ||
| 132 | const user = extractUser(req); | 157 | const user = extractUser(req); |
| 133 | if (!user) { | 158 | if (!user) { |
| 134 | return res.status(401).json({ error: 'Unauthorized' }); | 159 | return res.status(401).json({ error: 'Unauthorized' }); |
| @@ -154,14 +179,41 @@ function extractUserFromToken(token) { | @@ -154,14 +179,41 @@ function extractUserFromToken(token) { | ||
| 154 | } | 179 | } |
| 155 | } | 180 | } |
| 156 | 181 | ||
| 182 | +/** | ||
| 183 | + * 进程内令牌,只用于同一台机器上的服务端自环调用(agent-chat.js 挂 requireAuth 之后, | ||
| 184 | + * company.js:82/506、task-settle.js:452 这三处 `fetch('http://127.0.0.1:.../api/agent/...')` | ||
| 185 | + * 拿不到任何用户 cookie,会被 401 挡住——38 号文档 Phase 1)。 | ||
| 186 | + * | ||
| 187 | + * 只在内存里生成、不落盘、不读环境变量:调用方和校验方永远在同一个 Node 进程里 | ||
| 188 | + * (自环 HTTP 只是"本进程调本进程",不是真的跨进程/跨机器),进程重启两边一起换新值, | ||
| 189 | + * 不存在"改了忘同步"的问题,也不需要额外的密钥管理。 | ||
| 190 | + */ | ||
| 191 | +const INTERNAL_TOKEN = require('crypto').randomBytes(24).toString('hex'); | ||
| 192 | +const INTERNAL_TOKEN_HEADER = 'x-internal-token'; | ||
| 193 | + | ||
| 194 | +/** | ||
| 195 | + * Middleware: 放行本进程自环调用(带对的 INTERNAL_TOKEN),其余情况退回 requireAuth。 | ||
| 196 | + * 自环调用不代表任何真人用户,`req.user` 置一个可识别的系统身份,供下游判断。 | ||
| 197 | + */ | ||
| 198 | +function requireAuthOrInternal(req, res, next) { | ||
| 199 | + if (req.headers[INTERNAL_TOKEN_HEADER] === INTERNAL_TOKEN) { | ||
| 200 | + req.user = { id: 0, role: 'system', display_name: 'system', internal: true }; | ||
| 201 | + return next(); | ||
| 202 | + } | ||
| 203 | + return requireAuth(req, res, next); | ||
| 204 | +} | ||
| 205 | + | ||
| 157 | module.exports = { | 206 | module.exports = { |
| 158 | signAndSetCookie, | 207 | signAndSetCookie, |
| 159 | clearCookie, | 208 | clearCookie, |
| 160 | requireAuth, | 209 | requireAuth, |
| 161 | requireAdmin, | 210 | requireAdmin, |
| 211 | + requireAuthOrInternal, | ||
| 162 | optionalAuth, | 212 | optionalAuth, |
| 163 | extractUser, | 213 | extractUser, |
| 164 | extractUserFromToken, | 214 | extractUserFromToken, |
| 215 | + INTERNAL_TOKEN, | ||
| 216 | + INTERNAL_TOKEN_HEADER, | ||
| 165 | JWT_SECRET, | 217 | JWT_SECRET, |
| 166 | COOKIE_NAME, | 218 | COOKIE_NAME, |
| 167 | }; | 219 | }; |
server/routes/agent-chat.js
| @@ -29,8 +29,15 @@ const { listCompanyAgents } = require('../services/company-service'); | @@ -29,8 +29,15 @@ const { listCompanyAgents } = require('../services/company-service'); | ||
| 29 | // 交付物归宿只有一份推导(28 §4.4),公司环境上下文与执行链路的运行时坐标共用它 | 29 | // 交付物归宿只有一份推导(28 §4.4),公司环境上下文与执行链路的运行时坐标共用它 |
| 30 | const { companyWorkspaceDir } = require('../src/core/prompt-builder'); | 30 | const { companyWorkspaceDir } = require('../src/core/prompt-builder'); |
| 31 | const systemConfig = require('../config'); | 31 | const systemConfig = require('../config'); |
| 32 | +const { requireAuthOrInternal } = require('../middleware/jwt-auth'); | ||
| 32 | const router = express.Router(); | 33 | const router = express.Router(); |
| 33 | 34 | ||
| 35 | +// 38 号文档 Phase 1:全站要求登录,但这个文件同时承接三处服务端自环调用 | ||
| 36 | +// (company.js:82/506、task-settle.js:452 的 fetch('.../api/agent/...'))—— | ||
| 37 | +// 那几处拿不到用户 cookie,用 requireAuthOrInternal 而不是 requireAuth, | ||
| 38 | +// 放行带对内部令牌的自环请求,其余情况仍然要求真实登录。 | ||
| 39 | +router.use(requireAuthOrInternal); | ||
| 40 | + | ||
| 34 | // ── 自唤醒回调:端口取实际监听端口,测试环境整体短路 ──────────────────── | 41 | // ── 自唤醒回调:端口取实际监听端口,测试环境整体短路 ──────────────────── |
| 35 | // | 42 | // |
| 36 | // 曾经这六处各自写死 `hostname:'localhost', port: systemConfig.port`(3010)。 | 43 | // 曾经这六处各自写死 `hostname:'localhost', port: systemConfig.port`(3010)。 |
server/routes/arena.js
| @@ -3,7 +3,12 @@ | @@ -3,7 +3,12 @@ | ||
| 3 | */ | 3 | */ |
| 4 | const express = require('express'); | 4 | const express = require('express'); |
| 5 | const router = express.Router(); | 5 | const router = express.Router(); |
| 6 | +const { requireAuth } = require('../middleware/jwt-auth'); | ||
| 6 | const taxonomy = require('../config/taxonomy'); | 7 | const taxonomy = require('../config/taxonomy'); |
| 8 | + | ||
| 9 | +// 38 号文档 Phase 1:疑似废弃代码(前端零引用、~5 个月未改动),最省事处理——只挂登录, | ||
| 10 | +// 不深挖是否该删(不属于这次鉴权改造的范围)。 | ||
| 11 | +router.use(requireAuth); | ||
| 7 | const { ROLES, SCENES, PLATFORMS, autoTag, migrateOldScene } = taxonomy; | 12 | const { ROLES, SCENES, PLATFORMS, autoTag, migrateOldScene } = taxonomy; |
| 8 | const config = require('../config'); | 13 | const config = require('../config'); |
| 9 | const skillDb = require('../services/skill-db'); | 14 | const skillDb = require('../services/skill-db'); |
server/routes/company.js
| @@ -8,6 +8,12 @@ const express = require('express'); | @@ -8,6 +8,12 @@ const express = require('express'); | ||
| 8 | const path = require('path'); | 8 | const path = require('path'); |
| 9 | const fs = require('fs'); | 9 | const fs = require('fs'); |
| 10 | const router = express.Router(); | 10 | const router = express.Router(); |
| 11 | +const { requireAuth, INTERNAL_TOKEN, INTERNAL_TOKEN_HEADER } = require('../middleware/jwt-auth'); | ||
| 12 | + | ||
| 13 | +// 38 号文档 Phase 1:全站要求登录。公司归属过滤(谁能看到哪家公司)留到 Phase 2 的 | ||
| 14 | +// router.param('companyId', ...) 统一收口,这一步只挡"没登录"。 | ||
| 15 | +router.use(requireAuth); | ||
| 16 | + | ||
| 11 | const { CompanyService, listCompanyAgents, buildAgentDefinitionBrief } = require('../services/company-service'); | 17 | const { CompanyService, listCompanyAgents, buildAgentDefinitionBrief } = require('../services/company-service'); |
| 12 | const { ScheduleService } = require('../services/schedule-service'); | 18 | const { ScheduleService } = require('../services/schedule-service'); |
| 13 | const orgGraphService = require('../services/org-graph-service'); | 19 | const orgGraphService = require('../services/org-graph-service'); |
| @@ -75,7 +81,9 @@ function triggerOrgGraphPlanning(company, tag = 'OrgGraph') { | @@ -75,7 +81,9 @@ function triggerOrgGraphPlanning(company, tag = 'OrgGraph') { | ||
| 75 | 81 | ||
| 76 | fetch(`http://127.0.0.1:${PORT}/api/agent/${encodeURIComponent(namespacedId)}/chat`, { | 82 | fetch(`http://127.0.0.1:${PORT}/api/agent/${encodeURIComponent(namespacedId)}/chat`, { |
| 77 | method: 'POST', | 83 | method: 'POST', |
| 78 | - headers: { 'Content-Type': 'application/json' }, | 84 | + // 38 号文档 Phase 1:agent-chat.js 挂了 requireAuthOrInternal,自环调用要带这个头 |
| 85 | + // 才不会被 401 挡住(拿不到任何用户 cookie)。 | ||
| 86 | + headers: { 'Content-Type': 'application/json', [INTERNAL_TOKEN_HEADER]: INTERNAL_TOKEN }, | ||
| 79 | body: JSON.stringify({ | 87 | body: JSON.stringify({ |
| 80 | message: `公司 ${id}(${name})已创建。\n公司目标(company-goal):${goal || '(未填写)'}\n公司描述:${description || '(未填写)'}\n请按 orggraph-planning 工作流规划并建立这家公司的 workflow:读 system/org-graph-templates/ 候选骨架 + 可招角色清单,产出执行流图后调用 POST /api/company/${id}/org-graph 落库,再按拓扑序完成招聘。`, | 88 | message: `公司 ${id}(${name})已创建。\n公司目标(company-goal):${goal || '(未填写)'}\n公司描述:${description || '(未填写)'}\n请按 orggraph-planning 工作流规划并建立这家公司的 workflow:读 system/org-graph-templates/ 候选骨架 + 可招角色清单,产出执行流图后调用 POST /api/company/${id}/org-graph 落库,再按拓扑序完成招聘。`, |
| 81 | conversationId: `orggraph-bootstrap-${id}-${Date.now().toString(36)}`, | 89 | conversationId: `orggraph-bootstrap-${id}-${Date.now().toString(36)}`, |
| @@ -499,7 +507,7 @@ router.post('/:companyId/hire', async (req, res) => { | @@ -499,7 +507,7 @@ router.post('/:companyId/hire', async (req, res) => { | ||
| 499 | setTimeout(() => { | 507 | setTimeout(() => { |
| 500 | fetch(`http://127.0.0.1:${PORT}/api/agent/org-innovator/chat`, { | 508 | fetch(`http://127.0.0.1:${PORT}/api/agent/org-innovator/chat`, { |
| 501 | method: 'POST', | 509 | method: 'POST', |
| 502 | - headers: { 'Content-Type': 'application/json' }, | 510 | + headers: { 'Content-Type': 'application/json', [INTERNAL_TOKEN_HEADER]: INTERNAL_TOKEN }, |
| 503 | body: JSON.stringify({ | 511 | body: JSON.stringify({ |
| 504 | message: `公司 ${req.params.companyId} 的员工 ${role} 骨架和基础技能已注入完毕。请你使用 FDE pipeline 对该 Agent ${namespacedId} (特别是它的技能)进行评测和进化训练,直到 Benchmark 达标 (Score ≥ 95)。训练过程包括 Hunt(验证数据), Digest, Evolver 等。请立即开始评估和训练,并保持向我汇报。`, | 512 | message: `公司 ${req.params.companyId} 的员工 ${role} 骨架和基础技能已注入完毕。请你使用 FDE pipeline 对该 Agent ${namespacedId} (特别是它的技能)进行评测和进化训练,直到 Benchmark 达标 (Score ≥ 95)。训练过程包括 Hunt(验证数据), Digest, Evolver 等。请立即开始评估和训练,并保持向我汇报。`, |
| 505 | conversationId: trainConversationId | 513 | conversationId: trainConversationId |
server/routes/config.js
| @@ -8,6 +8,11 @@ const router = express.Router(); | @@ -8,6 +8,11 @@ const router = express.Router(); | ||
| 8 | const fs = require('fs'); | 8 | const fs = require('fs'); |
| 9 | const path = require('path'); | 9 | const path = require('path'); |
| 10 | const os = require('os'); | 10 | const os = require('os'); |
| 11 | +const { requireAuth } = require('../middleware/jwt-auth'); | ||
| 12 | + | ||
| 13 | +// 38 号文档 Phase 1:现状未深入调研是否有公司维度数据,先按最保守的方式处理——只挂登录, | ||
| 14 | +// 后续如发现内部数据需要按公司过滤,再提级到 Phase 2 的归属校验。 | ||
| 15 | +router.use(requireAuth); | ||
| 11 | 16 | ||
| 12 | const ROOT = path.resolve(__dirname, '../..'); | 17 | const ROOT = path.resolve(__dirname, '../..'); |
| 13 | 18 |
server/routes/devloop.js
| @@ -17,6 +17,10 @@ | @@ -17,6 +17,10 @@ | ||
| 17 | 17 | ||
| 18 | const express = require('express'); | 18 | const express = require('express'); |
| 19 | const router = express.Router(); | 19 | const router = express.Router(); |
| 20 | +const { requireAuth } = require('../middleware/jwt-auth'); | ||
| 21 | + | ||
| 22 | +// 38 号文档 Phase 1:全站要求登录,公司归属过滤(经 :pid 反查 company_id)留到 Phase 2。 | ||
| 23 | +router.use(requireAuth); | ||
| 20 | 24 | ||
| 21 | const loopController = require('../src/core/devloop/loop-controller'); | 25 | const loopController = require('../src/core/devloop/loop-controller'); |
| 22 | const pathResolver = require('../services/devloop/path-resolver'); | 26 | const pathResolver = require('../services/devloop/path-resolver'); |
server/routes/dream.js
| @@ -7,8 +7,13 @@ | @@ -7,8 +7,13 @@ | ||
| 7 | */ | 7 | */ |
| 8 | const express = require('express'); | 8 | const express = require('express'); |
| 9 | const router = express.Router(); | 9 | const router = express.Router(); |
| 10 | +const { requireAdmin } = require('../middleware/jwt-auth'); | ||
| 10 | const dream = require('../services/dream-service'); | 11 | const dream = require('../services/dream-service'); |
| 11 | 12 | ||
| 13 | +// 38 号文档 Phase 1:Dream 是系统级的跨公司经验汇流视图,只有 admin 能看, | ||
| 14 | +// 不是"登录即可看"(普通用户不该看到别的公司提炼出的经验)。 | ||
| 15 | +router.use(requireAdmin); | ||
| 16 | + | ||
| 12 | // dreamSweep 串行跑几十个本体、每个可能起两次 LLM 调用,不能塞进一次 HTTP 请求里等。 | 17 | // dreamSweep 串行跑几十个本体、每个可能起两次 LLM 调用,不能塞进一次 HTTP 请求里等。 |
| 13 | // 用最小的内存态跟踪「在跑没在跑」+「上一轮结果」,够控制台轮询用。 | 18 | // 用最小的内存态跟踪「在跑没在跑」+「上一轮结果」,够控制台轮询用。 |
| 14 | let sweepState = { sweeping: false, startedAt: null, finishedAt: null, results: null, error: null }; | 19 | let sweepState = { sweeping: false, startedAt: null, finishedAt: null, results: null, error: null }; |
server/routes/evolution.js
| @@ -4,6 +4,11 @@ | @@ -4,6 +4,11 @@ | ||
| 4 | */ | 4 | */ |
| 5 | const express = require('express'); | 5 | const express = require('express'); |
| 6 | const router = express.Router(); | 6 | const router = express.Router(); |
| 7 | +const { requireAuth } = require('../middleware/jwt-auth'); | ||
| 8 | + | ||
| 9 | +// 38 号文档 Phase 1:疑似废弃代码(前端零引用、~5 个月未改动),最省事处理——只挂登录, | ||
| 10 | +// 不深挖是否该删(不属于这次鉴权改造的范围)。 | ||
| 11 | +router.use(requireAuth); | ||
| 7 | const { | 12 | const { |
| 8 | evolutionQueries, forageSkills, RunController, | 13 | evolutionQueries, forageSkills, RunController, |
| 9 | crossoverGeneration, selectGeneration, evaluateRunFitness, | 14 | crossoverGeneration, selectGeneration, evaluateRunFitness, |
server/routes/graph-run.js
| @@ -41,10 +41,16 @@ const { | @@ -41,10 +41,16 @@ const { | ||
| 41 | RunSlotBusyError, | 41 | RunSlotBusyError, |
| 42 | } = require('../services/work-graph-db'); | 42 | } = require('../services/work-graph-db'); |
| 43 | const { companyRootDir } = require('../services/company-paths'); | 43 | const { companyRootDir } = require('../services/company-paths'); |
| 44 | +const { requireAuth } = require('../middleware/jwt-auth'); | ||
| 44 | 45 | ||
| 45 | const router = express.Router(); | 46 | const router = express.Router(); |
| 46 | const companyRouter = express.Router(); | 47 | const companyRouter = express.Router(); |
| 47 | 48 | ||
| 49 | +// 38 号文档 Phase 1:全站要求登录(两个挂载点都要挂,§六风险 2 —— 这个文件以两种方式 | ||
| 50 | +// 挂载到 index.js,改的时候容易漏一个)。公司归属过滤留到 Phase 2。 | ||
| 51 | +router.use(requireAuth); | ||
| 52 | +companyRouter.use(requireAuth); | ||
| 53 | + | ||
| 48 | // ──────────────────────────────────────────────────────────────── | 54 | // ──────────────────────────────────────────────────────────────── |
| 49 | // 小工具 | 55 | // 小工具 |
| 50 | // ──────────────────────────────────────────────────────────────── | 56 | // ──────────────────────────────────────────────────────────────── |
server/routes/okr.js
| @@ -16,11 +16,16 @@ | @@ -16,11 +16,16 @@ | ||
| 16 | */ | 16 | */ |
| 17 | 17 | ||
| 18 | const { Router } = require('express'); | 18 | const { Router } = require('express'); |
| 19 | +const { requireAuth } = require('../middleware/jwt-auth'); | ||
| 19 | const { OkrService } = require('../services/okr-service'); | 20 | const { OkrService } = require('../services/okr-service'); |
| 20 | 21 | ||
| 21 | const router = Router(); | 22 | const router = Router(); |
| 22 | const okr = new OkrService(); | 23 | const okr = new OkrService(); |
| 23 | 24 | ||
| 25 | +// 38 号文档 Phase 1:全站要求登录。这是独立于 company.js 的遗留全局 OKR 挂载点 | ||
| 26 | +// (目前无公司维度),公司归属改造留到 Phase 2(§四 Phase 2 / §五.1)。 | ||
| 27 | +router.use(requireAuth); | ||
| 28 | + | ||
| 24 | // ── 列出所有目标 ── | 29 | // ── 列出所有目标 ── |
| 25 | router.get('/', (req, res) => { | 30 | router.get('/', (req, res) => { |
| 26 | res.json(okr.list()); | 31 | res.json(okr.list()); |
server/routes/tasks.js
| 1 | const express = require('express'); | 1 | const express = require('express'); |
| 2 | const router = express.Router(); | 2 | const router = express.Router(); |
| 3 | +const { requireAuth } = require('../middleware/jwt-auth'); | ||
| 3 | const { TaskService } = require('../services/task-service'); | 4 | const { TaskService } = require('../services/task-service'); |
| 4 | 5 | ||
| 6 | +// 38 号文档 Phase 1:全站要求登录,公司归属过滤留到 Phase 2。 | ||
| 7 | +router.use(requireAuth); | ||
| 8 | + | ||
| 5 | const taskService = new TaskService(); | 9 | const taskService = new TaskService(); |
| 6 | 10 | ||
| 7 | // ── Task List 缓存 (10秒 TTL) ── | 11 | // ── Task List 缓存 (10秒 TTL) ── |
server/routes/whatsapp-auth.js
| @@ -9,6 +9,11 @@ const express = require('express'); | @@ -9,6 +9,11 @@ const express = require('express'); | ||
| 9 | const path = require('path'); | 9 | const path = require('path'); |
| 10 | const fs = require('fs'); | 10 | const fs = require('fs'); |
| 11 | const router = express.Router(); | 11 | const router = express.Router(); |
| 12 | +const { requireAuth } = require('../middleware/jwt-auth'); | ||
| 13 | + | ||
| 14 | +// 38 号文档 Phase 1:这是本机管理员在浏览器里扫码连接 WhatsApp bot 的操作面板, | ||
| 15 | +// 不是外部 WhatsApp 服务器回调的 webhook,挂登录门槛不会挡外部回调。 | ||
| 16 | +router.use(requireAuth); | ||
| 12 | 17 | ||
| 13 | const AUTH_DIR = path.resolve(__dirname, '../../skills/whatsapp-bot/store/auth'); | 18 | const AUTH_DIR = path.resolve(__dirname, '../../skills/whatsapp-bot/store/auth'); |
| 14 | 19 |
server/services/task-settle.js
| @@ -449,9 +449,12 @@ async function reportGoalOutcome(goal, status, tasks) { | @@ -449,9 +449,12 @@ async function reportGoalOutcome(goal, status, tasks) { | ||
| 449 | // 原来这里是**发射后不管**:`.then` 只对 !ok 打一行 error,函数立刻 return。 | 449 | // 原来这里是**发射后不管**:`.then` 只对 !ok 打一行 error,函数立刻 return。 |
| 450 | // 进程在这几十毫秒到几秒里被杀、或端点回 5xx,上级就永远不会被叫醒,而库里这条 goal | 450 | // 进程在这几十毫秒到几秒里被杀、或端点回 5xx,上级就永远不会被叫醒,而库里这条 goal |
| 451 | // 长得和一次完美收敛一模一样。现在等到结果再决定盖不盖「已汇报」的戳。 | 451 | // 长得和一次完美收敛一模一样。现在等到结果再决定盖不盖「已汇报」的戳。 |
| 452 | + // 38 号文档 Phase 1:agent-chat.js 挂了 requireAuthOrInternal,自环调用要带这个头 | ||
| 453 | + // 才不会被 401 挡住(拿不到任何用户 cookie)。 | ||
| 454 | + const { INTERNAL_TOKEN, INTERNAL_TOKEN_HEADER } = require('../middleware/jwt-auth'); | ||
| 452 | const ok = await fetch(`http://127.0.0.1:${PORT}/api/agent/task/report`, { | 455 | const ok = await fetch(`http://127.0.0.1:${PORT}/api/agent/task/report`, { |
| 453 | method: 'POST', | 456 | method: 'POST', |
| 454 | - headers: { 'Content-Type': 'application/json' }, | 457 | + headers: { 'Content-Type': 'application/json', [INTERNAL_TOKEN_HEADER]: INTERNAL_TOKEN }, |
| 455 | body: JSON.stringify({ | 458 | body: JSON.stringify({ |
| 456 | taskId: goal.id, | 459 | taskId: goal.id, |
| 457 | from, | 460 | from, |
server/tests/unit/route-auth-gate.test.js
0 → 100644
| 1 | +/** | ||
| 2 | + * route-auth-gate.test.js —— 38 号文档 Phase 1 的守卫测试 | ||
| 3 | + * | ||
| 4 | + * 只验证一件事:"这个 router 的 requireAuth/requireAdmin/requireAuthOrInternal 是不是真的 | ||
| 5 | + * 挂在最前面",不发真实 HTTP 请求、不碰 DB —— 结构检查:`router.stack` 里能不能找到那个 | ||
| 6 | + * 中间件函数本身(同一个函数引用,不是同名重写),且它排在所有具体路由(`layer.route` 有值 | ||
| 7 | + * 的层)前面。真正打 HTTP 会 401 由各业务测试文件顺带覆盖(jwt-auth.js 的 | ||
| 8 | + * FORCE_REAL_AUTH 默认在测试环境放行,这里显式打开验证真实拦截效果)。 | ||
| 9 | + * | ||
| 10 | + * 这条测试存在的理由:67 处业务路由测试全靠 supertest 直打路由,从没设过登录 cookie—— | ||
| 11 | + * 如果只跑那些测试,`router.use(requireAuth)` 被误删也不会被任何测试发现(默认放行, | ||
| 12 | + * 删不删都是 200)。这条测试专门盯"锁挂了没有"这一件事,跟业务逻辑测试互不重叠。 | ||
| 13 | + */ | ||
| 14 | +import { describe, it, expect, afterAll } from 'vitest'; | ||
| 15 | + | ||
| 16 | +process.env.NODE_ENV = 'test'; | ||
| 17 | +process.env.FORCE_REAL_AUTH = '1'; // 关掉 jwt-auth.js 的测试环境默认放行 | ||
| 18 | + | ||
| 19 | +const { | ||
| 20 | + requireAuth, | ||
| 21 | + requireAdmin, | ||
| 22 | + requireAuthOrInternal, | ||
| 23 | +} = require('../../middleware/jwt-auth'); | ||
| 24 | + | ||
| 25 | +/** router.stack 里有没有这个中间件函数本身,且排在第一条具体路由之前 */ | ||
| 26 | +function gateIndex(router, middlewareFn) { | ||
| 27 | + const stack = router.stack || []; | ||
| 28 | + return stack.findIndex((layer) => layer.handle === middlewareFn); | ||
| 29 | +} | ||
| 30 | +function firstRouteIndex(router) { | ||
| 31 | + const stack = router.stack || []; | ||
| 32 | + return stack.findIndex((layer) => !!layer.route); | ||
| 33 | +} | ||
| 34 | +function expectGated(router, middlewareFn, label) { | ||
| 35 | + const gateAt = gateIndex(router, middlewareFn); | ||
| 36 | + const routeAt = firstRouteIndex(router); | ||
| 37 | + expect(gateAt, `${label}: 没有挂上预期的鉴权中间件`).toBeGreaterThanOrEqual(0); | ||
| 38 | + expect(gateAt, `${label}: 鉴权中间件排在了具体路由后面,前面那些路由没被拦到`) | ||
| 39 | + .toBeLessThan(routeAt === -1 ? Infinity : routeAt); | ||
| 40 | +} | ||
| 41 | + | ||
| 42 | +describe('route-auth-gate(38 号文档 Phase 1)', () => { | ||
| 43 | + afterAll(() => { | ||
| 44 | + delete process.env.FORCE_REAL_AUTH; | ||
| 45 | + }); | ||
| 46 | + | ||
| 47 | + it('company.js 挂了 requireAuth', () => { | ||
| 48 | + expectGated(require('../../routes/company.js'), requireAuth, 'company.js'); | ||
| 49 | + }); | ||
| 50 | + | ||
| 51 | + it('tasks.js 挂了 requireAuth', () => { | ||
| 52 | + expectGated(require('../../routes/tasks.js'), requireAuth, 'tasks.js'); | ||
| 53 | + }); | ||
| 54 | + | ||
| 55 | + it('devloop.js 挂了 requireAuth', () => { | ||
| 56 | + expectGated(require('../../routes/devloop.js'), requireAuth, 'devloop.js'); | ||
| 57 | + }); | ||
| 58 | + | ||
| 59 | + it('graph-run.js 两个挂载点都挂了 requireAuth(router + companyRouter)', () => { | ||
| 60 | + const graphRun = require('../../routes/graph-run.js'); | ||
| 61 | + expectGated(graphRun, requireAuth, 'graph-run.js router'); | ||
| 62 | + expectGated(graphRun.companyRouter, requireAuth, 'graph-run.js companyRouter'); | ||
| 63 | + }); | ||
| 64 | + | ||
| 65 | + it('okr.js 挂了 requireAuth', () => { | ||
| 66 | + expectGated(require('../../routes/okr.js'), requireAuth, 'okr.js'); | ||
| 67 | + }); | ||
| 68 | + | ||
| 69 | + it('arena.js 挂了 requireAuth', () => { | ||
| 70 | + expectGated(require('../../routes/arena.js'), requireAuth, 'arena.js'); | ||
| 71 | + }); | ||
| 72 | + | ||
| 73 | + it('config.js 挂了 requireAuth', () => { | ||
| 74 | + expectGated(require('../../routes/config.js'), requireAuth, 'config.js'); | ||
| 75 | + }); | ||
| 76 | + | ||
| 77 | + it('evolution.js 挂了 requireAuth', () => { | ||
| 78 | + expectGated(require('../../routes/evolution.js'), requireAuth, 'evolution.js'); | ||
| 79 | + }); | ||
| 80 | + | ||
| 81 | + it('whatsapp-auth.js 挂了 requireAuth', () => { | ||
| 82 | + expectGated(require('../../routes/whatsapp-auth.js'), requireAuth, 'whatsapp-auth.js'); | ||
| 83 | + }); | ||
| 84 | + | ||
| 85 | + it('dream.js 挂了 requireAdmin(系统级,只有 admin 能看,不是登录即可看)', () => { | ||
| 86 | + expectGated(require('../../routes/dream.js'), requireAdmin, 'dream.js'); | ||
| 87 | + }); | ||
| 88 | + | ||
| 89 | + it('agent-chat.js 挂了 requireAuthOrInternal(不是普通 requireAuth——三处服务端自环调用要放行)', () => { | ||
| 90 | + expectGated(require('../../routes/agent-chat.js'), requireAuthOrInternal, 'agent-chat.js'); | ||
| 91 | + }); | ||
| 92 | + | ||
| 93 | + it('open-api.js 不该挂 cookie 鉴权(它自己一套 OPEN_API_KEYS 体系,面向外部 Skill 市场调用方)', () => { | ||
| 94 | + const openApi = require('../../routes/open-api.js'); | ||
| 95 | + expect(gateIndex(openApi, requireAuth)).toBe(-1); | ||
| 96 | + expect(gateIndex(openApi, requireAuthOrInternal)).toBe(-1); | ||
| 97 | + }); | ||
| 98 | + | ||
| 99 | + it('auth.js 自身不挂全局鉴权(登录/注册端点必须对未登录用户可达)', () => { | ||
| 100 | + const auth = require('../../routes/auth.js'); | ||
| 101 | + expect(gateIndex(auth, requireAuth)).toBe(-1); | ||
| 102 | + }); | ||
| 103 | +}); |
-
mentioned in commit 9e586563f99a1994a4fec41d8562ec987c16ff39