R87 修复企微直登会话提示
这个提交包含在:
@@ -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 写入;风险低,仅补齐与密码登录一致的前端会话提示。"
|
||||||
|
@@ -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) {
|
function bindCookieOptions(config) {
|
||||||
return {
|
return {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
@@ -519,6 +529,7 @@ async function wecomAuthRoutes(fastify) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
reply.setCookie(fastify.config.refreshCookieName, session.refreshToken, refreshCookieOptions(fastify.config));
|
reply.setCookie(fastify.config.refreshCookieName, session.refreshToken, refreshCookieOptions(fastify.config));
|
||||||
|
reply.setCookie("has_session", "1", sessionHintCookieOptions(fastify.config));
|
||||||
return reply.redirect(`${origin}${returnTo}`);
|
return reply.redirect(`${origin}${returnTo}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,6 +225,58 @@ describe("wecom auth routes R27", () => {
|
|||||||
await app.close();
|
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 () => {
|
it("returns structured diagnostics when wecom callback has only open id", async () => {
|
||||||
const logs = [];
|
const logs = [];
|
||||||
mockWecomFetchWithPayload({
|
mockWecomFetchWithPayload({
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户