diff --git a/src/app/api/oauth/[provider]/[action]/route.ts b/src/app/api/oauth/[provider]/[action]/route.ts index c29f6810fd..a1c140dd85 100644 --- a/src/app/api/oauth/[provider]/[action]/route.ts +++ b/src/app/api/oauth/[provider]/[action]/route.ts @@ -226,6 +226,12 @@ export async function POST( exchangeTokens(provider, code, redirectUri, codeVerifier, state) ); + // Normalize: if name is missing, use email or displayName as fallback so accounts + // always show a real label (e.g. user@gmail.com) instead of "Account #abc123" + if (!tokenData.name && (tokenData.email || tokenData.displayName)) { + tokenData.name = tokenData.email || tokenData.displayName; + } + // Upsert: update existing connection if same provider+email, else create new const expiresAt = tokenData.expiresIn ? new Date(Date.now() + tokenData.expiresIn * 1000).toISOString() @@ -297,6 +303,11 @@ export async function POST( } if (result.success) { + // Normalize: if name is missing, use email as fallback display label + if (!result.tokens.name && (result.tokens.email || result.tokens.displayName)) { + result.tokens.name = result.tokens.email || result.tokens.displayName; + } + // Upsert: update existing connection if same provider+email, else create new const expiresAt = result.tokens.expiresIn ? new Date(Date.now() + result.tokens.expiresIn * 1000).toISOString() @@ -418,6 +429,11 @@ export async function POST( exchangeTokens(provider, params.code, redirectUri, codeVerifier, params.state) ); + // Normalize: if name is missing, use email as fallback display label + if (!tokenData.name && (tokenData.email || tokenData.displayName)) { + tokenData.name = tokenData.email || tokenData.displayName; + } + // Upsert: update existing connection if same provider+email, else create new const expiresAt = tokenData.expiresIn ? new Date(Date.now() + tokenData.expiresIn * 1000).toISOString() diff --git a/src/lib/oauth/providers/cline.ts b/src/lib/oauth/providers/cline.ts index fa5490edb4..8f17f45cdc 100644 --- a/src/lib/oauth/providers/cline.ts +++ b/src/lib/oauth/providers/cline.ts @@ -13,7 +13,14 @@ export const cline = { }, exchangeToken: async (config, code, redirectUri) => { try { + // Cline embeds tokens as base64-encoded JSON in the auth code. + // The code may be URL-encoded when pasted from the callback URL. let base64 = code; + try { + base64 = decodeURIComponent(base64); + } catch { + /* already decoded */ + } const padding = 4 - (base64.length % 4); if (padding !== 4) { base64 += "=".repeat(padding); @@ -62,16 +69,23 @@ export const cline = { }; } }, - mapTokens: (tokens) => ({ - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token, - expiresIn: tokens.expires_at - ? Math.floor((new Date(tokens.expires_at).getTime() - Date.now()) / 1000) - : 3600, - email: tokens.email, - providerSpecificData: { - firstName: tokens.firstName, - lastName: tokens.lastName, - }, - }), + mapTokens: (tokens) => { + const firstName = tokens.firstName || ""; + const lastName = tokens.lastName || ""; + const fullName = [firstName, lastName].filter(Boolean).join(" ").trim(); + return { + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + expiresIn: tokens.expires_at + ? Math.floor((new Date(tokens.expires_at).getTime() - Date.now()) / 1000) + : 3600, + // Use full name if available, fallback to email so UI shows a real label + name: fullName || tokens.email || null, + email: tokens.email, + providerSpecificData: { + firstName: tokens.firstName, + lastName: tokens.lastName, + }, + }; + }, };