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`);
}