diff --git a/web/lib/auth-api.ts b/web/lib/auth-api.ts index c6a2fdf5..dcddaed7 100644 --- a/web/lib/auth-api.ts +++ b/web/lib/auth-api.ts @@ -49,20 +49,42 @@ export interface ApiResponse { } const TOKEN_KEY = "auth_token"; +const TOKEN_COOKIE = "deepwiki_token"; +const TOKEN_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; // 7 days export function getToken(): string | null { if (typeof window === "undefined") return null; return localStorage.getItem(TOKEN_KEY); } +/** + * Read JWT from cookie during SSR. Uses require() to avoid build errors + * when this module is imported in client components. + * Must be async because cookies() returns a Promise in Next.js 15+. + */ +export async function getServerToken(): Promise { + if (typeof window !== "undefined") return null; + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { cookies } = require("next/headers"); + const cookieStore = await cookies(); + return cookieStore.get(TOKEN_COOKIE)?.value ?? null; + } catch { + return null; + } +} + export function setToken(token: string): void { if (typeof window === "undefined") return; localStorage.setItem(TOKEN_KEY, token); + // Also set as cookie for SSR access to private repo pages + document.cookie = `${TOKEN_COOKIE}=${token}; path=/; max-age=${TOKEN_COOKIE_MAX_AGE}; SameSite=Lax`; } export function removeToken(): void { if (typeof window === "undefined") return; localStorage.removeItem(TOKEN_KEY); + document.cookie = `${TOKEN_COOKIE}=; path=/; max-age=0`; } export async function login(request: LoginRequest): Promise { diff --git a/web/lib/repository-api.ts b/web/lib/repository-api.ts index ad015ef2..9045f57e 100644 --- a/web/lib/repository-api.ts +++ b/web/lib/repository-api.ts @@ -13,6 +13,18 @@ import type { MindMapResponse } from "@/types/repository"; import { api, buildApiUrl } from "./api-client"; +import { getServerToken } from "./auth-api"; + +/** + * Returns Authorization header for SSR fetches if a JWT cookie is present. + * On the client side (window exists), getServerToken() returns null so + * this returns empty headers -- client auth goes through apiClient instead. + * Async because cookies() is async in Next.js 15+. + */ +async function getSSRAuthHeaders(): Promise { + const token = await getServerToken(); + return token ? { Authorization: `Bearer ${token}` } : {}; +} function encodePathSegments(path: string) { return path @@ -26,7 +38,7 @@ export async function fetchRepoBranches(owner: string, repo: string) { `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/branches`, ); - const response = await fetch(url, { cache: "no-store" }); + const response = await fetch(url, { cache: "no-store", headers: await getSSRAuthHeaders() }); if (!response.ok) { throw new Error("Failed to fetch repository branches"); @@ -44,7 +56,7 @@ export async function fetchGitBranches(gitUrl: string): Promise