R81 增加企微回调身份诊断

这个提交包含在:
2026-05-04 17:57:39 +08:00
父节点 c25a223945
当前提交 9dbac63744
共修改 3 个文件,包含 48 行新增和 1 行删除
@@ -0,0 +1,4 @@
"ID","Title","Description","Acceptance","Test_Method","Tools","Dev_Status","Review1_Status","Regression_Status","Files","Dependencies","Notes"
"A1","R81 执行合同建档与诊断范围冻结","为企微真机回调 `AUTH_003 / missing_user_id` 建立独立执行合同。本轮范围仅覆盖:后端最小安全诊断日志、单测、远端部署、单次真机复现取证;明确不改 `wecom_identity_bindings` 模型,不把 `OpenId` 直接接入登录主键。","CSV 列完整且通过校验;A2-A3 依赖顺序清晰;Notes 明确本轮不改绑定模型、不引入迁移、不改变现有登录语义。","命令:`python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-04_17-50-36-r81-wecom-callback-identity-diagnosis.csv`。业务:确认本轮只承接“腾讯回包身份字段取证与最小诊断”,不混入企微绑定模型改造。","manual","DONE","DONE","DONE","issues/2026-05-04_17-50-36-r81-wecom-callback-identity-diagnosis.csv","[]","回滚路径:删除本 CSV,不影响运行时。真实腾讯密钥继续只保留在线上 env,不入仓。2026-05-04:R81 合同已写入并通过 validate_issues_csv.py 校验。"
"A2","企微回调缺失 UserId 的最小诊断改动","在 `server/src/routes/wecomAuth.js` 为 `missing_user_id` 分支补充安全诊断:记录 `requestId`、`mode`、`hasUserId`、`hasOpenId`、`hasDeviceId`,不记录 token/secret/完整敏感回包;补 1 条单测覆盖 OpenId-only 响应。","OpenId-only 或 UserId 缺失时,服务端能留下足够判因的安全日志;接口仍返回 `AUTH_003`;单测可证明 `details` 带有 `hasOpenId/mode` 提示;`node --check` 与目标单测通过。","命令:`cd server && node --check src/routes/wecomAuth.js`;`cd server && npm test -- tests/unit/routes/wecomAuth.test.js`。业务:模拟企微仅回 `OpenId` 的场景,确认接口返回 `401 AUTH_003` 且错误细节明确为 `missing_user_id`。","vitest","DONE","DONE","DONE","server/src/routes/wecomAuth.js|server/tests/unit/routes/wecomAuth.test.js","[""A1""]","回滚路径:回退本次路由与单测改动即可;不涉及 schema、seed、env。2026-05-04:`node --check`、`npm test -- tests/unit/routes/wecomAuth.test.js`、`npm run prisma:validate` 均已通过。"
"A3","远端部署与单次真机复现取证","将 A2 变更部署到 `118.89.77.218` 的 trialrun 环境,保持现有企微 env 与 Caddy 配置不变;你在手机企业微信内复现 1 次,我读取 server 日志确认腾讯真实回包形态。","远端 server 已更新为包含诊断日志的版本;`/api/auth/wecom/config` 仍为 `ready`;你完成 1 次真机复现后,日志能明确给出 `hasOpenId` 等诊断字段,并据此判断下一步是提示优化还是模型改造。","命令:`ssh [email protected] 'cd /opt/hyb-oa-trialrun && docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env up -d --build server'`;`curl -k https://oa.hyinda.com/api/auth/wecom/config`;`ssh [email protected] 'docker logs --since 10m hyb-oa-trialrun-server'`。业务:你用手机企业微信重新打开 OA 应用 1 次;若再次失败,记录页面时间点,我同步抓取对应日志。","ssh|manual","DOING","DOING","TODO","deploy/trialrun/deploy.sh|external|artifacts/r70/manual-trialrun-checklist.md|artifacts/r70/manual-trialrun-report.md","[""A2""]","回滚路径:远端重新部署上一版本 server 镜像或回退工作区后重建 server 容器。若日志证实仅是非内部成员/可见范围问题,则下一轮只改错误提示,不扩模型。2026-05-04:已用 root 重建远端 server 容器;`/ready`、`/api/auth/wecom/config`、`/api/auth/wecom/login` 复核通过。待你用手机复现 1 次以抓取真实回包特征。"
1 ID Title Description Acceptance Test_Method Tools Dev_Status Review1_Status Regression_Status Files Dependencies Notes
2 A1 R81 执行合同建档与诊断范围冻结 为企微真机回调 `AUTH_003 / missing_user_id` 建立独立执行合同。本轮范围仅覆盖:后端最小安全诊断日志、单测、远端部署、单次真机复现取证;明确不改 `wecom_identity_bindings` 模型,不把 `OpenId` 直接接入登录主键。 CSV 列完整且通过校验;A2-A3 依赖顺序清晰;Notes 明确本轮不改绑定模型、不引入迁移、不改变现有登录语义。 命令:`python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-04_17-50-36-r81-wecom-callback-identity-diagnosis.csv`。业务:确认本轮只承接“腾讯回包身份字段取证与最小诊断”,不混入企微绑定模型改造。 manual DONE DONE DONE issues/2026-05-04_17-50-36-r81-wecom-callback-identity-diagnosis.csv [] 回滚路径:删除本 CSV,不影响运行时。真实腾讯密钥继续只保留在线上 env,不入仓。2026-05-04:R81 合同已写入并通过 validate_issues_csv.py 校验。
3 A2 企微回调缺失 UserId 的最小诊断改动 在 `server/src/routes/wecomAuth.js` 为 `missing_user_id` 分支补充安全诊断:记录 `requestId`、`mode`、`hasUserId`、`hasOpenId`、`hasDeviceId`,不记录 token/secret/完整敏感回包;补 1 条单测覆盖 OpenId-only 响应。 OpenId-only 或 UserId 缺失时,服务端能留下足够判因的安全日志;接口仍返回 `AUTH_003`;单测可证明 `details` 带有 `hasOpenId/mode` 提示;`node --check` 与目标单测通过。 命令:`cd server && node --check src/routes/wecomAuth.js`;`cd server && npm test -- tests/unit/routes/wecomAuth.test.js`。业务:模拟企微仅回 `OpenId` 的场景,确认接口返回 `401 AUTH_003` 且错误细节明确为 `missing_user_id`。 vitest DONE DONE DONE server/src/routes/wecomAuth.js|server/tests/unit/routes/wecomAuth.test.js ["A1"] 回滚路径:回退本次路由与单测改动即可;不涉及 schema、seed、env。2026-05-04:`node --check`、`npm test -- tests/unit/routes/wecomAuth.test.js`、`npm run prisma:validate` 均已通过。
4 A3 远端部署与单次真机复现取证 将 A2 变更部署到 `118.89.77.218` 的 trialrun 环境,保持现有企微 env 与 Caddy 配置不变;你在手机企业微信内复现 1 次,我读取 server 日志确认腾讯真实回包形态。 远端 server 已更新为包含诊断日志的版本;`/api/auth/wecom/config` 仍为 `ready`;你完成 1 次真机复现后,日志能明确给出 `hasOpenId` 等诊断字段,并据此判断下一步是提示优化还是模型改造。 命令:`ssh [email protected] 'cd /opt/hyb-oa-trialrun && docker-compose -f deploy/trialrun/docker-compose.yml --env-file deploy/trialrun/.env up -d --build server'`;`curl -k https://oa.hyinda.com/api/auth/wecom/config`;`ssh [email protected] 'docker logs --since 10m hyb-oa-trialrun-server'`。业务:你用手机企业微信重新打开 OA 应用 1 次;若再次失败,记录页面时间点,我同步抓取对应日志。 ssh|manual DOING DOING TODO deploy/trialrun/deploy.sh|external|artifacts/r70/manual-trialrun-checklist.md|artifacts/r70/manual-trialrun-report.md ["A2"] 回滚路径:远端重新部署上一版本 server 镜像或回退工作区后重建 server 容器。若日志证实仅是非内部成员/可见范围问题,则下一轮只改错误提示,不扩模型。2026-05-04:已用 root 重建远端 server 容器;`/ready`、`/api/auth/wecom/config`、`/api/auth/wecom/login` 复核通过。待你用手机复现 1 次以抓取真实回包特征。
+15 -1
查看文件
@@ -363,11 +363,25 @@ async function wecomAuthRoutes(fastify) {
const returnTo = sanitizeReturnTo(ctx.returnTo, fastify.config, request); const returnTo = sanitizeReturnTo(ctx.returnTo, fastify.config, request);
const profile = await exchangeWecomCode(fastify.config, code); const profile = await exchangeWecomCode(fastify.config, code);
if (!profile.externalUserId) { if (!profile.externalUserId) {
request.log.warn(
{
requestId: request.id,
mode,
hasUserId: false,
hasOpenId: Boolean(profile.externalOpenUserId),
hasDeviceId: Boolean(profile.deviceId),
},
"wecom callback missing internal user id",
);
throw new AppError({ throw new AppError({
code: "AUTH_003", code: "AUTH_003",
message: "企业微信认证失败", message: "企业微信认证失败",
statusCode: 401, statusCode: 401,
details: { wecom: "missing_user_id" }, details: {
wecom: "missing_user_id",
hasOpenId: Boolean(profile.externalOpenUserId),
mode,
},
}); });
} }
@@ -177,6 +177,35 @@ describe("wecom auth routes R27", () => {
await app.close(); await app.close();
}); });
it("returns structured diagnostics when wecom callback has only open id", async () => {
mockWecomFetch({ userId: "", openId: "wecom-open-only" });
const app = await buildApp();
const state = signPayload({
exp: Date.now() + 60_000,
nonce: "nonce-openid-only",
returnTo: "/portal",
mode: "login",
}, JWT_SECRET);
const response = await app.inject({
method: "GET",
url: `/api/auth/wecom/callback?state=${encodeURIComponent(state)}&code=test-code`,
});
expect(response.statusCode).toBe(401);
expect(response.json().error).toEqual(expect.objectContaining({
code: "AUTH_003",
message: "企业微信认证失败",
details: expect.objectContaining({
wecom: "missing_user_id",
hasOpenId: true,
mode: "login",
}),
}));
await app.close();
});
it("creates bind audit log on successful wecom bind callback", async () => { it("creates bind audit log on successful wecom bind callback", async () => {
mockWecomFetch({ userId: "wecom-bind-1" }); mockWecomFetch({ userId: "wecom-bind-1" });
const prisma = buildPrisma(); const prisma = buildPrisma();