mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-18 21:15:16 +02:00
refactor(web): replace BACKEND_URL with buildBackendUrl for dynamic URL construction
This commit is contained in:
parent
66659ee8d3
commit
371ff866c7
9 changed files with 138 additions and 79 deletions
|
|
@ -106,7 +106,7 @@ import {
|
|||
extractUserTurnForNewChatApi,
|
||||
type NewChatUserImagePayload,
|
||||
} from "@/lib/chat/user-turn-api-parts";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import { NotFoundError } from "@/lib/error";
|
||||
import {
|
||||
trackChatBlocked,
|
||||
|
|
@ -919,10 +919,9 @@ export default function NewChatPage() {
|
|||
if (threadId) {
|
||||
const token = getBearerToken();
|
||||
if (token) {
|
||||
const backendUrl = BACKEND_URL;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${backendUrl}/api/v1/threads/${threadId}/cancel-active-turn`,
|
||||
buildBackendUrl(`/api/v1/threads/${threadId}/cancel-active-turn`),
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
|
@ -1110,7 +1109,6 @@ export default function NewChatPage() {
|
|||
let streamBatcher: FrameBatchedUpdater | null = null;
|
||||
|
||||
try {
|
||||
const backendUrl = BACKEND_URL;
|
||||
const selection = await getAgentFilesystemSelection(searchSpaceId, {
|
||||
localFilesystemEnabled,
|
||||
});
|
||||
|
|
@ -1147,7 +1145,7 @@ export default function NewChatPage() {
|
|||
}
|
||||
|
||||
const response = await fetchWithTurnCancellingRetry(() =>
|
||||
fetch(`${backendUrl}/api/v1/new_chat`, {
|
||||
fetch(buildBackendUrl("/api/v1/new_chat"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -1642,12 +1640,11 @@ export default function NewChatPage() {
|
|||
}
|
||||
|
||||
try {
|
||||
const backendUrl = BACKEND_URL;
|
||||
const selection = await getAgentFilesystemSelection(searchSpaceId, {
|
||||
localFilesystemEnabled,
|
||||
});
|
||||
const response = await fetchWithTurnCancellingRetry(() =>
|
||||
fetch(`${backendUrl}/api/v1/threads/${resumeThreadId}/resume`, {
|
||||
fetch(buildBackendUrl(`/api/v1/threads/${resumeThreadId}/resume`), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { getReauthEndpoint } from "@/lib/connector-telemetry";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { DateRangeSelector } from "../../components/date-range-selector";
|
||||
import { PeriodicSyncConfig } from "../../components/periodic-sync-config";
|
||||
|
|
@ -95,12 +95,13 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
|
|||
if (!spaceId || !reauthEndpoint) return;
|
||||
setReauthing(true);
|
||||
try {
|
||||
const backendUrl = BACKEND_URL;
|
||||
const url = new URL(`${backendUrl}${reauthEndpoint}`);
|
||||
url.searchParams.set("connector_id", String(connector.id));
|
||||
url.searchParams.set("space_id", String(spaceId));
|
||||
url.searchParams.set("return_url", window.location.pathname);
|
||||
const response = await authenticatedFetch(url.toString());
|
||||
const response = await authenticatedFetch(
|
||||
buildBackendUrl(reauthEndpoint, {
|
||||
connector_id: connector.id,
|
||||
space_id: spaceId,
|
||||
return_url: window.location.pathname,
|
||||
})
|
||||
);
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
toast.error(data.detail ?? "Failed to initiate re-authentication.");
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
|||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { getReauthEndpoint } from "@/lib/connector-telemetry";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import { formatRelativeDate } from "@/lib/format-date";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LIVE_CONNECTOR_TYPES } from "../constants/connector-constants";
|
||||
|
|
@ -61,12 +61,13 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
|
|||
if (!searchSpaceId || !endpoint) return;
|
||||
setReauthingId(connector.id);
|
||||
try {
|
||||
const backendUrl = BACKEND_URL;
|
||||
const url = new URL(`${backendUrl}${endpoint}`);
|
||||
url.searchParams.set("connector_id", String(connector.id));
|
||||
url.searchParams.set("space_id", String(searchSpaceId));
|
||||
url.searchParams.set("return_url", window.location.pathname);
|
||||
const response = await authenticatedFetch(url.toString());
|
||||
const response = await authenticatedFetch(
|
||||
buildBackendUrl(endpoint, {
|
||||
connector_id: connector.id,
|
||||
space_id: searchSpaceId,
|
||||
return_url: window.location.pathname,
|
||||
})
|
||||
);
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
toast.error(data.detail ?? "Failed to initiate re-authentication.");
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import { useMediaQuery } from "@/hooks/use-media-query";
|
|||
import { useElectronAPI } from "@/hooks/use-platform";
|
||||
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
|
||||
import { inferMonacoLanguageFromPath } from "@/lib/editor-language";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
|
||||
const PlateEditor = dynamic(
|
||||
() => import("@/components/editor/plate-editor").then((m) => ({ default: m.PlateEditor })),
|
||||
|
|
@ -260,10 +260,12 @@ export function EditorPanelContent({
|
|||
return;
|
||||
}
|
||||
|
||||
const url = new URL(
|
||||
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/editor-content`
|
||||
const response = await authenticatedFetch(
|
||||
buildBackendUrl(
|
||||
`/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/editor-content`
|
||||
),
|
||||
{ method: "GET" }
|
||||
);
|
||||
const response = await authenticatedFetch(url.toString(), { method: "GET" });
|
||||
|
||||
if (controller.signal.aborted) return;
|
||||
|
||||
|
|
@ -402,7 +404,7 @@ export function EditorPanelContent({
|
|||
return;
|
||||
}
|
||||
const response = await authenticatedFetch(
|
||||
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/save`,
|
||||
buildBackendUrl(`/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/save`),
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
|
@ -496,7 +498,9 @@ export function EditorPanelContent({
|
|||
setDownloading(true);
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/download-markdown`,
|
||||
buildBackendUrl(
|
||||
`/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/download-markdown`
|
||||
),
|
||||
{ method: "GET" }
|
||||
);
|
||||
if (!response.ok) throw new Error("Download failed");
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { Alert, AlertDescription } from "@/components/ui/alert";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
|
||||
const LARGE_DOCUMENT_THRESHOLD = 2 * 1024 * 1024; // 2MB
|
||||
|
||||
|
|
@ -108,10 +108,12 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen
|
|||
}
|
||||
|
||||
try {
|
||||
const url = new URL(
|
||||
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/editor-content`
|
||||
const response = await authenticatedFetch(
|
||||
buildBackendUrl(
|
||||
`/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/editor-content`
|
||||
),
|
||||
{ method: "GET" }
|
||||
);
|
||||
const response = await authenticatedFetch(url.toString(), { method: "GET" });
|
||||
|
||||
if (controller.signal.aborted) return;
|
||||
|
||||
|
|
@ -165,7 +167,7 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen
|
|||
setSaving(true);
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/save`,
|
||||
buildBackendUrl(`/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/save`),
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
|
@ -323,7 +325,9 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen
|
|||
setDownloading(true);
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`${BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/download-markdown`,
|
||||
buildBackendUrl(
|
||||
`/api/v1/search-spaces/${searchSpaceId}/documents/${documentId}/download-markdown`
|
||||
),
|
||||
{ method: "GET" }
|
||||
);
|
||||
if (!response.ok) throw new Error("Download failed");
|
||||
|
|
|
|||
|
|
@ -9,6 +9,32 @@ const compat = new FlatCompat({
|
|||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [...compat.extends("next/core-web-vitals", "next/typescript")];
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
{
|
||||
rules: {
|
||||
"no-restricted-syntax": [
|
||||
"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.",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
export interface SearchSourceConnector {
|
||||
id: number;
|
||||
name: string;
|
||||
|
|
@ -106,16 +106,15 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
|
|||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Build URL with optional search_space_id query parameter
|
||||
const url = new URL(`${BACKEND_URL}/api/v1/search-source-connectors`);
|
||||
if (spaceId !== undefined) {
|
||||
url.searchParams.append("search_space_id", spaceId.toString());
|
||||
}
|
||||
|
||||
const response = await authenticatedFetch(url.toString(), {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
const response = await authenticatedFetch(
|
||||
buildBackendUrl("/api/v1/search-source-connectors", {
|
||||
search_space_id: spaceId,
|
||||
}),
|
||||
{
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch connectors: ${response.statusText}`);
|
||||
|
|
@ -166,15 +165,16 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
|
|||
spaceId: number
|
||||
) => {
|
||||
try {
|
||||
// Add search_space_id as a query parameter
|
||||
const url = new URL(`${BACKEND_URL}/api/v1/search-source-connectors`);
|
||||
url.searchParams.append("search_space_id", spaceId.toString());
|
||||
|
||||
const response = await authenticatedFetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(connectorData),
|
||||
});
|
||||
const response = await authenticatedFetch(
|
||||
buildBackendUrl("/api/v1/search-source-connectors", {
|
||||
search_space_id: spaceId,
|
||||
}),
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(connectorData),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to create connector: ${response.statusText}`);
|
||||
|
|
@ -204,7 +204,7 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
|
|||
) => {
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`${BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`,
|
||||
buildBackendUrl(`/api/v1/search-source-connectors/${connectorId}`),
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
|
@ -235,7 +235,7 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
|
|||
const deleteConnector = async (connectorId: number) => {
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`${BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`,
|
||||
buildBackendUrl(`/api/v1/search-source-connectors/${connectorId}`),
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
|
@ -267,19 +267,12 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
|
|||
endDate?: string
|
||||
) => {
|
||||
try {
|
||||
// Build query parameters
|
||||
const params = new URLSearchParams({
|
||||
search_space_id: searchSpaceId.toString(),
|
||||
});
|
||||
if (startDate) {
|
||||
params.append("start_date", startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
params.append("end_date", endDate);
|
||||
}
|
||||
|
||||
const response = await authenticatedFetch(
|
||||
`${BACKEND_URL}/api/v1/search-source-connectors/${connectorId}/index?${params.toString()}`,
|
||||
buildBackendUrl(`/api/v1/search-source-connectors/${connectorId}/index`, {
|
||||
search_space_id: searchSpaceId,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
}),
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ZodType } from "zod";
|
||||
import { BACKEND_URL } from "@/lib/env-config";
|
||||
import { buildBackendUrl } from "@/lib/env-config";
|
||||
import { getClientPlatform } from "../agent-filesystem";
|
||||
import { getBearerToken, handleUnauthorized, refreshAccessToken } from "../auth-utils";
|
||||
import {
|
||||
|
|
@ -31,8 +31,6 @@ export type RequestOptions = {
|
|||
};
|
||||
|
||||
class BaseApiService {
|
||||
baseUrl: string;
|
||||
|
||||
noAuthEndpoints: string[] = ["/auth/jwt/login", "/auth/register", "/auth/refresh"];
|
||||
|
||||
// Prefixes that don't require auth (checked with startsWith)
|
||||
|
|
@ -44,12 +42,9 @@ class BaseApiService {
|
|||
return typeof window !== "undefined" ? getBearerToken() || "" : "";
|
||||
}
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
// Keep for backward compatibility, but token is now always read from localStorage
|
||||
setBearerToken(_bearerToken: string) {
|
||||
void _bearerToken;
|
||||
// No-op: token is now always read fresh from localStorage via the getter
|
||||
}
|
||||
|
||||
|
|
@ -102,8 +97,7 @@ class BaseApiService {
|
|||
throw new AuthenticationError("You are not authenticated. Please login again.");
|
||||
}
|
||||
|
||||
// Construct the full URL. Empty baseUrl is valid for same-origin proxy mode.
|
||||
const fullUrl = this.baseUrl ? new URL(url, this.baseUrl).toString() : url;
|
||||
const fullUrl = buildBackendUrl(url);
|
||||
|
||||
// Prepare fetch options
|
||||
const fetchOptions: RequestInit = {
|
||||
|
|
@ -379,7 +373,8 @@ class BaseApiService {
|
|||
options?: Omit<RequestOptions, "method" | "responseType" | "body"> & { body: FormData }
|
||||
) {
|
||||
// Remove Content-Type from options headers if present
|
||||
const { "Content-Type": _, ...headersWithoutContentType } = options?.headers ?? {};
|
||||
const headersWithoutContentType = { ...(options?.headers ?? {}) };
|
||||
delete headersWithoutContentType["Content-Type"];
|
||||
|
||||
return this.request(url, responseSchema, {
|
||||
method: "POST",
|
||||
|
|
@ -394,4 +389,4 @@ class BaseApiService {
|
|||
}
|
||||
}
|
||||
|
||||
export const baseApiService = new BaseApiService(BACKEND_URL);
|
||||
export const baseApiService = new BaseApiService();
|
||||
|
|
|
|||
|
|
@ -16,6 +16,44 @@ export const AUTH_TYPE = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "G
|
|||
// same-origin relative requests (e.g. /api/v1/... and /auth/...).
|
||||
export const BACKEND_URL = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL ?? "";
|
||||
|
||||
type BackendUrlParam = string | number | boolean | null | undefined;
|
||||
|
||||
/**
|
||||
* Build browser-facing backend URLs without breaking proxy mode.
|
||||
*
|
||||
* In proxy mode BACKEND_URL intentionally stays empty, so callers must keep
|
||||
* same-origin relative URLs ("/api/v1/...") and let Caddy route them. When
|
||||
* BACKEND_URL is explicitly configured, the same path resolves against that
|
||||
* absolute backend origin.
|
||||
*/
|
||||
export function buildBackendUrl(
|
||||
path: string,
|
||||
params?: Record<string, BackendUrlParam>
|
||||
): string {
|
||||
const backendPath = path.startsWith("/") ? path : `/${path}`;
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (params) {
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== null && value !== undefined) {
|
||||
queryParams.append(key, String(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (BACKEND_URL) {
|
||||
const url = new URL(backendPath, BACKEND_URL);
|
||||
for (const [key, value] of queryParams) {
|
||||
url.searchParams.append(key, value);
|
||||
}
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
if (!queryString) return backendPath;
|
||||
return `${backendPath}${backendPath.includes("?") ? "&" : "?"}${queryString}`;
|
||||
}
|
||||
|
||||
// Server-side backend URL. Relative browser URLs do not work from RSC/API route
|
||||
// code, so server callers should use Docker DNS or an explicit public backend.
|
||||
export const SERVER_BACKEND_URL =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue