refactor(web): replace instances of BACKEND_URL with buildBackendUrl for improved URL handling

This commit is contained in:
Anish Sarkar 2026-06-16 14:51:25 +05:30
parent 371ff866c7
commit 3f69bfd5e4
21 changed files with 98 additions and 95 deletions

View file

@ -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 (
<div className="relative w-full overflow-hidden">

View file

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

View file

@ -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 () => {

View file

@ -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";

View file

@ -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<CirclebackConfigProps> = ({ 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;

View file

@ -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" });

View file

@ -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");

View file

@ -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 }),

View file

@ -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",

View file

@ -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",

View file

@ -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) {

View file

@ -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({
</div>
) : reportContent.content_type === "typst" ? (
<PdfViewer
pdfUrl={`${BACKEND_URL}${shareToken ? `/api/v1/public/${shareToken}/reports/${activeReportId}/preview` : `/api/v1/reports/${activeReportId}/preview`}`}
pdfUrl={buildBackendUrl(
shareToken
? `/api/v1/public/${shareToken}/reports/${activeReportId}/preview`
: `/api/v1/reports/${activeReportId}/preview`
)}
isPublic={isPublic}
toolbarActions={
<>

View file

@ -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) {

View file

@ -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;

View file

@ -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,
}),

View file

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

View file

@ -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<string | null>(null);
const backendUrl = BACKEND_URL ?? "";
const audioBlobUrlsRef = useRef<string[]>([]);
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();

View file

@ -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.",
},
],
},
],
},

View file

@ -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<AnonModel[]> => {
@ -102,4 +96,4 @@ class AnonymousChatApiService {
};
}
export const anonymousChatApiService = new AnonymousChatApiService(BACKEND_URL);
export const anonymousChatApiService = new AnonymousChatApiService();

View file

@ -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<boolean> {
// 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<string | null> {
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",

View file

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