fix: Cline OAuth base64 parsing + name=email fallback for all OAuth accounts

- cline.ts: add decodeURIComponent before base64 decode to handle URL-encoded codes
- cline.ts: populate name = firstName+lastName || email in mapTokens
- oauth/exchange route: normalize name=email for all providers on exchange/poll/poll-callback
- Fixes: accounts showing Account #ID instead of email in providers dashboard
This commit is contained in:
diegosouzapw
2026-03-12 12:22:20 -03:00
parent 87ffe41d8c
commit 2042dcf991
2 changed files with 42 additions and 12 deletions

View File

@@ -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()

View File

@@ -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,
},
};
},
};