diff --git a/src/api/api.ts b/src/api/api.ts index 98cbbd46..90c639ae 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -1,28 +1,12 @@ import ky, { type HTTPError, type KyInstance, type Options } from "ky"; import { env } from "@/lib/env"; +import { resolveApiPrefixUrl } from "@/lib/resolve-api-prefix-url"; import { API_PATHS } from "@/api/api-paths"; import { createApiErrorFromResponse, mapHttpStatusToMessage } from "@/api/api-error"; import { queryKeys } from "@/infra/query-key/query-key"; import queryClient from "@/lib/query/query-client"; import { clearAuth, getAccessToken } from "@/lib/auth/auth-storage"; -function resolvePrefixUrl(url: string): string { - const trimmed = url.trim() || "/api"; - - if (trimmed.startsWith("http")) { - return trimmed.endsWith("/") ? trimmed : `${trimmed}/`; - } - - const path = trimmed.startsWith("/") ? trimmed : `/${trimmed}`; - const pathWithSlash = path.endsWith("/") ? path : `${path}/`; - const origin = - typeof window !== "undefined" && window.location?.origin - ? window.location.origin - : "http://localhost:3000"; - - return `${origin}${pathWithSlash}`; -} - function clearSessionAndRedirectToLogin(): void { clearAuth(); queryClient.setQueryData(queryKeys.auth.session(), null); @@ -37,7 +21,7 @@ function isAuthLoginRequest(request: Request): boolean { } export const api: KyInstance = ky.create({ - prefixUrl: resolvePrefixUrl(env.apiUrl), + prefixUrl: resolveApiPrefixUrl(env.apiUrl), headers: { Accept: "application/json", "Content-Type": "application/json", diff --git a/src/domain/vendor-portal/api/vendor-portal-api.ts b/src/domain/vendor-portal/api/vendor-portal-api.ts index 73801ce8..2630e86a 100644 --- a/src/domain/vendor-portal/api/vendor-portal-api.ts +++ b/src/domain/vendor-portal/api/vendor-portal-api.ts @@ -1,5 +1,6 @@ import { API_PATHS } from "@/api/api-paths"; import { env } from "@/lib/env"; +import { resolveApiBaseUrl } from "@/lib/resolve-api-prefix-url"; import type { VendorPortalComment, VendorPortalDispatchDetail, @@ -9,11 +10,8 @@ import type { } from "@/domain/vendor-portal/types/vendor-portal"; function resolveVendorPortalUrl(path: string): string { - const base = env.apiUrl.trim().replace(/\/$/, ""); - const prefix = base.startsWith("http") - ? base - : `${typeof window !== "undefined" ? window.location.origin : "http://localhost:3000"}${base.startsWith("/") ? base : `/${base}`}`; - return `${prefix}/${API_PATHS.vendorPortal.base}${path}`; + const base = resolveApiBaseUrl(env.apiUrl); + return `${base}/${API_PATHS.vendorPortal.base}${path}`; } async function vendorPortalCall( diff --git a/src/lib/resolve-api-prefix-url.ts b/src/lib/resolve-api-prefix-url.ts new file mode 100644 index 00000000..0d42df45 --- /dev/null +++ b/src/lib/resolve-api-prefix-url.ts @@ -0,0 +1,35 @@ +const DEFAULT_ORIGIN = "http://localhost:3000"; + +function getDefaultOrigin(): string { + if (typeof window !== "undefined" && window.location?.origin) { + return window.location.origin; + } + + return DEFAULT_ORIGIN; +} + +/** + * Resolves VITE_API_URL to a Ky-compatible prefixUrl (always ends with /). + * + * Contract: + * - Relative paths (e.g. `/api`) are joined with the app origin. + * - Absolute URLs are used as-is; they must already include `/api` for production. + * - API_PATHS do not include `/api` — only this prefix supplies it. + */ +export function resolveApiPrefixUrl(url: string, origin = getDefaultOrigin()): string { + const trimmed = url.trim() || "/api"; + + if (trimmed.startsWith("http")) { + return trimmed.endsWith("/") ? trimmed : `${trimmed}/`; + } + + const path = trimmed.startsWith("/") ? trimmed : `/${trimmed}`; + const pathWithSlash = path.endsWith("/") ? path : `${path}/`; + + return `${origin}${pathWithSlash}`; +} + +/** API base without trailing slash (for fetch URLs outside Ky). */ +export function resolveApiBaseUrl(url: string, origin = getDefaultOrigin()): string { + return resolveApiPrefixUrl(url, origin).replace(/\/$/, ""); +}