diff --git a/src/app/api/metrics/prs/route.ts b/src/app/api/metrics/prs/route.ts index b5f734133..a80b0ceb0 100644 --- a/src/app/api/metrics/prs/route.ts +++ b/src/app/api/metrics/prs/route.ts @@ -3,6 +3,7 @@ import { NextRequest } from "next/server"; import { authOptions } from "@/lib/auth"; import { getAccountToken, getAllAccounts } from "@/lib/github-accounts"; import { GITHUB_API } from "@/lib/github"; +import { GitHubAuthError, githubAuthErrorResponse } from "@/lib/github-fetch"; import { isMetricsCacheBypassed, METRICS_CACHE_TTL_SECONDS, @@ -538,7 +539,7 @@ async function fetchReviewMetrics(token: string): Promise { export async function GET(req: NextRequest) { const session = await getServerSession(authOptions); if (!session?.accessToken) { - return Response.json({ error: "Unauthorized" }, { status: 401 }); + return githubAuthErrorResponse(); } const gitlabToken = typeof session.gitlabToken === "string" ? session.gitlabToken : undefined; @@ -603,9 +604,10 @@ export async function GET(req: NextRequest) { ]); return Response.json({ ...formatPRMetricsResponse(result, gitlab), reviews }); - } catch { - // Catches errors from fetchCachedPRMetrics (GitHub Search API failures). - // Returns 502 so the client knows the data is unavailable, not just empty. + } catch (e: any) { + if (e instanceof GitHubAuthError || e?.status === 401) { + return githubAuthErrorResponse(); + } return Response.json({ error: "GitHub API error" }, { status: 502 }); } } diff --git a/src/components/ContributionGraph.tsx b/src/components/ContributionGraph.tsx index b3291ee90..bcdcf7204 100644 --- a/src/components/ContributionGraph.tsx +++ b/src/components/ContributionGraph.tsx @@ -149,6 +149,7 @@ export default function ContributionGraph() { const [lastUpdated, setLastUpdated] = useState(null); const [minutesAgo, setMinutesAgo] = useState(0); const [error, setError] = useState(null); + const [sessionExpired, setSessionExpired] = useState(false); const [commits, setCommits] = useState([]); const [usesTouchTooltip, setUsesTouchTooltip] = useState(false); const [repo, setRepo] = useState("all"); @@ -264,7 +265,26 @@ export default function ContributionGraph() { // 2. Perform background sync / standard fetch try { const r = await fetch(url); - if (!r.ok) throw new Error("API error"); + if (!r.ok) { + if (r.status === 401) { + const errData = await r.json().catch(() => ({})); + if ( + errData.error === "token_expired" || + errData.error === "session_expired" || + errData.error === "Unauthorized" + ) { + if (active) { + setSessionExpired(true); + setError("Your session expired — please sign in again"); + setTimeout(() => { + window.location.href = "/api/auth/signin"; + }, 3000); + } + return; + } + } + throw new Error("API error"); + } const res: ContributionResponse = await r.json(); if (!active) return; @@ -488,6 +508,24 @@ export default function ContributionGraph() { id="contribution-activity" className="rounded-xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-sm transition-all duration-300 hover:shadow-md hover:-translate-y-1 fade-in-up" > + {sessionExpired && ( +
+
+ 🔒 +
+

Your session expired — please sign in again

+

Auto-redirecting to sign in page in 3 seconds…

+
+
+ + Sign in now + +
+ )} +

diff --git a/src/lib/github-fetch.ts b/src/lib/github-fetch.ts index bc83b1093..aaa851c91 100644 --- a/src/lib/github-fetch.ts +++ b/src/lib/github-fetch.ts @@ -78,7 +78,11 @@ function isSecondaryRateLimitBody(body: unknown): boolean { async function buildGitHubError( res: Response, -): Promise { +): Promise { + if (res.status === 401) { + return new GitHubAuthError(); + } + const { resetAt, retryAfter, remaining } = extractRateLimitInfo(res.headers); // 429: always a rate limit