Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 44 additions & 21 deletions app/api/analyze/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server"
import { getSession } from "@/lib/session"
import { getOptionalSession, getSession } from "@/lib/session"
import { analyzeUser, GitHubError } from "@/lib/github"

/**
Expand All @@ -8,37 +8,60 @@ import { analyzeUser, GitHubError } from "@/lib/github"
* /api/analyze?username=octocat public data only, no authorization needed
*/
export async function GET(request: NextRequest) {
const requested = request.nextUrl.searchParams.get("username")?.trim()
const session = await getSession()
try {
const requested = request.nextUrl.searchParams.get("username")?.trim()

let token: string | null = null
let username: string
let token: string | null = null
let username: string

if (requested) {
if (!/^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$/.test(requested)) {
return NextResponse.json(
{ error: "That is not a valid GitHub username." },
{ status: 400 }
)
}

username = requested

// Public username analysis must remain available even when the hosted
// OAuth/session layer is intentionally not configured.
const session = await getOptionalSession()

// Only reuse a hosted session token when the visitor is analyzing
// themselves. Otherwise public GitHub data is used without a token.
if (
session?.accessToken &&
session.user?.login.toLowerCase() === requested.toLowerCase()
) {
token = session.accessToken
}
} else {
// The private dashboard is an authenticated hosted feature, so keep its
// session requirement strict.
const session = await getSession()

if (!session.accessToken || !session.user) {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 })
}

if (requested) {
if (!/^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$/.test(requested)) {
return NextResponse.json({ error: "That is not a valid GitHub username." }, { status: 400 })
}
username = requested
// Only reuse the session token when the visitor is asking about themselves.
if (session.accessToken && session.user?.login.toLowerCase() === requested.toLowerCase()) {
token = session.accessToken
username = session.user.login
}
} else {
if (!session.accessToken || !session.user) {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 })
}
token = session.accessToken
username = session.user.login
}

try {
const data = await analyzeUser(token, username)
return NextResponse.json(data)
} catch (err) {
if (err instanceof GitHubError) {
return NextResponse.json({ error: err.message }, { status: err.status })
}
const message = err instanceof Error ? err.message : "Unknown error"

const message =
err instanceof Error && err.message
? err.message
: "Public GitHub analysis failed unexpectedly."

console.error("GitHub analysis request failed", err)
return NextResponse.json({ error: message }, { status: 500 })
}
}
30 changes: 23 additions & 7 deletions components/dashboard-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,30 @@ import Link from "next/link"
import { Loader2, AlertCircle, RefreshCcw, Lock, ShieldCheck } from "lucide-react"
import type { AnalysisData } from "@/lib/github"

const fetcher = (url: string) =>
fetch(url).then(async (r) => {
if (!r.ok) {
const err = await r.json().catch(() => ({ error: "Unknown error" }))
throw new Error(err.error ?? "Failed to fetch")
const fetcher = async (url: string) => {
const response = await fetch(url)
const contentType = response.headers.get("content-type") ?? ""

if (!response.ok) {
if (contentType.includes("application/json")) {
const payload = (await response.json().catch(() => null)) as
| { error?: string }
| null
throw new Error(
payload?.error || `GitHub analysis request failed (${response.status}).`
)
}
return r.json()
})

const text = await response.text().catch(() => "")
throw new Error(
text.trim()
? `GitHub analysis request failed (${response.status}): ${text.slice(0, 160)}`
: `GitHub analysis request failed (${response.status}).`
)
}

return response.json()
}

interface DashboardClientProps {
/** Set for a public lookup; omit to analyze the signed-in user. */
Expand Down
Loading