From de4507f4139709237cbff144b8edeac5d866a47e Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 26 Jun 2026 21:00:55 +0530 Subject: [PATCH 01/10] feat: enhance session management in chat page - Implemented session refresh logic in the fetchWithTurnCancellingRetry function to handle 401 errors more gracefully. - Added a new import for refreshSession utility to facilitate session renewal. --- .../[search_space_id]/new-chat/[[...chat_id]]/page.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index 9c3a7c617..407a9c53b 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -72,6 +72,7 @@ import { useThreadDetail, useThreadMessages } from "@/hooks/use-thread-queries"; import { getAgentFilesystemSelection } from "@/lib/agent-filesystem"; import { documentsApiService } from "@/lib/apis/documents-api.service"; import { getDesktopAccessToken } from "@/lib/auth-fetch"; +import { refreshSession } from "@/lib/auth-utils"; import { type ChatFlow, classifyChatError } from "@/lib/chat/chat-error-classifier"; import { tagPreAcceptSendFailure, toHttpResponseError } from "@/lib/chat/chat-request-errors"; import { getMentionDocKey } from "@/lib/chat/mention-doc-key"; @@ -688,11 +689,19 @@ export default function NewChatPage() { const fetchWithTurnCancellingRetry = useCallback(async (runFetch: () => Promise) => { const maxAttempts = 4; + let didRefreshAuth = false; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { const response = await runFetch(); if (response.ok) { return response; } + if (response.status === 401 && !didRefreshAuth) { + didRefreshAuth = true; + const refreshed = await refreshSession(); + if (refreshed) { + continue; + } + } const error = await toHttpResponseError(response); const withMeta = error as Error & { errorCode?: string; retryAfterMs?: number }; const isTurnCancelling = withMeta.errorCode === "TURN_CANCELLING"; From 23c128dd0d494f80cd9164eaff94ecc1148cb366 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 26 Jun 2026 21:24:44 +0530 Subject: [PATCH 02/10] feat: improve fetchZeroContext with enhanced session handling - Refactored fetchZeroContext to include a buildHeaders function for better header management. - Added a request function to handle 401 errors and refresh sessions as needed. - Improved overall session management for desktop authentication. --- .../components/providers/ZeroProvider.tsx | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/surfsense_web/components/providers/ZeroProvider.tsx b/surfsense_web/components/providers/ZeroProvider.tsx index 1a157a854..05ff1b4b1 100644 --- a/surfsense_web/components/providers/ZeroProvider.tsx +++ b/surfsense_web/components/providers/ZeroProvider.tsx @@ -31,21 +31,41 @@ function getCacheURL() { } async function fetchZeroContext(isDesktop: boolean): Promise { - const headers: HeadersInit = {}; - let desktopAuth: string | undefined; + const buildHeaders = async ( + forceRefresh = false + ): Promise<{ headers: HeadersInit; desktopAuth?: string } | null> => { + const headers: HeadersInit = {}; - if (isDesktop) { - const token = await getDesktopAccessToken(); - if (!token) return null; - desktopAuth = token; - headers.Authorization = `Bearer ${token}`; + if (isDesktop) { + const token = await getDesktopAccessToken({ forceRefresh }); + if (!token) return null; + headers.Authorization = `Bearer ${token}`; + return { headers, desktopAuth: token }; + } + + return { headers }; + }; + + const request = async (forceRefresh = false) => { + const auth = await buildHeaders(forceRefresh); + if (!auth) return null; + const response = await fetch(buildBackendUrl("/zero/context"), { + credentials: "include", + headers: auth.headers, + }); + return { response, desktopAuth: auth.desktopAuth }; + }; + + let result = await request(); + if (result?.response.status === 401) { + const refreshed = await refreshSession(); + if (refreshed) { + result = await request(true); + } } - const response = await fetch(buildBackendUrl("/zero/context"), { - credentials: "include", - headers, - }); - + if (!result) return null; + const { response, desktopAuth } = result; if (!response.ok) return null; return { From 013fae6eba142e72cb3851faf7183861e40ffd73 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 26 Jun 2026 21:25:00 +0530 Subject: [PATCH 03/10] feat: enhance session handling in useSession and auth-fetch - Introduced fetchSession function to streamline session fetching logic. - Updated useSession to handle 401 errors by refreshing the session when necessary. - Modified getDesktopAccessToken to accept options for forced token refresh, improving desktop authentication flow. --- surfsense_web/hooks/use-session.ts | 19 +++++++++++++++---- surfsense_web/lib/auth-fetch.ts | 12 +++++++++--- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/surfsense_web/hooks/use-session.ts b/surfsense_web/hooks/use-session.ts index 6bb10456f..0012e8763 100644 --- a/surfsense_web/hooks/use-session.ts +++ b/surfsense_web/hooks/use-session.ts @@ -1,6 +1,7 @@ "use client"; import { useCallback, useEffect, useState } from "react"; +import { refreshSession } from "@/lib/auth-utils"; import { buildBackendUrl } from "@/lib/env-config"; type SessionState = @@ -17,6 +18,13 @@ async function getSessionHeaders(): Promise { return token ? { Authorization: `Bearer ${token}` } : {}; } +async function fetchSession(): Promise { + return fetch(buildBackendUrl("/auth/session"), { + credentials: "include", + headers: await getSessionHeaders(), + }); +} + export function useSession() { const [state, setState] = useState({ status: "loading", @@ -26,10 +34,13 @@ export function useSession() { const refresh = useCallback(async () => { try { - const response = await fetch(buildBackendUrl("/auth/session"), { - credentials: "include", - headers: await getSessionHeaders(), - }); + let response = await fetchSession(); + if (response.status === 401) { + const refreshed = await refreshSession(); + if (refreshed) { + response = await fetchSession(); + } + } if (!response.ok) { setState({ status: "unauthenticated", diff --git a/surfsense_web/lib/auth-fetch.ts b/surfsense_web/lib/auth-fetch.ts index 20b236854..4f512c2ca 100644 --- a/surfsense_web/lib/auth-fetch.ts +++ b/surfsense_web/lib/auth-fetch.ts @@ -3,6 +3,10 @@ import { handleUnauthorized, isDesktopClient, refreshSession } from "@/lib/auth- let desktopAccessToken: string | null = null; let didSubscribeToDesktopAuth = false; +type DesktopAccessTokenOptions = { + forceRefresh?: boolean; +}; + function subscribeToDesktopAuth(): void { if (didSubscribeToDesktopAuth || typeof window === "undefined" || !window.electronAPI) { return; @@ -17,10 +21,12 @@ function subscribeToDesktopAuth(): void { }); } -export async function getDesktopAccessToken(): Promise { +export async function getDesktopAccessToken( + options: DesktopAccessTokenOptions = {} +): Promise { if (!isDesktopClient()) return null; subscribeToDesktopAuth(); - if (desktopAccessToken) return desktopAccessToken; + if (desktopAccessToken && !options.forceRefresh) return desktopAccessToken; const token = (await window.electronAPI?.getAccessToken?.()) || null; desktopAccessToken = token; return token; @@ -55,7 +61,7 @@ export async function authenticatedFetch( if (!skipRefresh) { const refreshed = await refreshSession(); if (refreshed) { - const newToken = await getDesktopAccessToken(); + const newToken = await getDesktopAccessToken({ forceRefresh: true }); return fetch(url, { ...fetchOptions, headers: { From ef9b5b42a837b84715ab091e31cc50d46d84720d Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 26 Jun 2026 21:25:27 +0530 Subject: [PATCH 04/10] feat: update JWT token lifetime and enhance header management in chat page - Increased ACCESS_TOKEN_LIFETIME_SECONDS from 30 minutes to 60 minutes for improved session duration. - Introduced getRequestHeadersWithCurrentDesktopAuth function to streamline authorization header management across fetch requests in the chat page. --- surfsense_backend/app/config/__init__.py | 2 +- .../new-chat/[[...chat_id]]/page.tsx | 40 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/surfsense_backend/app/config/__init__.py b/surfsense_backend/app/config/__init__.py index 47e529741..63de28bfc 100644 --- a/surfsense_backend/app/config/__init__.py +++ b/surfsense_backend/app/config/__init__.py @@ -916,7 +916,7 @@ class Config: # JWT Token Lifetimes ACCESS_TOKEN_LIFETIME_SECONDS = int( - os.getenv("ACCESS_TOKEN_LIFETIME_SECONDS", str(30 * 60)) # 30 minutes + os.getenv("ACCESS_TOKEN_LIFETIME_SECONDS", str(60 * 60)) # 60 minutes ) MIN_ISSUED_AT = int(os.getenv("MIN_ISSUED_AT", "0")) REFRESH_TOKEN_LIFETIME_SECONDS = int( diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index 407a9c53b..a9851cbbc 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -289,6 +289,16 @@ const TURN_CANCELLING_BACKOFF_FACTOR = 2; const TURN_CANCELLING_MAX_DELAY_MS = 1500; const RECENT_CANCEL_WINDOW_MS = 5_000; +async function getRequestHeadersWithCurrentDesktopAuth( + headers: Record = {} +): Promise> { + const token = await getDesktopAccessToken({ forceRefresh: true }); + return { + ...headers, + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }; +} + function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -941,13 +951,12 @@ export default function NewChatPage() { // Cancel ongoing request const cancelRun = useCallback(async () => { if (threadId) { - const token = await getDesktopAccessToken(); try { const response = await fetch( buildBackendUrl(`/api/v1/threads/${threadId}/cancel-active-turn`), { method: "POST", - headers: token ? { Authorization: `Bearer ${token}` } : undefined, + headers: await getRequestHeadersWithCurrentDesktopAuth(), credentials: "include", } ); @@ -995,8 +1004,6 @@ export default function NewChatPage() { if (!userQuery.trim() && userImages.length === 0) return; - const token = await getDesktopAccessToken(); - // Lazy thread creation: create thread on first message if it doesn't exist let currentThreadId = threadId; let isNewThread = false; @@ -1167,13 +1174,12 @@ export default function NewChatPage() { const hasConnectorIds = mentionPayload.connector_ids.length > 0; const hasThreadIds = mentionPayload.thread_ids.length > 0; - const response = await fetchWithTurnCancellingRetry(() => + const response = await fetchWithTurnCancellingRetry(async () => fetch(buildBackendUrl("/api/v1/new_chat"), { method: "POST", - headers: { + headers: await getRequestHeadersWithCurrentDesktopAuth({ "Content-Type": "application/json", - ...(token ? { Authorization: `Bearer ${token}` } : {}), - }, + }), credentials: "include", body: JSON.stringify({ chat_id: currentThreadId, @@ -1555,8 +1561,6 @@ export default function NewChatPage() { stagedDecisionsByInterruptIdRef.current.clear(); setIsRunning(true); - const token = await getDesktopAccessToken(); - const controller = new AbortController(); abortControllerRef.current = controller; @@ -1656,13 +1660,12 @@ export default function NewChatPage() { const selection = await getAgentFilesystemSelection(searchSpaceId, { localFilesystemEnabled, }); - const response = await fetchWithTurnCancellingRetry(() => + const response = await fetchWithTurnCancellingRetry(async () => fetch(buildBackendUrl(`/api/v1/threads/${resumeThreadId}/resume`), { method: "POST", - headers: { + headers: await getRequestHeadersWithCurrentDesktopAuth({ "Content-Type": "application/json", - ...(token ? { Authorization: `Bearer ${token}` } : {}), - }, + }), credentials: "include", body: JSON.stringify({ search_space_id: searchSpaceId, @@ -1995,8 +1998,6 @@ export default function NewChatPage() { abortControllerRef.current = null; } - const token = await getDesktopAccessToken(); - // Extract the original user query BEFORE removing messages (for reload mode) let userQueryToDisplay: string | undefined; let originalUserMessageContent: ThreadMessageLike["content"] | null = null; @@ -2113,13 +2114,12 @@ export default function NewChatPage() { requestBody.revert_actions = true; } } - const response = await fetchWithTurnCancellingRetry(() => + const response = await fetchWithTurnCancellingRetry(async () => fetch(getRegenerateUrl(threadId), { method: "POST", - headers: { + headers: await getRequestHeadersWithCurrentDesktopAuth({ "Content-Type": "application/json", - ...(token ? { Authorization: `Bearer ${token}` } : {}), - }, + }), credentials: "include", body: JSON.stringify(requestBody), signal: controller.signal, From 9fc93e5e82475ed07e79fbd16db6be329064b54c Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 26 Jun 2026 22:03:59 +0530 Subject: [PATCH 05/10] fix(auth): centralize session refresh retry --- surfsense_web/lib/auth-utils.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/surfsense_web/lib/auth-utils.ts b/surfsense_web/lib/auth-utils.ts index 47b2f043f..7372c8b66 100644 --- a/surfsense_web/lib/auth-utils.ts +++ b/surfsense_web/lib/auth-utils.ts @@ -188,9 +188,23 @@ async function doRefreshSession(): Promise { } } +let refreshPromise: Promise | null = null; + export async function refreshSession(): Promise { - if (typeof navigator !== "undefined" && "locks" in navigator) { - return navigator.locks.request("ss-token-refresh", () => doRefreshSession()); + if (refreshPromise) { + return refreshPromise; + } + + refreshPromise = (async () => { + if (typeof navigator !== "undefined" && "locks" in navigator) { + return navigator.locks.request("ss-token-refresh", () => doRefreshSession()); + } + return doRefreshSession(); + })(); + + try { + return await refreshPromise; + } finally { + refreshPromise = null; } - return doRefreshSession(); } From 4c257e912205019df38c3b2a44097b0b6b31e4e3 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 26 Jun 2026 22:04:23 +0530 Subject: [PATCH 06/10] refactor(auth): route session and zero context through shared fetch --- .../components/providers/ZeroProvider.tsx | 44 +++---------------- 1 file changed, 6 insertions(+), 38 deletions(-) diff --git a/surfsense_web/components/providers/ZeroProvider.tsx b/surfsense_web/components/providers/ZeroProvider.tsx index 05ff1b4b1..3f87ed007 100644 --- a/surfsense_web/components/providers/ZeroProvider.tsx +++ b/surfsense_web/components/providers/ZeroProvider.tsx @@ -8,7 +8,7 @@ import { import { usePathname } from "next/navigation"; import { useEffect, useMemo, useRef, useState } from "react"; import { useSession } from "@/hooks/use-session"; -import { getDesktopAccessToken } from "@/lib/auth-fetch"; +import { authenticatedFetch, getDesktopAccessToken } from "@/lib/auth-fetch"; import { handleUnauthorized, isPublicRoute, refreshSession } from "@/lib/auth-utils"; import { buildBackendUrl } from "@/lib/env-config"; import type { Context } from "@/types/zero"; @@ -31,46 +31,14 @@ function getCacheURL() { } async function fetchZeroContext(isDesktop: boolean): Promise { - const buildHeaders = async ( - forceRefresh = false - ): Promise<{ headers: HeadersInit; desktopAuth?: string } | null> => { - const headers: HeadersInit = {}; - - if (isDesktop) { - const token = await getDesktopAccessToken({ forceRefresh }); - if (!token) return null; - headers.Authorization = `Bearer ${token}`; - return { headers, desktopAuth: token }; - } - - return { headers }; - }; - - const request = async (forceRefresh = false) => { - const auth = await buildHeaders(forceRefresh); - if (!auth) return null; - const response = await fetch(buildBackendUrl("/zero/context"), { - credentials: "include", - headers: auth.headers, - }); - return { response, desktopAuth: auth.desktopAuth }; - }; - - let result = await request(); - if (result?.response.status === 401) { - const refreshed = await refreshSession(); - if (refreshed) { - result = await request(true); - } - } - - if (!result) return null; - const { response, desktopAuth } = result; + const response = await authenticatedFetch(buildBackendUrl("/zero/context"), { + skipAuthRedirect: true, + }); if (!response.ok) return null; return { context: (await response.json()) as ZeroContext, - desktopAuth, + desktopAuth: isDesktop ? (await getDesktopAccessToken()) || undefined : undefined, }; } @@ -126,7 +94,7 @@ function ZeroAuthSync({ isDesktop }: { isDesktop: boolean }) { } if (isDesktop) { - const newToken = await getDesktopAccessToken(); + const newToken = await getDesktopAccessToken({ forceRefresh: true }); if (!newToken) { handleUnauthorized(); return; From 652a25be37a9f733fbd149af886d63b7aac973b7 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 26 Jun 2026 22:04:33 +0530 Subject: [PATCH 07/10] refactor(chat): use shared authenticated fetch for chat requests --- .../new-chat/[[...chat_id]]/page.tsx | 52 +++++-------------- 1 file changed, 14 insertions(+), 38 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index a9851cbbc..64971c3bb 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -71,8 +71,7 @@ import { useMessagesSync } from "@/hooks/use-messages-sync"; import { useThreadDetail, useThreadMessages } from "@/hooks/use-thread-queries"; import { getAgentFilesystemSelection } from "@/lib/agent-filesystem"; import { documentsApiService } from "@/lib/apis/documents-api.service"; -import { getDesktopAccessToken } from "@/lib/auth-fetch"; -import { refreshSession } from "@/lib/auth-utils"; +import { authenticatedFetch } from "@/lib/auth-fetch"; import { type ChatFlow, classifyChatError } from "@/lib/chat/chat-error-classifier"; import { tagPreAcceptSendFailure, toHttpResponseError } from "@/lib/chat/chat-request-errors"; import { getMentionDocKey } from "@/lib/chat/mention-doc-key"; @@ -289,16 +288,6 @@ const TURN_CANCELLING_BACKOFF_FACTOR = 2; const TURN_CANCELLING_MAX_DELAY_MS = 1500; const RECENT_CANCEL_WINDOW_MS = 5_000; -async function getRequestHeadersWithCurrentDesktopAuth( - headers: Record = {} -): Promise> { - const token = await getDesktopAccessToken({ forceRefresh: true }); - return { - ...headers, - ...(token ? { Authorization: `Bearer ${token}` } : {}), - }; -} - function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -699,19 +688,11 @@ export default function NewChatPage() { const fetchWithTurnCancellingRetry = useCallback(async (runFetch: () => Promise) => { const maxAttempts = 4; - let didRefreshAuth = false; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { const response = await runFetch(); if (response.ok) { return response; } - if (response.status === 401 && !didRefreshAuth) { - didRefreshAuth = true; - const refreshed = await refreshSession(); - if (refreshed) { - continue; - } - } const error = await toHttpResponseError(response); const withMeta = error as Error & { errorCode?: string; retryAfterMs?: number }; const isTurnCancelling = withMeta.errorCode === "TURN_CANCELLING"; @@ -952,12 +933,10 @@ export default function NewChatPage() { const cancelRun = useCallback(async () => { if (threadId) { try { - const response = await fetch( + const response = await authenticatedFetch( buildBackendUrl(`/api/v1/threads/${threadId}/cancel-active-turn`), { method: "POST", - headers: await getRequestHeadersWithCurrentDesktopAuth(), - credentials: "include", } ); if (response.ok) { @@ -1174,13 +1153,12 @@ export default function NewChatPage() { const hasConnectorIds = mentionPayload.connector_ids.length > 0; const hasThreadIds = mentionPayload.thread_ids.length > 0; - const response = await fetchWithTurnCancellingRetry(async () => - fetch(buildBackendUrl("/api/v1/new_chat"), { + const response = await fetchWithTurnCancellingRetry(() => + authenticatedFetch(buildBackendUrl("/api/v1/new_chat"), { method: "POST", - headers: await getRequestHeadersWithCurrentDesktopAuth({ + headers: { "Content-Type": "application/json", - }), - credentials: "include", + }, body: JSON.stringify({ chat_id: currentThreadId, user_query: userQuery.trim(), @@ -1660,13 +1638,12 @@ export default function NewChatPage() { const selection = await getAgentFilesystemSelection(searchSpaceId, { localFilesystemEnabled, }); - const response = await fetchWithTurnCancellingRetry(async () => - fetch(buildBackendUrl(`/api/v1/threads/${resumeThreadId}/resume`), { + const response = await fetchWithTurnCancellingRetry(() => + authenticatedFetch(buildBackendUrl(`/api/v1/threads/${resumeThreadId}/resume`), { method: "POST", - headers: await getRequestHeadersWithCurrentDesktopAuth({ + headers: { "Content-Type": "application/json", - }), - credentials: "include", + }, body: JSON.stringify({ search_space_id: searchSpaceId, decisions, @@ -2114,13 +2091,12 @@ export default function NewChatPage() { requestBody.revert_actions = true; } } - const response = await fetchWithTurnCancellingRetry(async () => - fetch(getRegenerateUrl(threadId), { + const response = await fetchWithTurnCancellingRetry(() => + authenticatedFetch(getRegenerateUrl(threadId), { method: "POST", - headers: await getRequestHeadersWithCurrentDesktopAuth({ + headers: { "Content-Type": "application/json", - }), - credentials: "include", + }, body: JSON.stringify(requestBody), signal: controller.signal, }) From 4b6bcaeb1bd3db5c7e9ddfee5aba95e025c965ae Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 26 Jun 2026 22:04:59 +0530 Subject: [PATCH 08/10] refactor(auth): reuse desktop token cache in API clients --- surfsense_web/components/tool-ui/sandbox-execute.tsx | 8 ++------ surfsense_web/lib/apis/base-api.service.ts | 12 +++++------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/surfsense_web/components/tool-ui/sandbox-execute.tsx b/surfsense_web/components/tool-ui/sandbox-execute.tsx index 535968908..2bac3e1ca 100644 --- a/surfsense_web/components/tool-ui/sandbox-execute.tsx +++ b/surfsense_web/components/tool-ui/sandbox-execute.tsx @@ -16,7 +16,7 @@ import { z } from "zod"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; -import { getDesktopAccessToken } from "@/lib/auth-fetch"; +import { authenticatedFetch } from "@/lib/auth-fetch"; import { buildBackendUrl } from "@/lib/env-config"; import { cn } from "@/lib/utils"; @@ -157,14 +157,10 @@ function truncateCommand(command: string, maxLen = 80): string { // ============================================================================ async function downloadSandboxFile(threadId: string, filePath: string, fileName: string) { - const token = await getDesktopAccessToken(); const url = buildBackendUrl(`/api/v1/threads/${threadId}/sandbox/download`, { path: filePath, }); - const res = await fetch(url, { - headers: token ? { Authorization: `Bearer ${token}` } : undefined, - credentials: "include", - }); + const res = await authenticatedFetch(url); if (!res.ok) { throw new Error(`Download failed: ${res.statusText}`); } diff --git a/surfsense_web/lib/apis/base-api.service.ts b/surfsense_web/lib/apis/base-api.service.ts index 5afb291ba..0cc5224e2 100644 --- a/surfsense_web/lib/apis/base-api.service.ts +++ b/surfsense_web/lib/apis/base-api.service.ts @@ -1,4 +1,5 @@ import type { ZodType } from "zod"; +import { getDesktopAccessToken } from "@/lib/auth-fetch"; import { buildBackendUrl } from "@/lib/env-config"; import { getClientPlatform } from "../agent-filesystem"; import { handleUnauthorized, refreshSession } from "../auth-utils"; @@ -59,11 +60,6 @@ class BaseApiService { return typeof window !== "undefined" && !!window.electronAPI; } - private async getDesktopAccessToken(): Promise { - if (!this.isDesktopClient) return ""; - return (await window.electronAPI?.getAccessToken?.()) || ""; - } - async request( url: string, responseSchema?: ZodType, @@ -90,7 +86,7 @@ class BaseApiService { this.noAuthPrefixes.some((prefix) => url.startsWith(prefix)) || /^\/api\/v1\/invites\/[^/]+\/info$/.test(url); const desktopAccessToken = - this.isDesktopClient && !isNoAuthEndpoint ? await this.getDesktopAccessToken() : ""; + this.isDesktopClient && !isNoAuthEndpoint ? (await getDesktopAccessToken()) || "" : ""; const defaultOptions: RequestOptions = { headers: { ...(desktopAccessToken ? { Authorization: `Bearer ${desktopAccessToken}` } : {}), @@ -174,7 +170,9 @@ class BaseApiService { } else if (!isNoAuthEndpoint && !isRefreshRetryBlocked(refreshRetryKey)) { const refreshed = await refreshSession(); if (refreshed) { - const newToken = this.isDesktopClient ? await this.getDesktopAccessToken() : ""; + const newToken = this.isDesktopClient + ? (await getDesktopAccessToken({ forceRefresh: true })) || "" + : ""; return this.request(url, responseSchema, { ...mergedOptions, headers: { From 3ce759f1d6b267ecaf9cd61e7062df0b881bb69b Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 26 Jun 2026 22:05:11 +0530 Subject: [PATCH 09/10] refactor(auth): streamline session handling with authenticatedFetch --- surfsense_web/hooks/use-session.ts | 28 ++--------- surfsense_web/lib/auth-fetch.ts | 79 ++++++++++++++++++++---------- 2 files changed, 56 insertions(+), 51 deletions(-) diff --git a/surfsense_web/hooks/use-session.ts b/surfsense_web/hooks/use-session.ts index 0012e8763..207475eae 100644 --- a/surfsense_web/hooks/use-session.ts +++ b/surfsense_web/hooks/use-session.ts @@ -1,7 +1,7 @@ "use client"; import { useCallback, useEffect, useState } from "react"; -import { refreshSession } from "@/lib/auth-utils"; +import { authenticatedFetch } from "@/lib/auth-fetch"; import { buildBackendUrl } from "@/lib/env-config"; type SessionState = @@ -9,22 +9,6 @@ type SessionState = | { status: "authenticated"; authenticated: true; accessExpiresAt: number | null } | { status: "unauthenticated"; authenticated: false; accessExpiresAt: null }; -async function getSessionHeaders(): Promise { - if (typeof window === "undefined" || !window.electronAPI?.getAccessToken) { - return {}; - } - - const token = await window.electronAPI.getAccessToken(); - return token ? { Authorization: `Bearer ${token}` } : {}; -} - -async function fetchSession(): Promise { - return fetch(buildBackendUrl("/auth/session"), { - credentials: "include", - headers: await getSessionHeaders(), - }); -} - export function useSession() { const [state, setState] = useState({ status: "loading", @@ -34,13 +18,9 @@ export function useSession() { const refresh = useCallback(async () => { try { - let response = await fetchSession(); - if (response.status === 401) { - const refreshed = await refreshSession(); - if (refreshed) { - response = await fetchSession(); - } - } + const response = await authenticatedFetch(buildBackendUrl("/auth/session"), { + skipAuthRedirect: true, + }); if (!response.ok) { setState({ status: "unauthenticated", diff --git a/surfsense_web/lib/auth-fetch.ts b/surfsense_web/lib/auth-fetch.ts index 4f512c2ca..a79777825 100644 --- a/surfsense_web/lib/auth-fetch.ts +++ b/surfsense_web/lib/auth-fetch.ts @@ -7,6 +7,12 @@ type DesktopAccessTokenOptions = { forceRefresh?: boolean; }; +type AuthenticatedFetchOptions = RequestInit & { + skipAuthRedirect?: boolean; + skipRefresh?: boolean; + forceDesktopTokenRefresh?: boolean; +}; + function subscribeToDesktopAuth(): void { if (didSubscribeToDesktopAuth || typeof window === "undefined" || !window.electronAPI) { return; @@ -40,42 +46,61 @@ export function getAuthHeaders(additionalHeaders?: Record): Reco }; } +async function fetchWithAuth( + url: string, + options: RequestInit, + { forceDesktopTokenRefresh = false }: { forceDesktopTokenRefresh?: boolean } = {} +): Promise { + const headers = new Headers(options.headers); + const token = await getDesktopAccessToken({ forceRefresh: forceDesktopTokenRefresh }); + if (token) { + headers.set("Authorization", `Bearer ${token}`); + } + + return fetch(url, { + ...options, + headers, + credentials: options.credentials ?? "include", + }); +} + export async function authenticatedFetch( url: string, - options?: RequestInit & { skipAuthRedirect?: boolean; skipRefresh?: boolean } + options: AuthenticatedFetchOptions = {} ): Promise { - const { skipAuthRedirect = false, skipRefresh = false, ...fetchOptions } = options || {}; - const token = await getDesktopAccessToken(); - const headers = { - ...(fetchOptions.headers as Record), - ...(token ? { Authorization: `Bearer ${token}` } : {}), - }; + const { + skipAuthRedirect = false, + skipRefresh = false, + forceDesktopTokenRefresh = false, + ...fetchOptions + } = options; - const response = await fetch(url, { - ...fetchOptions, - headers, - credentials: "include", + const response = await fetchWithAuth(url, fetchOptions, { + forceDesktopTokenRefresh, }); - if (response.status === 401 && !skipAuthRedirect) { - if (!skipRefresh) { - const refreshed = await refreshSession(); - if (refreshed) { - const newToken = await getDesktopAccessToken({ forceRefresh: true }); - return fetch(url, { - ...fetchOptions, - headers: { - ...(fetchOptions.headers as Record), - ...(newToken ? { Authorization: `Bearer ${newToken}` } : {}), - }, - credentials: "include", - }); - } - } + if (response.status !== 401) { + return response; + } + let unauthorizedResponse = response; + if (!skipRefresh) { + const refreshed = await refreshSession(); + if (refreshed) { + const retryResponse = await fetchWithAuth(url, fetchOptions, { + forceDesktopTokenRefresh: true, + }); + if (retryResponse.status !== 401) { + return retryResponse; + } + unauthorizedResponse = retryResponse; + } + } + + if (!skipAuthRedirect) { handleUnauthorized(); throw new Error("Unauthorized: Redirecting to login page"); } - return response; + return unauthorizedResponse; } From f14c471a0315a8f24caff66cb87a75183fc6d914 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 26 Jun 2026 22:15:48 +0530 Subject: [PATCH 10/10] refactor(ZeroProvider): simplify route handling by removing desktop check for public routes --- surfsense_web/components/providers/ZeroProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/surfsense_web/components/providers/ZeroProvider.tsx b/surfsense_web/components/providers/ZeroProvider.tsx index 3f87ed007..cfb8fe586 100644 --- a/surfsense_web/components/providers/ZeroProvider.tsx +++ b/surfsense_web/components/providers/ZeroProvider.tsx @@ -250,7 +250,7 @@ export function ZeroProvider({ children }: { children: React.ReactNode }) { const pathname = usePathname(); const isDesktop = typeof window !== "undefined" && !!window.electronAPI; - if (!isDesktop && isPublicRoute(pathname)) { + if (isPublicRoute(pathname)) { return <>{children}; }