From 01bf3ad93c3fefb06329c89952c54e57aa659aa8 Mon Sep 17 00:00:00 2001 From: willsather <56037657+willsather@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:09:09 +0000 Subject: [PATCH 1/6] fix: handle expired tokens in GitHub connected accounts --- .../api/github/orgs/install-status/route.ts | 222 ++++++-- apps/web/app/settings/accounts-section.tsx | 493 ++++++++++-------- apps/web/hooks/use-session.ts | 1 - 3 files changed, 450 insertions(+), 266 deletions(-) diff --git a/apps/web/app/api/github/orgs/install-status/route.ts b/apps/web/app/api/github/orgs/install-status/route.ts index 4b2b5a522..d83945423 100644 --- a/apps/web/app/api/github/orgs/install-status/route.ts +++ b/apps/web/app/api/github/orgs/install-status/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; +import { getGitHubAccount } from "@/lib/db/accounts"; import { getInstallationsByUserId } from "@/lib/db/installations"; import { isGitHubAppConfigured } from "@/lib/github/app-auth"; import { getInstallationManageUrl } from "@/lib/github/installation-url"; @@ -11,22 +12,44 @@ interface GitHubOrg { avatar_url: string; } +interface GitHubMembership { + organization: GitHubOrg; + role: "admin" | "member"; + state: "active" | "pending"; +} + interface GitHubUser { id: number; login: string; avatar_url: string; } +export interface GitHubUserProfile { + githubId: number; + login: string; + avatarUrl: string; +} + export interface OrgInstallStatus { - /** Numeric GitHub account/org ID, used for target_id in install URLs */ githubId: number; login: string; avatarUrl: string; - type: "User" | "Organization"; installStatus: "installed" | "not_installed"; installationId: number | null; installationUrl: string | null; repositorySelection: "all" | "selected" | null; + role: "admin" | "member" | null; +} + +export interface ConnectionStatusResponse { + user: GitHubUserProfile; + /** Whether the user's personal account has the app installed */ + personalInstallStatus: "installed" | "not_installed"; + personalInstallationUrl: string | null; + personalRepositorySelection: "all" | "selected" | null; + orgs: OrgInstallStatus[]; + /** True when the GitHub token is expired and the data is from the DB cache */ + tokenExpired?: boolean; } export async function GET() { @@ -35,14 +58,6 @@ export async function GET() { return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); } - const token = await getUserGitHubToken(); - if (!token) { - return NextResponse.json( - { error: "GitHub not connected" }, - { status: 401 }, - ); - } - if (!isGitHubAppConfigured()) { return NextResponse.json( { error: "GitHub App not configured" }, @@ -50,15 +65,81 @@ export async function GET() { ); } + const token = await getUserGitHubToken(); + + // When the token is expired/invalid, fall back to DB-cached data so the UI + // can still show the connected user and prompt to reconnect. + if (!token) { + const [ghAccount, installations] = await Promise.all([ + getGitHubAccount(session.user.id), + getInstallationsByUserId(session.user.id), + ]); + + if (!ghAccount) { + return NextResponse.json( + { error: "GitHub not connected" }, + { status: 401 }, + ); + } + + const personalInstallation = installations.find( + (i) => + i.accountLogin.toLowerCase() === ghAccount.username.toLowerCase(), + ); + + const orgs: OrgInstallStatus[] = installations + .filter( + (i) => + i.accountLogin.toLowerCase() !== ghAccount.username.toLowerCase(), + ) + .map((i) => ({ + githubId: 0, + login: i.accountLogin, + avatarUrl: "", + installStatus: "installed" as const, + installationId: i.installationId, + installationUrl: getInstallationManageUrl( + i.installationId, + i.installationUrl, + ), + repositorySelection: i.repositorySelection, + role: null, + })); + + const response: ConnectionStatusResponse = { + user: { + githubId: Number(ghAccount.externalUserId) || 0, + login: ghAccount.username, + avatarUrl: `https://avatars.githubusercontent.com/u/${ghAccount.externalUserId}?v=4`, + }, + personalInstallStatus: personalInstallation ? "installed" : "not_installed", + personalInstallationUrl: personalInstallation + ? getInstallationManageUrl( + personalInstallation.installationId, + personalInstallation.installationUrl, + ) + : null, + personalRepositorySelection: + personalInstallation?.repositorySelection ?? null, + orgs, + tokenExpired: true, + }; + return NextResponse.json(response); + } + try { - // Fetch orgs and user profile in parallel - const [orgsResponse, userResponse] = await Promise.all([ - fetch("https://api.github.com/user/orgs?per_page=100", { - headers: { - Authorization: `Bearer ${token}`, - Accept: "application/vnd.github.v3+json", + // Fetch memberships and user profile in parallel. + // /user/memberships/orgs gives us role info (admin vs member). + const [membershipsResponse, userResponse] = await Promise.all([ + fetch( + "https://api.github.com/user/memberships/orgs?per_page=100&state=active", + { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/vnd.github.v3+json", + }, }, - }), + ), fetch("https://api.github.com/user", { headers: { Authorization: `Bearer ${token}`, @@ -67,17 +148,22 @@ export async function GET() { }), ]); - if (!orgsResponse.ok || !userResponse.ok) { + if (!userResponse.ok) { return NextResponse.json( { error: "Failed to fetch GitHub data" }, { status: 502 }, ); } - const [orgs, user] = (await Promise.all([ - orgsResponse.json(), - userResponse.json(), - ])) as [GitHubOrg[], GitHubUser]; + const user = (await userResponse.json()) as GitHubUser; + + // Memberships may 403 if the user has restricted org visibility; fall back + // to empty list and we'll still show installations from the DB. + let memberships: GitHubMembership[] = []; + if (membershipsResponse.ok) { + memberships = + (await membershipsResponse.json()) as GitHubMembership[]; + } // Get all installations from DB const installations = await getInstallationsByUserId(session.user.id); @@ -85,36 +171,27 @@ export async function GET() { installations.map((i) => [i.accountLogin.toLowerCase(), i]), ); - // Build status for the personal account + // Personal account install status const personalInstallation = installationsByLogin.get( user.login.toLowerCase(), ); - const results: OrgInstallStatus[] = [ - { - githubId: user.id, - login: user.login, - avatarUrl: user.avatar_url, - type: "User", - installStatus: personalInstallation ? "installed" : "not_installed", - installationId: personalInstallation?.installationId ?? null, - installationUrl: personalInstallation - ? getInstallationManageUrl( - personalInstallation.installationId, - personalInstallation.installationUrl, - ) - : null, - repositorySelection: personalInstallation?.repositorySelection ?? null, - }, - ]; - - // Build status for each org - for (const org of orgs) { - const installation = installationsByLogin.get(org.login.toLowerCase()); - results.push({ - githubId: org.id, - login: org.login, - avatarUrl: org.avatar_url, - type: "Organization", + + // Build org list: merge memberships + installations (some orgs may be + // installed but not in memberships if visibility is restricted, or + // vice versa). + const seenLogins = new Set(); + const orgs: OrgInstallStatus[] = []; + + // First, add all orgs from memberships + for (const membership of memberships) { + const login = membership.organization.login; + const lowerLogin = login.toLowerCase(); + seenLogins.add(lowerLogin); + const installation = installationsByLogin.get(lowerLogin); + orgs.push({ + githubId: membership.organization.id, + login, + avatarUrl: membership.organization.avatar_url, installStatus: installation ? "installed" : "not_installed", installationId: installation?.installationId ?? null, installationUrl: installation @@ -124,10 +201,55 @@ export async function GET() { ) : null, repositorySelection: installation?.repositorySelection ?? null, + role: membership.role, }); } - return NextResponse.json(results); + // Then, add any installed orgs not in memberships (can happen with + // restricted org visibility) + for (const installation of installations) { + const lowerLogin = installation.accountLogin.toLowerCase(); + if ( + lowerLogin === user.login.toLowerCase() || + seenLogins.has(lowerLogin) + ) { + continue; + } + orgs.push({ + githubId: 0, + login: installation.accountLogin, + avatarUrl: "", + installStatus: "installed", + installationId: installation.installationId, + installationUrl: getInstallationManageUrl( + installation.installationId, + installation.installationUrl, + ), + repositorySelection: installation.repositorySelection, + role: null, + }); + } + + const response: ConnectionStatusResponse = { + user: { + githubId: user.id, + login: user.login, + avatarUrl: user.avatar_url, + }, + personalInstallStatus: personalInstallation + ? "installed" + : "not_installed", + personalInstallationUrl: personalInstallation + ? getInstallationManageUrl( + personalInstallation.installationId, + personalInstallation.installationUrl, + ) + : null, + personalRepositorySelection: + personalInstallation?.repositorySelection ?? null, + orgs, + }; + return NextResponse.json(response); } catch (error) { console.error("Failed to fetch org install status:", error); return NextResponse.json( diff --git a/apps/web/app/settings/accounts-section.tsx b/apps/web/app/settings/accounts-section.tsx index 7f0cb6af2..5a378a419 100644 --- a/apps/web/app/settings/accounts-section.tsx +++ b/apps/web/app/settings/accounts-section.tsx @@ -4,11 +4,11 @@ import { AlertCircle, Building2, CheckCircle2, + ChevronDown, Circle, ExternalLink, Loader2, RefreshCw, - User as UserIcon, } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; @@ -32,15 +32,30 @@ import { fetcher } from "@/lib/swr"; // ── Types ────────────────────────────────────────────────────────────────── +interface GitHubUserProfile { + githubId: number; + login: string; + avatarUrl: string; +} + interface OrgInstallStatus { githubId: number; login: string; avatarUrl: string; - type: "User" | "Organization"; installStatus: "installed" | "not_installed"; installationId: number | null; installationUrl: string | null; repositorySelection: "all" | "selected" | null; + role: "admin" | "member" | null; +} + +interface ConnectionStatusResponse { + user: GitHubUserProfile; + personalInstallStatus: "installed" | "not_installed"; + personalInstallationUrl: string | null; + personalRepositorySelection: "all" | "selected" | null; + orgs: OrgInstallStatus[]; + tokenExpired?: boolean; } // ── Icons ────────────────────────────────────────────────────────────────── @@ -60,15 +75,6 @@ function GitHubIcon({ className }: { className?: string }) { // ── Navigation helpers ───────────────────────────────────────────────────── -function startGitHubInstallForOrg(githubId: number) { - const params = new URLSearchParams({ - next: "/settings/connections", - target_id: String(githubId), - }); - - window.location.href = `/api/github/app/install?${params.toString()}`; -} - function startGitHubInstallFromSettings() { const params = new URLSearchParams({ next: "/settings/connections", @@ -87,7 +93,6 @@ function useGitHubReturnToast() { if (!githubParam) return; - // Clean up URL params without navigation const url = new URL(window.location.href); url.searchParams.delete("github"); url.searchParams.delete("missing_installation_id"); @@ -148,7 +153,6 @@ function useGitHubReturnToast() { export function AccountsSectionSkeleton() { return (
- {/* GitHub section skeleton */}
@@ -157,22 +161,17 @@ export function AccountsSectionSkeleton() {
-
- {[1, 2].map((i) => ( -
-
- -
- - -
+
+
+
+ +
+ +
-
- ))} + +
@@ -183,45 +182,34 @@ export function AccountsSectionSkeleton() { function OrgRow({ org }: { org: OrgInstallStatus }) { const isInstalled = org.installStatus === "installed"; - const isOrg = org.type === "Organization"; return ( -
+
- {/* Avatar */} {org.avatarUrl ? ( {org.login} - ) : isOrg ? ( - ) : ( - + )} - - {/* Info */}
-
-

{org.login}

- {isOrg && ( - - org - - )} -
+

{org.login}

{isInstalled ? ( - Installed + {org.repositorySelection === "all" + ? "All repositories" + : "Selected repositories"} ) : ( - + Not installed )} @@ -229,52 +217,24 @@ function OrgRow({ org }: { org: OrgInstallStatus }) {

- {/* Right side: repo selection + action */} -
- {isInstalled && ( - - {org.repositorySelection === "all" - ? "all repositories" - : "selected repositories"} - - )} - {isInstalled ? ( - org.installationUrl ? ( - - ) : null - ) : ( +
+ {isInstalled && org.installationUrl ? ( + + ) : !isInstalled ? ( - )} -
-
- ); -} - -// ── Request access guidance ──────────────────────────────────────────────── - -function RequestAccessGuidance() { - return ( -
- -
-

Missing an organization?

-

- If an organization is not listed, you may not have membership, or the - org restricts third-party access. Ask an org owner to install the - GitHub App, or request access from your organization's settings - page on GitHub. -

+ ) : null}
); @@ -289,26 +249,27 @@ export function AccountsSection() { useGitHubReturnToast(); - // Fetch org install status only when a GitHub account is linked const { - data: orgs, - isLoading: orgsLoading, - mutate: mutateOrgs, - } = useSWR( + data: connectionData, + isLoading: connectionLoading, + mutate: mutateConnection, + } = useSWR( hasGitHubAccount ? "/api/github/orgs/install-status" : null, fetcher, ); + const tokenExpired = connectionData?.tokenExpired ?? false; + const [isRefreshing, setIsRefreshing] = useState(false); const handleRefresh = useCallback(async () => { setIsRefreshing(true); try { - await mutateOrgs(); + await mutateConnection(); } finally { setIsRefreshing(false); } - }, [mutateOrgs]); + }, [mutateConnection]); async function handleUnlink() { setUnlinking(true); @@ -316,6 +277,7 @@ export function AccountsSection() { const res = await fetch("/api/auth/github/unlink", { method: "POST" }); if (res.ok) { await mutate("/api/auth/info"); + await mutateConnection(); toast.success("GitHub disconnected"); } } catch (error) { @@ -332,84 +294,178 @@ export function AccountsSection() { return (
- {/* ── GitHub connection ── */} - - - {/* ── Future: MCP connections would go here ── */} - {/* */} +
+ {/* Header */} +
+
+ + GitHub +
+ {hasGitHub && ( + + )} +
+ + {/* Body */} +
+ {!hasGitHub ? ( + + ) : connectionLoading && !connectionData ? ( + + ) : connectionData ? ( + + ) : ( + + )} +
+
); } -// ── GitHub connection block ──────────────────────────────────────────────── +// ── Not connected ────────────────────────────────────────────────────────── -function GitHubConnection({ - hasGitHub, - orgs, - orgsLoading, - isRefreshing, +function NotConnectedState() { + return ( +
+

+ Connect your GitHub account to access repositories. +

+ +
+ ); +} + +// ── Loading skeleton for connection data ─────────────────────────────────── + +function ConnectionLoadingSkeleton() { + return ( +
+
+ +
+ + +
+
+ +
+ ); +} + +// ── Connected state ──────────────────────────────────────────────────────── + +function ConnectedState({ + data, + tokenExpired, unlinking, - onRefresh, onUnlink, }: { - hasGitHub: boolean; - orgs: OrgInstallStatus[] | null; - orgsLoading: boolean; - isRefreshing: boolean; + data: ConnectionStatusResponse; + tokenExpired: boolean; unlinking: boolean; - onRefresh: () => void; onUnlink: () => void; }) { const [disconnectOpen, setDisconnectOpen] = useState(false); - const installedCount = - orgs?.filter((o) => o.installStatus === "installed").length ?? 0; + const [orgsExpanded, setOrgsExpanded] = useState(false); + const installedOrgCount = data.orgs.filter( + (o) => o.installStatus === "installed", + ).length; return ( -
- {/* Header: GitHub branding + actions */} -
-
- -
- GitHub - {hasGitHub && ( - - · {installedCount}{" "} - {installedCount === 1 ? "account" : "accounts"} configured - - )} + <> + {/* ── User identity ── */} +
+
+ {data.user.avatarUrl ? ( + {data.user.login} + ) : ( +
+ +
+ )} +
+

{data.user.login}

+

+ {tokenExpired ? ( + + + Session expired + + ) : data.personalInstallStatus === "installed" ? ( + + + {data.personalRepositorySelection === "all" + ? "All repositories" + : "Selected repositories"} + + ) : ( + + + App not installed on personal account + + )} +

-
- {hasGitHub && ( + +
+ {tokenExpired ? ( + + ) : ( <> - - + {data.personalInstallationUrl && ( + + )} )} - {!hasGitHub && ( - - )}
+ {/* ── Organizations accordion ── */} + {data.orgs.length > 0 && !tokenExpired && ( +
+ + + {orgsExpanded && ( +
+ {data.orgs.map((org) => ( + + ))} + + {/* Add organization */} +
+ +
+ + {/* Guidance */} +
+
+ +
+

+ Missing an organization? +

+

+ If an organization is not listed, you may not have + membership, or the org restricts third-party access. Ask an + org owner to install the GitHub App, or request access from + your organization's settings page on GitHub. +

+
+
+
+
+ )} +
+ )} + {/* Disconnect confirmation dialog */} @@ -467,49 +573,6 @@ function GitHubConnection({ - - {/* Body */} -
- {!hasGitHub ? ( -

- Connect GitHub to access private repositories and enable - installations for your accounts and organizations. -

- ) : orgsLoading && !orgs ? ( -
- {[1, 2].map((i) => ( -
-
- -
- - -
-
- -
- ))} -
- ) : orgs && orgs.length > 0 ? ( -
- {orgs.map((org) => ( - - ))} - -
- ) : ( -
-

- No accounts found. Install the GitHub App to an account or - organization. -

- -
- )} -
-
+ ); } diff --git a/apps/web/hooks/use-session.ts b/apps/web/hooks/use-session.ts index 0bab76f8e..8afeb2dfd 100644 --- a/apps/web/hooks/use-session.ts +++ b/apps/web/hooks/use-session.ts @@ -10,7 +10,6 @@ export function useSession() { fetcher, { revalidateOnFocus: true, - fallbackData: { user: undefined }, }, ); From a7ac3e5a0040fc4f5786a84c30a738da92665919 Mon Sep 17 00:00:00 2001 From: willsather <56037657+willsather@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:19:22 +0000 Subject: [PATCH 2/6] fix: use GitHub orgs API instead of memberships for org list --- .../api/github/orgs/install-status/route.ts | 66 +++++++------------ apps/web/app/settings/accounts-section.tsx | 20 +++++- 2 files changed, 41 insertions(+), 45 deletions(-) diff --git a/apps/web/app/api/github/orgs/install-status/route.ts b/apps/web/app/api/github/orgs/install-status/route.ts index d83945423..132e50cc6 100644 --- a/apps/web/app/api/github/orgs/install-status/route.ts +++ b/apps/web/app/api/github/orgs/install-status/route.ts @@ -12,12 +12,6 @@ interface GitHubOrg { avatar_url: string; } -interface GitHubMembership { - organization: GitHubOrg; - role: "admin" | "member"; - state: "active" | "pending"; -} - interface GitHubUser { id: number; login: string; @@ -38,7 +32,6 @@ export interface OrgInstallStatus { installationId: number | null; installationUrl: string | null; repositorySelection: "all" | "selected" | null; - role: "admin" | "member" | null; } export interface ConnectionStatusResponse { @@ -103,7 +96,6 @@ export async function GET() { i.installationUrl, ), repositorySelection: i.repositorySelection, - role: null, })); const response: ConnectionStatusResponse = { @@ -112,7 +104,9 @@ export async function GET() { login: ghAccount.username, avatarUrl: `https://avatars.githubusercontent.com/u/${ghAccount.externalUserId}?v=4`, }, - personalInstallStatus: personalInstallation ? "installed" : "not_installed", + personalInstallStatus: personalInstallation + ? "installed" + : "not_installed", personalInstallationUrl: personalInstallation ? getInstallationManageUrl( personalInstallation.installationId, @@ -128,18 +122,14 @@ export async function GET() { } try { - // Fetch memberships and user profile in parallel. - // /user/memberships/orgs gives us role info (admin vs member). - const [membershipsResponse, userResponse] = await Promise.all([ - fetch( - "https://api.github.com/user/memberships/orgs?per_page=100&state=active", - { - headers: { - Authorization: `Bearer ${token}`, - Accept: "application/vnd.github.v3+json", - }, + // Fetch orgs and user profile in parallel + const [orgsResponse, userResponse] = await Promise.all([ + fetch("https://api.github.com/user/orgs?per_page=100", { + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/vnd.github.v3+json", }, - ), + }), fetch("https://api.github.com/user", { headers: { Authorization: `Bearer ${token}`, @@ -148,22 +138,17 @@ export async function GET() { }), ]); - if (!userResponse.ok) { + if (!userResponse.ok || !orgsResponse.ok) { return NextResponse.json( { error: "Failed to fetch GitHub data" }, { status: 502 }, ); } - const user = (await userResponse.json()) as GitHubUser; - - // Memberships may 403 if the user has restricted org visibility; fall back - // to empty list and we'll still show installations from the DB. - let memberships: GitHubMembership[] = []; - if (membershipsResponse.ok) { - memberships = - (await membershipsResponse.json()) as GitHubMembership[]; - } + const [githubOrgs, user] = (await Promise.all([ + orgsResponse.json(), + userResponse.json(), + ])) as [GitHubOrg[], GitHubUser]; // Get all installations from DB const installations = await getInstallationsByUserId(session.user.id); @@ -176,22 +161,18 @@ export async function GET() { user.login.toLowerCase(), ); - // Build org list: merge memberships + installations (some orgs may be - // installed but not in memberships if visibility is restricted, or - // vice versa). + // Build org list: merge GitHub orgs + DB installations const seenLogins = new Set(); const orgs: OrgInstallStatus[] = []; - // First, add all orgs from memberships - for (const membership of memberships) { - const login = membership.organization.login; - const lowerLogin = login.toLowerCase(); + for (const org of githubOrgs) { + const lowerLogin = org.login.toLowerCase(); seenLogins.add(lowerLogin); const installation = installationsByLogin.get(lowerLogin); orgs.push({ - githubId: membership.organization.id, - login, - avatarUrl: membership.organization.avatar_url, + githubId: org.id, + login: org.login, + avatarUrl: org.avatar_url, installStatus: installation ? "installed" : "not_installed", installationId: installation?.installationId ?? null, installationUrl: installation @@ -201,12 +182,10 @@ export async function GET() { ) : null, repositorySelection: installation?.repositorySelection ?? null, - role: membership.role, }); } - // Then, add any installed orgs not in memberships (can happen with - // restricted org visibility) + // Add any installed orgs not in the GitHub orgs list for (const installation of installations) { const lowerLogin = installation.accountLogin.toLowerCase(); if ( @@ -226,7 +205,6 @@ export async function GET() { installation.installationUrl, ), repositorySelection: installation.repositorySelection, - role: null, }); } diff --git a/apps/web/app/settings/accounts-section.tsx b/apps/web/app/settings/accounts-section.tsx index 5a378a419..254191bb1 100644 --- a/apps/web/app/settings/accounts-section.tsx +++ b/apps/web/app/settings/accounts-section.tsx @@ -46,7 +46,6 @@ interface OrgInstallStatus { installationId: number | null; installationUrl: string | null; repositorySelection: "all" | "selected" | null; - role: "admin" | "member" | null; } interface ConnectionStatusResponse { @@ -251,6 +250,7 @@ export function AccountsSection() { const { data: connectionData, + error: connectionError, isLoading: connectionLoading, mutate: mutateConnection, } = useSWR( @@ -322,6 +322,8 @@ export function AccountsSection() { ) : connectionLoading && !connectionData ? ( + ) : connectionError && !connectionData ? ( + ) : connectionData ? ( void }) { + return ( +
+
+ + Failed to load GitHub connection info. +
+ +
+ ); +} + // ── Loading skeleton for connection data ─────────────────────────────────── function ConnectionLoadingSkeleton() { From e27e6bd036b7f8a785c50f9b70f1fcc2b994445a Mon Sep 17 00:00:00 2001 From: willsather <56037657+willsather@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:29:33 +0000 Subject: [PATCH 3/6] fix: add debug logging for GitHub API errors in install-status --- .../api/github/orgs/install-status/route.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/apps/web/app/api/github/orgs/install-status/route.ts b/apps/web/app/api/github/orgs/install-status/route.ts index 132e50cc6..67ef35a9d 100644 --- a/apps/web/app/api/github/orgs/install-status/route.ts +++ b/apps/web/app/api/github/orgs/install-status/route.ts @@ -59,6 +59,10 @@ export async function GET() { } const token = await getUserGitHubToken(); + console.log("install-status: token resolved", { + userId: session.user.id, + hasToken: !!token, + }); // When the token is expired/invalid, fall back to DB-cached data so the UI // can still show the connected user and prompt to reconnect. @@ -139,6 +143,23 @@ export async function GET() { ]); if (!userResponse.ok || !orgsResponse.ok) { + const [userBody, orgsBody] = await Promise.all([ + userResponse.ok + ? Promise.resolve("OK") + : userResponse.text().catch(() => "unreadable"), + orgsResponse.ok + ? Promise.resolve("OK") + : orgsResponse.text().catch(() => "unreadable"), + ]); + console.error("GitHub API error in install-status:", { + userId: session.user.id, + userStatus: userResponse.status, + userBody: userResponse.ok ? "(ok)" : userBody, + orgsStatus: orgsResponse.status, + orgsBody: orgsResponse.ok ? "(ok)" : orgsBody, + tokenLength: token.length, + tokenPrefix: token.substring(0, 8) + "...", + }); return NextResponse.json( { error: "Failed to fetch GitHub data" }, { status: 502 }, From cd9036f5046e9981ae9b17ec44358e0e0c6ee9fc Mon Sep 17 00:00:00 2001 From: willsather <56037657+willsather@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:41:50 +0000 Subject: [PATCH 4/6] fix: improve GitHub connected accounts UI and avatar handling --- apps/web/app/settings/accounts-section.tsx | 81 +++++++++------------- 1 file changed, 34 insertions(+), 47 deletions(-) diff --git a/apps/web/app/settings/accounts-section.tsx b/apps/web/app/settings/accounts-section.tsx index 254191bb1..04b6e1ac2 100644 --- a/apps/web/app/settings/accounts-section.tsx +++ b/apps/web/app/settings/accounts-section.tsx @@ -2,10 +2,7 @@ import { AlertCircle, - Building2, - CheckCircle2, ChevronDown, - Circle, ExternalLink, Loader2, RefreshCw, @@ -181,54 +178,52 @@ export function AccountsSectionSkeleton() { function OrgRow({ org }: { org: OrgInstallStatus }) { const isInstalled = org.installStatus === "installed"; + // GitHub CDN avatar works for any org by login, even without an avatar_url + const avatarSrc = + org.avatarUrl || + (org.githubId + ? `https://avatars.githubusercontent.com/u/${org.githubId}?s=40&v=4` + : ""); return ( -
-
- {org.avatarUrl ? ( +
+
+ {avatarSrc ? ( {org.login} ) : ( - +
+ )} + {org.login} + {isInstalled ? ( + + {org.repositorySelection === "all" ? "all repos" : "selected repos"} + + ) : ( + + not installed + )} -
-

{org.login}

-

- {isInstalled ? ( - - - {org.repositorySelection === "all" - ? "All repositories" - : "Selected repositories"} - - ) : ( - - - Not installed - - )} -

-
-
+
{isInstalled && org.installationUrl ? ( - ) : !isInstalled ? ( ) : ( <> + {!tokenExpired && ( + + )} {data.personalInstallationUrl && (
-
@@ -552,9 +563,9 @@ function ConnectedState({

If an organization is not listed, you may not have - membership, or the org restricts third-party access. Ask an - org owner to install the GitHub App, or request access from - your organization's settings page on GitHub. + membership, or the org restricts third-party access. Ask + an org owner to install the GitHub App, or request access + from your organization's settings page on GitHub.