diff --git a/frontend-web/src/pages/LoginPage.tsx b/frontend-web/src/pages/LoginPage.tsx index 1d2188a0..40433aee 100644 --- a/frontend-web/src/pages/LoginPage.tsx +++ b/frontend-web/src/pages/LoginPage.tsx @@ -35,6 +35,35 @@ import { fetchYLogConfig } from '@/core/log/y-log-config'; import { persistYLogToken } from '@/core/log/y-log-token'; import { persistLoginCredentials } from '@/core/auth/login-credentials'; import { OptType, writeYLog } from '@/core/log/y-log'; +import { authStorageGet } from '@/core/auth/scoped-storage'; + +/** 令牌脱敏:日志里只露头尾,避免完整令牌泄漏。 */ +function maskLoginToken(value: string): string { + if (!value) return ''; + if (value.length <= 10) return `${value.slice(0, 2)}***(${value.length})`; + return `${value.slice(0, 6)}...${value.slice(-4)}(${value.length})`; +} + +/** 读取已落地的 userInfo(脱敏 access token 后)供登录后排查。 */ +function summarizeStoredUserInfo(): Record | null { + try { + const raw = authStorageGet('userInfo'); + if (!raw) return null; + const info = JSON.parse(raw) as Record; + return { + keys: Object.keys(info), + yUserId: info.yUserId, + userId: info.userId, + userName: info.userName, + gateDepartName: info.gateDepartName, + typeName: info.typeName, + access_token: maskLoginToken(String(info.access_token ?? '')), + accessToken: maskLoginToken(String(info.accessToken ?? '')), + }; + } catch { + return null; + } +} const DEFAULT_USERNAME = 'guest'; const IFRAME_LOGIN_SOURCE = 'iframe'; @@ -171,19 +200,68 @@ export default function LoginPage() { async function runLogin() { setStatus('loading'); setErrorMessage(''); + + // ── 调试:登录路由参数全打印 ────────────────────────────────────────────── + // token / yUserId / 用户名 / ticket 任一方式都打印(令牌脱敏),方便排查 + // “userInfo 取不到值 / 传值不对” 一类问题。`embed=0`/正常态也照打。 + const loginMethod = authToken + ? 'token(authToken)' + : yUserId + ? 'yUserId' + : onlineTicket + ? 'onlineTicket' + : toolToken + ? 'toolsToken' + : 'username'; + console.info('[login] route params', { + method: loginMethod, + pathUsername: resolvedUsername, + yUserId, + access_token: maskLoginToken(access_token), + toolToken: maskLoginToken(toolToken), + authToken: maskLoginToken(authToken), + gatePassword: gatePassword ? maskLoginToken(gatePassword) : '', + onlineTicket: onlineTicket ? maskLoginToken(onlineTicket) : '', + taskId, + goPath, + redirect: redirectTo, + isEmbed, + rawSearch: searchParams.toString(), + }); + try { let auth; let loginUserName = ''; + + // 外部用户信息(yUserId → consumer 用户详情)与登录鉴权方式**解耦**:只要 URL + // 带 yUserId 就拉一次 userDetailByYuserId,把 yUserId/userName/gateDepartName/ + // typeName/accessToken 落进 userInfo。之前 token(authToken) 登录会短路掉这一步, + // 导致 token 登录后 userInfo 残缺(只剩 userName)、iframe/轻应用拿不到这些值。 + if (yUserId) { + if (authToken) { + // token 登录下 consumer 失败不应阻断登录(仅影响 userInfo 的富信息)。 + try { + await loginByYUserId(yUserId, toolToken || null, access_token || null); + } catch (error) { + console.warn('[login] loginByYUserId failed under token login (non-fatal)', error); + } + } else { + await loginByYUserId(yUserId, toolToken || null, access_token || null); + } + } + if (authToken) { // token 换登录:后端用 authToken 调上游接口换出用户名再登录 auth = await loginByToken(authToken); + // 让 token 登录也写入正确用户名(后端 getTokenInfo 解析出的原始登录名)。 + // 若上面 yUserId 已从 consumer 落了 userName,则 setStoredUserName 不会覆盖它。 + loginUserName = auth.username?.trim() ?? ''; } else { let deerflowUsername = resolvedUsername; - if (yUserId) { - await loginByYUserId(yUserId, toolToken || null, access_token || null); - } else if (onlineTicket) { + // yUserId 优先(已在上面落好 userInfo + tools_token);其余方式仅在无 yUserId 时启用。 + if (!yUserId && onlineTicket) { deerflowUsername = await resolveUsernameByOnlineTicket(onlineTicket); - } else if (toolToken) { + } else if (!yUserId && toolToken) { persistToolsToken(toolToken); } @@ -217,6 +295,17 @@ export default function LoginPage() { if (!yUserId) { persistAccessToken(access_token); } + + // ── 调试:登录落地结果(auth + 实际写入的 userInfo,access token 已脱敏) ── + console.info('[login] stored result', { + method: loginMethod, + user_id: auth.user_id, + email: auth.email, + authUsername: auth.username, + loginUserName, + userInfo: summarizeStoredUserInfo(), + }); + // 嵌入模式不写 sidebarEnabled(localStorage 的「简洁/全量」偏好会跨标签污染正常页); // 去 chrome 由按标签页的嵌入会话(isEmbedActive)驱动,见 PageLayout。 if (!isEmbed) persistSidebarEnabled(sidebarEnabled);