From 3f69bfd5e4a397bfe2621d22c98348d4b5635223 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:51:25 +0530 Subject: [PATCH] refactor(web): replace instances of BACKEND_URL with buildBackendUrl for improved URL handling --- .../app/(home)/login/GoogleLoginButton.tsx | 4 +-- .../components/MessagingChannelsContent.tsx | 36 +++++++++++-------- surfsense_web/app/desktop/login/page.tsx | 4 +-- .../components/obsidian-connect-form.tsx | 1 - .../components/circleback-config.tsx | 6 ++-- .../hooks/use-connector-dialog.ts | 6 ++-- .../documents/download-original-button.tsx | 4 +-- .../components/editor-panel/memory.ts | 10 ++---- .../components/free-chat/anonymous-chat.tsx | 4 +-- .../components/free-chat/free-chat-page.tsx | 4 +-- .../layout/ui/sidebar/DocumentsSidebar.tsx | 22 +++++++----- .../components/report-panel/report-panel.tsx | 12 ++++--- .../settings/general-settings-manager.tsx | 4 +-- .../components/tool-ui/generate-resume.tsx | 4 +-- .../components/tool-ui/podcast/player.tsx | 4 +-- .../components/tool-ui/sandbox-execute.tsx | 6 ++-- .../generate-video-presentation.tsx | 7 ++-- surfsense_web/eslint.config.mjs | 33 ++++++++--------- .../lib/apis/anonymous-chat-api.service.ts | 12 ++----- surfsense_web/lib/auth-utils.ts | 6 ++-- surfsense_web/lib/chat/thread-persistence.ts | 4 +-- 21 files changed, 98 insertions(+), 95 deletions(-) diff --git a/surfsense_web/app/(home)/login/GoogleLoginButton.tsx b/surfsense_web/app/(home)/login/GoogleLoginButton.tsx index 1c91f8115..a9e1b553e 100644 --- a/surfsense_web/app/(home)/login/GoogleLoginButton.tsx +++ b/surfsense_web/app/(home)/login/GoogleLoginButton.tsx @@ -3,7 +3,7 @@ import { useTranslations } from "next-intl"; import { useState } from "react"; import { Logo } from "@/components/Logo"; import { Button } from "@/components/ui/button"; -import { BACKEND_URL } from "@/lib/env-config"; +import { buildBackendUrl } from "@/lib/env-config"; import { trackLoginAttempt } from "@/lib/posthog/events"; import { AmbientBackground } from "./AmbientBackground"; @@ -51,7 +51,7 @@ export function GoogleLoginButton() { // cross-origin fetch requests may not be sent on subsequent redirects. // The authorize-redirect endpoint does a server-side redirect to Google // and sets the CSRF cookie properly for same-site context. - window.location.href = `${BACKEND_URL}/auth/google/authorize-redirect`; + window.location.href = buildBackendUrl("/auth/google/authorize-redirect"); }; return (
diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MessagingChannelsContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MessagingChannelsContent.tsx index b0cb6699c..138a2aad1 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MessagingChannelsContent.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MessagingChannelsContent.tsx @@ -19,7 +19,7 @@ import { Skeleton } from "@/components/ui/skeleton"; import type { SearchSpace } from "@/contracts/types/search-space.types"; import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service"; import { authenticatedFetch } from "@/lib/auth-utils"; -import { BACKEND_URL } from "@/lib/env-config"; +import { buildBackendUrl } from "@/lib/env-config"; import { cn } from "@/lib/utils"; type GatewayConnection = { @@ -82,13 +82,14 @@ export function MessagingChannelsContent() { const discordGatewayEnabled = gatewayConfig?.discord_enabled ?? false; const fetchConnections = useCallback(async (platform?: GatewayPlatform) => { - const query = platform ? `?platform=${encodeURIComponent(platform)}` : ""; - const res = await authenticatedFetch(`${BACKEND_URL}/api/v1/gateway/connections${query}`); + const res = await authenticatedFetch( + buildBackendUrl("/api/v1/gateway/connections", platform ? { platform } : undefined) + ); return (await res.json()) as GatewayConnection[]; }, []); const fetchGatewayConfig = useCallback(async () => { - const res = await authenticatedFetch(`${BACKEND_URL}/api/v1/gateway/config`); + const res = await authenticatedFetch(buildBackendUrl("/api/v1/gateway/config")); return (await res.json()) as GatewayConfig; }, []); @@ -125,7 +126,9 @@ export function MessagingChannelsContent() { const refreshBaileysHealth = useCallback(async () => { if (whatsappMode !== "baileys") return; - const res = await authenticatedFetch(`${BACKEND_URL}/api/v1/gateway/whatsapp/baileys/health`); + const res = await authenticatedFetch( + buildBackendUrl("/api/v1/gateway/whatsapp/baileys/health") + ); if (!res.ok) return; const data = (await res.json()) as BaileysHealth; setBaileysHealth(data); @@ -136,7 +139,7 @@ export function MessagingChannelsContent() { }, [refreshBaileysHealth]); async function startPairing(platform: PairingPlatform) { - const res = await authenticatedFetch(`${BACKEND_URL}/api/v1/gateway/bindings/start`, { + const res = await authenticatedFetch(buildBackendUrl("/api/v1/gateway/bindings/start"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ platform, search_space_id: searchSpaceId }), @@ -148,7 +151,7 @@ export function MessagingChannelsContent() { async function installSlackGateway() { const res = await authenticatedFetch( - `${BACKEND_URL}/api/v1/gateway/slack/install?search_space_id=${searchSpaceId}` + buildBackendUrl("/api/v1/gateway/slack/install", { search_space_id: searchSpaceId }) ); if (!res.ok) return; const data = (await res.json()) as { auth_url?: string }; @@ -159,7 +162,7 @@ export function MessagingChannelsContent() { async function installDiscordGateway() { const res = await authenticatedFetch( - `${BACKEND_URL}/api/v1/gateway/discord/install?search_space_id=${searchSpaceId}` + buildBackendUrl("/api/v1/gateway/discord/install", { search_space_id: searchSpaceId }) ); if (!res.ok) return; const data = (await res.json()) as { auth_url?: string }; @@ -181,8 +184,8 @@ export function MessagingChannelsContent() { async function revoke(connection: GatewayConnection) { const url = connection.route_type === "account" && connection.account_id - ? `${BACKEND_URL}/api/v1/gateway/accounts/${connection.account_id}` - : `${BACKEND_URL}/api/v1/gateway/bindings/${connection.id}`; + ? buildBackendUrl(`/api/v1/gateway/accounts/${connection.account_id}`) + : buildBackendUrl(`/api/v1/gateway/bindings/${connection.id}`); await authenticatedFetch(url, { method: "DELETE", }); @@ -205,8 +208,8 @@ export function MessagingChannelsContent() { ); const url = connection.route_type === "account" && connection.account_id - ? `${BACKEND_URL}/api/v1/gateway/accounts/${connection.account_id}/search-space` - : `${BACKEND_URL}/api/v1/gateway/bindings/${connection.id}/search-space`; + ? buildBackendUrl(`/api/v1/gateway/accounts/${connection.account_id}/search-space`) + : buildBackendUrl(`/api/v1/gateway/bindings/${connection.id}/search-space`); const res = await authenticatedFetch(url, { method: "PATCH", headers: { "Content-Type": "application/json" }, @@ -222,9 +225,12 @@ export function MessagingChannelsContent() { } async function resume(connection: GatewayConnection) { - await authenticatedFetch(`${BACKEND_URL}/api/v1/gateway/bindings/${connection.id}/resume`, { - method: "POST", - }); + await authenticatedFetch( + buildBackendUrl(`/api/v1/gateway/bindings/${connection.id}/resume`), + { + method: "POST", + } + ); await refreshPlatform(connection.platform as GatewayPlatform); } diff --git a/surfsense_web/app/desktop/login/page.tsx b/surfsense_web/app/desktop/login/page.tsx index 399da7434..0d91588e1 100644 --- a/surfsense_web/app/desktop/login/page.tsx +++ b/surfsense_web/app/desktop/login/page.tsx @@ -18,7 +18,7 @@ import { Spinner } from "@/components/ui/spinner"; import { useElectronAPI } from "@/hooks/use-platform"; import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service"; import { setBearerToken } from "@/lib/auth-utils"; -import { BACKEND_URL } from "@/lib/env-config"; +import { buildBackendUrl } from "@/lib/env-config"; type ShortcutKey = "generalAssist" | "quickAsk" | "screenshotAssist"; type ShortcutMap = typeof DEFAULT_SHORTCUTS; @@ -240,7 +240,7 @@ export default function DesktopLoginPage() { const handleGoogleLogin = () => { if (isGoogleRedirecting) return; setIsGoogleRedirecting(true); - window.location.href = `${BACKEND_URL}/auth/google/authorize-redirect`; + window.location.href = buildBackendUrl("/auth/google/authorize-redirect"); }; const autoSetSearchSpace = async () => { diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/obsidian-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/obsidian-connect-form.tsx index a9231d846..695e97d7b 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/obsidian-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/obsidian-connect-form.tsx @@ -6,7 +6,6 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { EnumConnectorName } from "@/contracts/enums/connector"; import { useApiKey } from "@/hooks/use-api-key"; -import { BACKEND_URL } from "@/lib/env-config"; import { getConnectorBenefits } from "../connector-benefits"; import type { ConnectFormProps } from "../index"; diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/circleback-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/circleback-config.tsx index 8547f218d..283c052cb 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/circleback-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/circleback-config.tsx @@ -9,7 +9,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { authenticatedFetch } from "@/lib/auth-utils"; -import { BACKEND_URL } from "@/lib/env-config"; +import { buildBackendUrl } from "@/lib/env-config"; import type { ConnectorConfigProps } from "../index"; export interface CirclebackConfigProps extends ConnectorConfigProps { onNameChange?: (name: string) => void; @@ -42,12 +42,10 @@ export const CirclebackConfig: FC = ({ connector, onNameC const doFetch = async () => { if (!connector.search_space_id) return; - const baseUrl = BACKEND_URL; - setIsLoading(true); try { const response = await authenticatedFetch( - `${baseUrl}/api/v1/webhooks/circleback/${connector.search_space_id}/info`, + buildBackendUrl(`/api/v1/webhooks/circleback/${connector.search_space_id}/info`), { signal: controller.signal } ); if (controller.signal.aborted) return; diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts index 45c174d74..2f10152b8 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts @@ -16,7 +16,7 @@ import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import { searchSourceConnector } from "@/contracts/types/connector.types"; import { OAUTH_RESULT_COOKIE, parseOAuthCallbackResult } from "@/contracts/types/oauth.types"; import { authenticatedFetch } from "@/lib/auth-utils"; -import { BACKEND_URL } from "@/lib/env-config"; +import { buildBackendUrl } from "@/lib/env-config"; import { trackConnectorConnected, trackConnectorDeleted, @@ -351,9 +351,7 @@ export const useConnectorDialog = () => { trackConnectorSetupStarted(Number(searchSpaceId), connector.connectorType, "oauth_click"); try { - // Check if authEndpoint already has query parameters - const separator = connector.authEndpoint.includes("?") ? "&" : "?"; - const url = `${BACKEND_URL}${connector.authEndpoint}${separator}space_id=${searchSpaceId}`; + const url = buildBackendUrl(connector.authEndpoint, { space_id: searchSpaceId }); const response = await authenticatedFetch(url, { method: "GET" }); diff --git a/surfsense_web/components/documents/download-original-button.tsx b/surfsense_web/components/documents/download-original-button.tsx index b79b289b4..e04ead89a 100644 --- a/surfsense_web/components/documents/download-original-button.tsx +++ b/surfsense_web/components/documents/download-original-button.tsx @@ -7,7 +7,7 @@ import { Button } from "@/components/ui/button"; import { Spinner } from "@/components/ui/spinner"; import { documentsApiService } from "@/lib/apis/documents-api.service"; import { authenticatedFetch } from "@/lib/auth-utils"; -import { BACKEND_URL } from "@/lib/env-config"; +import { buildBackendUrl } from "@/lib/env-config"; interface DownloadOriginalButtonProps { documentId: number; @@ -41,7 +41,7 @@ export function DownloadOriginalButton({ documentId }: DownloadOriginalButtonPro setDownloading(true); try { const response = await authenticatedFetch( - `${BACKEND_URL}/api/v1/documents/${documentId}/download-original`, + buildBackendUrl(`/api/v1/documents/${documentId}/download-original`), { method: "GET" } ); if (!response.ok) throw new Error("Download failed"); diff --git a/surfsense_web/components/editor-panel/memory.ts b/surfsense_web/components/editor-panel/memory.ts index 7f939772c..1beb977a6 100644 --- a/surfsense_web/components/editor-panel/memory.ts +++ b/surfsense_web/components/editor-panel/memory.ts @@ -1,7 +1,7 @@ "use client"; import { authenticatedFetch } from "@/lib/auth-utils"; -import { BACKEND_URL } from "@/lib/env-config"; +import { buildBackendUrl } from "@/lib/env-config"; export type MemoryScope = "user" | "team"; @@ -30,10 +30,6 @@ function getMemoryPath(scope: MemoryScope, searchSpaceId?: number | null) { return `/api/v1/searchspaces/${searchSpaceId}/memory`; } -function getBackendUrl(path: string) { - return `${BACKEND_URL}${path}`; -} - export function getMemoryLimitState(length: number, limits?: MemoryLimits | null) { if (!limits) { return { @@ -66,7 +62,7 @@ export async function fetchMemoryEditorDocument({ title?: string | null; signal?: AbortSignal; }) { - const response = await authenticatedFetch(getBackendUrl(getMemoryPath(scope, searchSpaceId)), { + const response = await authenticatedFetch(buildBackendUrl(getMemoryPath(scope, searchSpaceId)), { method: "GET", signal, }); @@ -98,7 +94,7 @@ export async function saveMemoryMarkdown({ searchSpaceId?: number | null; markdown: string; }) { - const response = await authenticatedFetch(getBackendUrl(getMemoryPath(scope, searchSpaceId)), { + const response = await authenticatedFetch(buildBackendUrl(getMemoryPath(scope, searchSpaceId)), { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ memory_md: markdown }), diff --git a/surfsense_web/components/free-chat/anonymous-chat.tsx b/surfsense_web/components/free-chat/anonymous-chat.tsx index 0829efba2..e3b8273bc 100644 --- a/surfsense_web/components/free-chat/anonymous-chat.tsx +++ b/surfsense_web/components/free-chat/anonymous-chat.tsx @@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button"; import type { AnonModel, AnonQuotaResponse } from "@/contracts/types/anonymous-chat.types"; import { anonymousChatApiService } from "@/lib/apis/anonymous-chat-api.service"; import { readSSEStream } from "@/lib/chat/streaming-state"; -import { BACKEND_URL } from "@/lib/env-config"; +import { buildBackendUrl } from "@/lib/env-config"; import { trackAnonymousChatMessageSent } from "@/lib/posthog/events"; import { cn } from "@/lib/utils"; import { QuotaBar } from "./quota-bar"; @@ -81,7 +81,7 @@ export function AnonymousChat({ model }: AnonymousChatProps) { content: m.content, })); - const response = await fetch(`${BACKEND_URL}/api/v1/public/anon-chat/stream`, { + const response = await fetch(buildBackendUrl("/api/v1/public/anon-chat/stream"), { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", diff --git a/surfsense_web/components/free-chat/free-chat-page.tsx b/surfsense_web/components/free-chat/free-chat-page.tsx index 8508b3d0c..966aaee60 100644 --- a/surfsense_web/components/free-chat/free-chat-page.tsx +++ b/surfsense_web/components/free-chat/free-chat-page.tsx @@ -33,7 +33,7 @@ import { updateThinkingSteps, updateToolCall, } from "@/lib/chat/streaming-state"; -import { BACKEND_URL } from "@/lib/env-config"; +import { buildBackendUrl } from "@/lib/env-config"; import { trackAnonymousChatMessageSent } from "@/lib/posthog/events"; import { FreeThread } from "./free-thread"; import { RemoveAdsBanner } from "./remove-ads-banner"; @@ -176,7 +176,7 @@ export function FreeChatPage() { if (!webSearchEnabled) reqBody.disabled_tools = ["web_search"]; if (turnstileToken) reqBody.turnstile_token = turnstileToken; - const response = await fetch(`${BACKEND_URL}/api/v1/public/anon-chat/stream`, { + const response = await fetch(buildBackendUrl("/api/v1/public/anon-chat/stream"), { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", diff --git a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx index 37463cfde..bc5b9ac13 100644 --- a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx @@ -79,7 +79,7 @@ import { foldersApiService } from "@/lib/apis/folders-api.service"; import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service"; import { authenticatedFetch } from "@/lib/auth-utils"; import { getMentionDocKey } from "@/lib/chat/mention-doc-key"; -import { BACKEND_URL } from "@/lib/env-config"; +import { buildBackendUrl } from "@/lib/env-config"; import { uploadFolderScan } from "@/lib/folder-sync-upload"; import { getSupportedExtensionsSet } from "@/lib/supported-extensions"; import { queries } from "@/zero/queries/index"; @@ -751,7 +751,9 @@ function AuthenticatedDocumentsSidebarBase({ .trim() .slice(0, 80) || "folder"; await doExport( - `${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/export?folder_id=${ctx.folder.id}`, + buildBackendUrl(`/api/v1/search-spaces/${searchSpaceId}/export`, { + folder_id: ctx.folder.id, + }), `${safeName}.zip` ); toast.success(`Folder "${ctx.folder.name}" exported`); @@ -803,7 +805,9 @@ function AuthenticatedDocumentsSidebarBase({ .trim() .slice(0, 80) || "folder"; await doExport( - `${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/export?folder_id=${folder.id}`, + buildBackendUrl(`/api/v1/search-spaces/${searchSpaceId}/export`, { + folder_id: folder.id, + }), `${safeName}.zip` ); toast.success(`Folder "${folder.name}" exported`); @@ -823,8 +827,8 @@ function AuthenticatedDocumentsSidebarBase({ try { const endpoint = doc.document_type === "USER_MEMORY" - ? `${BACKEND_URL}/api/v1/users/me/memory` - : `${BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/memory`; + ? buildBackendUrl("/api/v1/users/me/memory") + : buildBackendUrl(`/api/v1/searchspaces/${searchSpaceId}/memory`); const response = await authenticatedFetch(endpoint, { method: "GET" }); if (!response.ok) { const errorData = await response.json().catch(() => ({ detail: "Export failed" })); @@ -852,7 +856,9 @@ function AuthenticatedDocumentsSidebarBase({ try { const response = await authenticatedFetch( - `${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${doc.id}/export?format=${format}`, + buildBackendUrl(`/api/v1/search-spaces/${searchSpaceId}/documents/${doc.id}/export`, { + format, + }), { method: "GET" } ); @@ -1031,8 +1037,8 @@ function AuthenticatedDocumentsSidebarBase({ } const endpoint = doc.document_type === "USER_MEMORY" - ? `${BACKEND_URL}/api/v1/users/me/memory/reset` - : `${BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}/memory/reset`; + ? buildBackendUrl("/api/v1/users/me/memory/reset") + : buildBackendUrl(`/api/v1/searchspaces/${searchSpaceId}/memory/reset`); try { const response = await authenticatedFetch(endpoint, { method: "POST" }); if (!response.ok) { diff --git a/surfsense_web/components/report-panel/report-panel.tsx b/surfsense_web/components/report-panel/report-panel.tsx index 682235e0f..53b0c9867 100644 --- a/surfsense_web/components/report-panel/report-panel.tsx +++ b/surfsense_web/components/report-panel/report-panel.tsx @@ -22,7 +22,7 @@ import { Spinner } from "@/components/ui/spinner"; import { useMediaQuery } from "@/hooks/use-media-query"; import { baseApiService } from "@/lib/apis/base-api.service"; import { authenticatedFetch } from "@/lib/auth-utils"; -import { BACKEND_URL } from "@/lib/env-config"; +import { buildBackendUrl } from "@/lib/env-config"; function ReportPanelSkeleton() { return ( @@ -245,7 +245,7 @@ export function ReportPanelContent({ URL.revokeObjectURL(url); } else { const response = await authenticatedFetch( - `${BACKEND_URL}/api/v1/reports/${activeReportId}/export?format=${format}`, + buildBackendUrl(`/api/v1/reports/${activeReportId}/export`, { format }), { method: "GET" } ); @@ -278,7 +278,7 @@ export function ReportPanelContent({ setSaving(true); try { const response = await authenticatedFetch( - `${BACKEND_URL}/api/v1/reports/${activeReportId}/content`, + buildBackendUrl(`/api/v1/reports/${activeReportId}/content`), { method: "PUT", headers: { "Content-Type": "application/json" }, @@ -506,7 +506,11 @@ export function ReportPanelContent({
) : reportContent.content_type === "typst" ? ( diff --git a/surfsense_web/components/settings/general-settings-manager.tsx b/surfsense_web/components/settings/general-settings-manager.tsx index a308acfad..68ff21f07 100644 --- a/surfsense_web/components/settings/general-settings-manager.tsx +++ b/surfsense_web/components/settings/general-settings-manager.tsx @@ -12,7 +12,7 @@ import { Label } from "@/components/ui/label"; import { Skeleton } from "@/components/ui/skeleton"; import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service"; import { authenticatedFetch } from "@/lib/auth-utils"; -import { BACKEND_URL } from "@/lib/env-config"; +import { buildBackendUrl } from "@/lib/env-config"; import { cacheKeys } from "@/lib/query-client/cache-keys"; import { Spinner } from "../ui/spinner"; @@ -49,7 +49,7 @@ export function GeneralSettingsManager({ searchSpaceId }: GeneralSettingsManager setIsExporting(true); try { const response = await authenticatedFetch( - `${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/export`, + buildBackendUrl(`/api/v1/search-spaces/${searchSpaceId}/export`), { method: "GET" } ); if (!response.ok) { diff --git a/surfsense_web/components/tool-ui/generate-resume.tsx b/surfsense_web/components/tool-ui/generate-resume.tsx index 5533674bf..9147d4199 100644 --- a/surfsense_web/components/tool-ui/generate-resume.tsx +++ b/surfsense_web/components/tool-ui/generate-resume.tsx @@ -13,7 +13,7 @@ import { Button } from "@/components/ui/button"; import { useMediaQuery } from "@/hooks/use-media-query"; import { baseApiService } from "@/lib/apis/base-api.service"; import { getAuthHeaders } from "@/lib/auth-utils"; -import { BACKEND_URL } from "@/lib/env-config"; +import { buildBackendUrl } from "@/lib/env-config"; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( "pdfjs-dist/build/pdf.worker.min.mjs", @@ -223,7 +223,7 @@ function ResumeCard({ const previewPath = shareToken ? `/api/v1/public/${shareToken}/reports/${reportId}/preview` : `/api/v1/reports/${reportId}/preview`; - setPdfUrl(`${BACKEND_URL}${previewPath}`); + setPdfUrl(buildBackendUrl(previewPath)); if (autoOpen && isDesktop && !autoOpenedRef.current) { autoOpenedRef.current = true; diff --git a/surfsense_web/components/tool-ui/podcast/player.tsx b/surfsense_web/components/tool-ui/podcast/player.tsx index 2a3746844..ac00b6780 100644 --- a/surfsense_web/components/tool-ui/podcast/player.tsx +++ b/surfsense_web/components/tool-ui/podcast/player.tsx @@ -14,7 +14,7 @@ import { import { baseApiService } from "@/lib/apis/base-api.service"; import { podcastsApiService } from "@/lib/apis/podcasts-api.service"; import { authenticatedFetch } from "@/lib/auth-utils"; -import { BACKEND_URL } from "@/lib/env-config"; +import { buildBackendUrl } from "@/lib/env-config"; import { speakerLabel } from "./schema"; // Public snapshots predate the transcript.turns shape and keep their own. @@ -121,7 +121,7 @@ export function PodcastPlayer({ ); } else { const [audioResponse, detail] = await Promise.all([ - authenticatedFetch(`${BACKEND_URL}/api/v1/podcasts/${podcastId}/stream`, { + authenticatedFetch(buildBackendUrl(`/api/v1/podcasts/${podcastId}/stream`), { method: "GET", signal: controller.signal, }), diff --git a/surfsense_web/components/tool-ui/sandbox-execute.tsx b/surfsense_web/components/tool-ui/sandbox-execute.tsx index 3d309332e..a7633d0ec 100644 --- a/surfsense_web/components/tool-ui/sandbox-execute.tsx +++ b/surfsense_web/components/tool-ui/sandbox-execute.tsx @@ -17,7 +17,7 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { getBearerToken } from "@/lib/auth-utils"; -import { BACKEND_URL } from "@/lib/env-config"; +import { buildBackendUrl } from "@/lib/env-config"; import { cn } from "@/lib/utils"; // ============================================================================ @@ -158,7 +158,9 @@ function truncateCommand(command: string, maxLen = 80): string { async function downloadSandboxFile(threadId: string, filePath: string, fileName: string) { const token = getBearerToken(); - const url = `${BACKEND_URL}/api/v1/threads/${threadId}/sandbox/download?path=${encodeURIComponent(filePath)}`; + const url = buildBackendUrl(`/api/v1/threads/${threadId}/sandbox/download`, { + path: filePath, + }); const res = await fetch(url, { headers: { Authorization: `Bearer ${token || ""}` }, }); diff --git a/surfsense_web/components/tool-ui/video-presentation/generate-video-presentation.tsx b/surfsense_web/components/tool-ui/video-presentation/generate-video-presentation.tsx index 1db8dabb0..9f2115073 100644 --- a/surfsense_web/components/tool-ui/video-presentation/generate-video-presentation.tsx +++ b/surfsense_web/components/tool-ui/video-presentation/generate-video-presentation.tsx @@ -10,7 +10,7 @@ import { TextShimmerLoader } from "@/components/prompt-kit/loader"; import { Button } from "@/components/ui/button"; import { baseApiService } from "@/lib/apis/base-api.service"; import { authenticatedFetch } from "@/lib/auth-utils"; -import { BACKEND_URL } from "@/lib/env-config"; +import { buildBackendUrl } from "@/lib/env-config"; import { compileCheck, compileToComponent } from "@/lib/remotion/compile-check"; import { FPS } from "@/lib/remotion/constants"; import { @@ -137,7 +137,6 @@ function VideoPresentationPlayer({ const [isPptxExporting, setIsPptxExporting] = useState(false); const [pptxProgress, setPptxProgress] = useState(null); - const backendUrl = BACKEND_URL ?? ""; const audioBlobUrlsRef = useRef([]); const loadPresentation = useCallback(async () => { @@ -177,7 +176,7 @@ function VideoPresentationPlayer({ title: scene.title ?? slide.title, code: scene.code, durationInFrames, - audioUrl: slide.audio_url ? `${backendUrl}${slide.audio_url}` : undefined, + audioUrl: slide.audio_url ? buildBackendUrl(slide.audio_url) : undefined, }); } @@ -222,7 +221,7 @@ function VideoPresentationPlayer({ } finally { setIsLoading(false); } - }, [presentationId, backendUrl, shareToken]); + }, [presentationId, shareToken]); useEffect(() => { loadPresentation(); diff --git a/surfsense_web/eslint.config.mjs b/surfsense_web/eslint.config.mjs index 6a3f3f4c6..9531332bb 100644 --- a/surfsense_web/eslint.config.mjs +++ b/surfsense_web/eslint.config.mjs @@ -13,24 +13,25 @@ const eslintConfig = [ ...compat.extends("next/core-web-vitals", "next/typescript"), { rules: { - "no-restricted-syntax": [ + "no-restricted-imports": [ "error", { - selector: - "NewExpression[callee.name='URL'] TemplateLiteral Identifier[name='BACKEND_URL']", - message: - "Use buildBackendUrl(path, params) for backend URLs. BACKEND_URL may be empty in proxy mode, and new URL('/relative') throws without a base.", - }, - { - selector: - "NewExpression[callee.name='URL'] TemplateLiteral Identifier[name='backendUrl']", - message: - "Use buildBackendUrl(path, params) for backend URLs instead of aliasing BACKEND_URL into new URL().", - }, - { - selector: "VariableDeclarator[id.name='backendUrl'][init.name='BACKEND_URL']", - message: - "Do not alias BACKEND_URL for URL construction. Use buildBackendUrl(path, params) instead.", + paths: [ + { + name: "@/lib/env-config", + importNames: ["BACKEND_URL"], + message: + "Use buildBackendUrl(path, params) for browser-facing backend URLs. BACKEND_URL is empty in proxy mode; importing it bypasses the single URL seam.", + }, + ], + patterns: [ + { + group: ["**/env-config", "**/env-config.ts"], + importNames: ["BACKEND_URL"], + message: + "Use buildBackendUrl(path, params). Import BACKEND_URL only inside lib/env-config.ts.", + }, + ], }, ], }, diff --git a/surfsense_web/lib/apis/anonymous-chat-api.service.ts b/surfsense_web/lib/apis/anonymous-chat-api.service.ts index 843576a50..5cdc139e1 100644 --- a/surfsense_web/lib/apis/anonymous-chat-api.service.ts +++ b/surfsense_web/lib/apis/anonymous-chat-api.service.ts @@ -7,7 +7,7 @@ import { getAnonModelResponse, getAnonModelsResponse, } from "@/contracts/types/anonymous-chat.types"; -import { BACKEND_URL } from "../env-config"; +import { buildBackendUrl } from "../env-config"; import { ValidationError } from "../error"; const BASE = "/api/v1/public/anon-chat"; @@ -17,14 +17,8 @@ export type AnonUploadResult = | { ok: false; reason: "quota_exceeded" }; class AnonymousChatApiService { - private baseUrl: string; - - constructor(baseUrl: string) { - this.baseUrl = baseUrl; - } - private fullUrl(path: string): string { - return `${this.baseUrl}${BASE}${path}`; + return buildBackendUrl(`${BASE}${path}`); } getModels = async (): Promise => { @@ -102,4 +96,4 @@ class AnonymousChatApiService { }; } -export const anonymousChatApiService = new AnonymousChatApiService(BACKEND_URL); +export const anonymousChatApiService = new AnonymousChatApiService(); diff --git a/surfsense_web/lib/auth-utils.ts b/surfsense_web/lib/auth-utils.ts index b7dab7717..8ad10308b 100644 --- a/surfsense_web/lib/auth-utils.ts +++ b/surfsense_web/lib/auth-utils.ts @@ -1,7 +1,7 @@ /** * Authentication utilities for handling token expiration and redirects */ -import { BACKEND_URL } from "@/lib/env-config"; +import { buildBackendUrl } from "@/lib/env-config"; const REDIRECT_PATH_KEY = "surfsense_redirect_path"; const BEARER_TOKEN_KEY = "surfsense_bearer_token"; @@ -195,7 +195,7 @@ export async function logout(): Promise { // Call backend to revoke the refresh token if (refreshToken) { try { - const response = await fetch(`${BACKEND_URL}/auth/jwt/revoke`, { + const response = await fetch(buildBackendUrl("/auth/jwt/revoke"), { method: "POST", headers: { "Content-Type": "application/json", @@ -273,7 +273,7 @@ export async function refreshAccessToken(): Promise { isRefreshing = true; refreshPromise = (async () => { try { - const response = await fetch(`${BACKEND_URL}/auth/jwt/refresh`, { + const response = await fetch(buildBackendUrl("/auth/jwt/refresh"), { method: "POST", headers: { "Content-Type": "application/json", diff --git a/surfsense_web/lib/chat/thread-persistence.ts b/surfsense_web/lib/chat/thread-persistence.ts index d30b87665..dc5846f23 100644 --- a/surfsense_web/lib/chat/thread-persistence.ts +++ b/surfsense_web/lib/chat/thread-persistence.ts @@ -4,7 +4,7 @@ */ import { baseApiService } from "@/lib/apis/base-api.service"; -import { BACKEND_URL } from "@/lib/env-config"; +import { buildBackendUrl } from "@/lib/env-config"; // ============================================================================= // Types matching backend schemas // ============================================================================= @@ -227,5 +227,5 @@ export interface RegenerateParams { * Get the URL for the regenerate endpoint (for streaming fetch) */ export function getRegenerateUrl(threadId: number): string { - return `${BACKEND_URL}/api/v1/threads/${threadId}/regenerate`; + return buildBackendUrl(`/api/v1/threads/${threadId}/regenerate`); }