From a2deeb61b03ce0a37f4c8246ce7f1406694a7699 Mon Sep 17 00:00:00 2001 From: chenchen Date: Mon, 11 May 2026 20:44:33 +0800 Subject: [PATCH] fix(client+server): OAuth login path now surfaces user identity in desktop TitleBar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of the recurring "I don't see the logged-in user info in the desktop client" complaint: the credentials-login path builds an mcpAuth record with email / displayName / role from the mcp-server /api/auth/login response and the TitleBar UserPill renders fine. But the OAuth-login path (the typical browser-redirect flow) only received an sk- API key in the callback query string — no user fields. So: status.user == null UserPill: if (!user) return null → blank space where the user pill should be. Fix on backend (heicode_oauth.go HeicodeOAuthAuthorize): - After issuing the sk- token, load the authenticated user from the session and embed email / name / role (root|admin|user) / channel_id / user_id as query params on the redirect URI. Fix on client (cc-haha/src/server/api/heicode-auth.ts handleOAuthCallback): - Read those query params (pickUserFromQuery), build an mcpAuth record (buildMcpAuthFromOAuthQuery), and pass it through loginAndActivate the same way the credentials path does. The accessToken slot holds the sk- key as a placeholder — OAuth flow doesn't deliver a refreshable JWT pair, and this mcpAuth exists purely to surface identity on the TitleBar. After this, OAuth-route users see the same gradient-avatar pill with their email / name / role badge that credentials-route users already see. Co-Authored-By: Claude Opus 4.7 (1M context) --- cc-haha/src/server/api/heicode-auth.ts | 69 +++++++++++++++++++++++++- heicode/controller/heicode_oauth.go | 32 ++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/cc-haha/src/server/api/heicode-auth.ts b/cc-haha/src/server/api/heicode-auth.ts index fb9316f..dabd70f 100644 --- a/cc-haha/src/server/api/heicode-auth.ts +++ b/cc-haha/src/server/api/heicode-auth.ts @@ -481,9 +481,20 @@ async function handleOAuthCallback(url: URL): Promise { oauthSessions.delete(state) try { + // Pull the user identity fields that the heicode backend's + // /heicode/oauth/authorize redirect now embeds in the query string + // (email, name, role, channel_id, user_id). Without this the TitleBar + // UserPill stays hidden because mcpAuth.email is undefined. + const oauthUser = pickUserFromQuery(url) + const tokenFromQuery = pickTokenFromQuery(url) if (tokenFromQuery) { - await loginAndActivate(session.providerId, tokenFromQuery) + await loginAndActivate( + session.providerId, + tokenFromQuery, + undefined, + oauthUser ? buildMcpAuthFromOAuthQuery(tokenFromQuery, oauthUser) : undefined, + ) return callbackHtml(true, '登录成功,已回到 HeiCode。你现在可以关闭此页面。') } @@ -507,13 +518,67 @@ async function handleOAuthCallback(url: URL): Promise { redirectUri, codeVerifier: session.codeVerifier, }) - await loginAndActivate(session.providerId, exchangedToken) + await loginAndActivate( + session.providerId, + exchangedToken, + undefined, + oauthUser ? buildMcpAuthFromOAuthQuery(exchangedToken, oauthUser) : undefined, + ) return callbackHtml(true, '登录成功,已回到 HeiCode。你现在可以关闭此页面。') } catch (err) { return callbackHtml(false, err instanceof Error ? err.message : String(err)) } } +/** Read user identity fields the heicode backend embeds in the OAuth redirect. */ +function pickUserFromQuery(url: URL): { + email?: string + name?: string + role?: string + channelId?: string + userId?: string +} | null { + const email = url.searchParams.get('email')?.trim() + const name = url.searchParams.get('name')?.trim() + const role = url.searchParams.get('role')?.trim() + const channelId = url.searchParams.get('channel_id')?.trim() + const userId = url.searchParams.get('user_id')?.trim() + if (!email && !name && !role && !channelId && !userId) return null + return { + ...(email && { email }), + ...(name && { name }), + ...(role && { role }), + ...(channelId && { channelId }), + ...(userId && { userId }), + } +} + +/** + * Shape a mcpAuth record from the OAuth-redirect query params. The OAuth flow + * does not deliver a refreshable JWT pair (it only gives the sk- API key), so + * we set placeholder access/refresh tokens. They're only meaningful for the + * credentials flow (Step 1 POST /api/auth/login); this record exists purely to + * surface the user identity on the TitleBar UserPill. + */ +function buildMcpAuthFromOAuthQuery( + apiKey: string, + u: { email?: string; name?: string; role?: string; channelId?: string; userId?: string }, +) { + const now = Date.now() + return { + accessToken: apiKey, + refreshToken: '', + accessExpiresAt: now + 24 * 3600 * 1000, + refreshExpiresAt: now, + managerLoginUrl: '', + ...(u.userId && { userId: u.userId }), + ...(u.channelId && { channelId: u.channelId }), + ...(u.email && { email: u.email }), + ...(u.name && { displayName: u.name }), + ...(u.role && { role: u.role }), + } +} + // ─── Helpers ─────────────────────────────────────────────────── async function loginAndActivate( diff --git a/heicode/controller/heicode_oauth.go b/heicode/controller/heicode_oauth.go index 83a53e7..48e6bd6 100644 --- a/heicode/controller/heicode_oauth.go +++ b/heicode/controller/heicode_oauth.go @@ -85,6 +85,38 @@ func HeicodeOAuthAuthorize(c *gin.Context) { if providerID != "" { q.Set("provider_id", providerID) } + + // Include the authenticated user's identity so the desktop client can + // display it in the title bar / settings without making a separate + // /api/user/self call (that endpoint requires a session cookie which the + // desktop client doesn't have). This is the missing piece that left the + // UserPill empty for OAuth-route logins. + var user model.User + if dbErr := model.DB.First(&user, userID).Error; dbErr == nil { + if email := strings.TrimSpace(user.Email); email != "" { + q.Set("email", email) + } + display := strings.TrimSpace(user.DisplayName) + if display == "" { + display = strings.TrimSpace(user.Username) + } + if display != "" { + q.Set("name", display) + } + switch { + case user.Role >= 100: + q.Set("role", "root") + case user.Role >= 10: + q.Set("role", "admin") + default: + q.Set("role", "user") + } + if g := strings.TrimSpace(user.Group); g != "" { + q.Set("channel_id", g) + } + q.Set("user_id", fmt.Sprintf("%d", userID)) + } + parsed.RawQuery = q.Encode() c.Redirect(http.StatusFound, parsed.String()) }