Skip to content
Open
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
22 changes: 22 additions & 0 deletions web/lib/auth-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,42 @@ export interface ApiResponse<T> {
}

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<string | null> {
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<LoginResponse> {
Expand Down
30 changes: 21 additions & 9 deletions web/lib/repository-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HeadersInit> {
const token = await getServerToken();
return token ? { Authorization: `Bearer ${token}` } : {};
}

function encodePathSegments(path: string) {
return path
Expand All @@ -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");
Expand All @@ -44,7 +56,7 @@ export async function fetchGitBranches(gitUrl: string): Promise<GitBranchesRespo

const url = buildApiUrl(`/api/v1/repositories/branches?${params.toString()}`);

const response = await fetch(url, { cache: "no-store" });
const response = await fetch(url, { cache: "no-store", headers: await getSSRAuthHeaders() });

if (!response.ok) {
return { branches: [], defaultBranch: null, isSupported: false };
Expand All @@ -63,7 +75,7 @@ export async function fetchRepoTree(owner: string, repo: string, branch?: string
`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/tree${queryString ? `?${queryString}` : ""}`,
);

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 tree");
Expand All @@ -83,7 +95,7 @@ export async function fetchRepoDoc(owner: string, repo: string, slug: string, br
`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/docs/${encodedSlug}${queryString ? `?${queryString}` : ""}`,
);

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 doc");
Expand Down Expand Up @@ -133,7 +145,7 @@ export async function fetchRepositoryList(params?: {
const queryString = searchParams.toString();
const url = buildApiUrl(`/api/v1/repositories/list${queryString ? `?${queryString}` : ""}`);

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 list");
Expand Down Expand Up @@ -161,7 +173,7 @@ export async function fetchRepoStatus(owner: string, repo: string): Promise<Repo
`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/tree`,
);

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 status");
Expand Down Expand Up @@ -191,7 +203,7 @@ export async function fetchProcessingLogs(
`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/processing-logs${queryString ? `?${queryString}` : ""}`
);

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 processing logs");
Expand All @@ -212,7 +224,7 @@ export async function checkGitHubRepo(
`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/check`
);

const response = await fetch(url, { cache: "no-store" });
const response = await fetch(url, { cache: "no-store", headers: await getSSRAuthHeaders() });

if (!response.ok) {
return {
Expand Down Expand Up @@ -265,7 +277,7 @@ export async function fetchMindMap(
`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/mindmap${queryString ? `?${queryString}` : ""}`
);

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 mind map");
Expand Down