From ee0cfb00fe913ad4cf11f2b34187691ba2fade09 Mon Sep 17 00:00:00 2001 From: shunli Date: Tue, 5 May 2026 14:03:47 +0800 Subject: [PATCH] =?UTF-8?q?R87=20=E4=BF=AE=E5=A4=8D=E4=BC=81=E5=BE=AE?= =?UTF-8?q?=E7=9B=B4=E7=99=BB=E4=BC=9A=E8=AF=9D=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-05-05_14-01-13-r87-wecom-session-hint.csv | 2 + server/src/routes/wecomAuth.js | 11 ++++ server/tests/unit/routes/wecomAuth.test.js | 52 +++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 issues/2026-05-05_14-01-13-r87-wecom-session-hint.csv diff --git a/issues/2026-05-05_14-01-13-r87-wecom-session-hint.csv b/issues/2026-05-05_14-01-13-r87-wecom-session-hint.csv new file mode 100644 index 00000000..719c26fe --- /dev/null +++ b/issues/2026-05-05_14-01-13-r87-wecom-session-hint.csv @@ -0,0 +1,2 @@ +ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes +R87,企微直登会话提示闭环,"修复企业微信 OAuth 回调成功后前端仍判定未登录的问题:企微登录成功分支需与账号密码登录一致写入 has_session 提示 cookie。","已绑定企微员工从企业微信应用进入后,OAuth 回调成功应直接进入 /portal,前端启动时能通过 refresh 恢复登录态,不再回到登录页。","业务流程:企业微信应用进入 OA→OAuth 回调→服务端创建会话→前端 /portal 启动→refresh 恢复登录态。验证:cd server && npm test -- tests/unit/routes/wecomAuth.test.js;cd server && node --check src/routes/wecomAuth.js;部署后检查线上 AUTH_WECOM_LOGIN_SUCCESS 后不再回到 /login。",shell|vitest,DONE,DONE,DONE,server/src/routes/wecomAuth.js|server/tests/unit/routes/wecomAuth.test.js,"[]","回滚路径:撤销企微成功登录分支新增的 has_session cookie 写入;风险低,仅补齐与密码登录一致的前端会话提示。" diff --git a/server/src/routes/wecomAuth.js b/server/src/routes/wecomAuth.js index 86390380..38a2bd04 100644 --- a/server/src/routes/wecomAuth.js +++ b/server/src/routes/wecomAuth.js @@ -28,6 +28,16 @@ function refreshCookieOptions(config) { }; } +function sessionHintCookieOptions(config) { + return { + httpOnly: false, + sameSite: "strict", + secure: config.cookieSecure, + path: "/", + maxAge: config.refreshTtlSeconds, + }; +} + function bindCookieOptions(config) { return { httpOnly: true, @@ -519,6 +529,7 @@ async function wecomAuthRoutes(fastify) { }); reply.setCookie(fastify.config.refreshCookieName, session.refreshToken, refreshCookieOptions(fastify.config)); + reply.setCookie("has_session", "1", sessionHintCookieOptions(fastify.config)); return reply.redirect(`${origin}${returnTo}`); }); } diff --git a/server/tests/unit/routes/wecomAuth.test.js b/server/tests/unit/routes/wecomAuth.test.js index ebd4e562..2a67693d 100644 --- a/server/tests/unit/routes/wecomAuth.test.js +++ b/server/tests/unit/routes/wecomAuth.test.js @@ -225,6 +225,58 @@ describe("wecom auth routes R27", () => { await app.close(); }); + it("sets session hint cookie after successful wecom login callback", async () => { + mockWecomFetch({ userId: "wecom-bound-user" }); + const authSession = { + findMany: vi.fn().mockResolvedValue([]), + updateMany: vi.fn().mockResolvedValue({ count: 0 }), + create: vi.fn().mockResolvedValue({ id: "session-1" }), + }; + const prisma = buildPrisma({ + authSession, + wecomIdentityBinding: { + findUnique: vi.fn().mockResolvedValue({ + employee_id: EMPLOYEE_ID, + employee: { + id: EMPLOYEE_ID, + status: "active", + token_version: 0, + }, + }), + }, + }); + prisma.$transaction = vi.fn(async (callback) => callback(prisma)); + const app = await buildApp({ prisma }); + const state = signPayload({ + exp: Date.now() + 60_000, + nonce: "nonce-login-success", + 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(302); + expect(response.headers.location).toBe("https://oa.hyinda.com/portal"); + const setCookieHeader = String(response.headers["set-cookie"] ?? ""); + expect(setCookieHeader).toContain("refresh_token="); + expect(setCookieHeader).toContain("has_session=1"); + expect(authSession.create).toHaveBeenCalled(); + expect(prisma.auditLog.create).toHaveBeenCalledWith(expect.objectContaining({ + data: expect.objectContaining({ + actor_id: EMPLOYEE_ID, + action: "AUTH_WECOM_LOGIN_SUCCESS", + target_type: "employee", + target_id: EMPLOYEE_ID, + }), + })); + + await app.close(); + }); + it("returns structured diagnostics when wecom callback has only open id", async () => { const logs = []; mockWecomFetchWithPayload({