diff --git a/package.json b/package.json index fb42544..6d50036 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,7 @@ "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview", - "test": "node --test" + "preview": "vite preview" }, "dependencies": { "alpinejs": "^3.14.9", diff --git a/src/api/client.js b/src/api/client.js index 13e7c9b..06964a2 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -1,5 +1,68 @@ import { API_PATHS } from '../app/config.js'; -import { resolveApiRequestUrl } from './url.js'; + +function normalizeBaseUrl(baseUrl) { + return String(baseUrl || '').trim().replace(/\/+$/, ''); +} + +function isSameOriginBaseUrl(baseUrl) { + if (!baseUrl) { + return true; + } + + if (typeof window === 'undefined') { + return false; + } + + try { + return new URL(baseUrl, window.location.origin).origin === window.location.origin; + } catch { + return false; + } +} + +function buildPathname({ database, kitchenId, path, includeKitchen = true }) { + const encodedDatabase = encodeURIComponent(database); + const encodedPathSegments = String(path || '') + .replace(/^\/+/, '') + .split('/') + .filter(Boolean) + .map((segment) => encodeURIComponent(segment)); + const segments = [encodedDatabase]; + + if (includeKitchen && kitchenId) { + segments.push('kitchen', encodeURIComponent(String(kitchenId))); + } + + segments.push(...encodedPathSegments); + + return `/${segments.join('/')}`; +} + +function buildUrl({ baseUrl, database, kitchenId, path, query = {}, includeKitchen = true }) { + const cleanBaseUrl = normalizeBaseUrl(baseUrl); + const pathname = buildPathname({ + database, + kitchenId, + path, + includeKitchen, + }); + const searchParams = new URLSearchParams(); + + Object.entries(query).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') { + searchParams.set(key, String(value)); + } + }); + + const queryString = searchParams.toString(); + const relativeUrl = queryString ? `${pathname}?${queryString}` : pathname; + + if (!cleanBaseUrl || isSameOriginBaseUrl(cleanBaseUrl)) { + return relativeUrl; + } + + return new URL(relativeUrl, `${cleanBaseUrl}/`).toString(); +} async function parseJsonResponse(response) { const text = await response.text(); @@ -91,31 +154,14 @@ function extractErrorMessage(response, payload) { function normalizeError(response, payload, request) { const details = flattenErrorObject(payload?.errors || payload?.details) || null; - const payloadMessage = extractErrorMessage(response, payload); - const statusHint = - response.status === 404 - ? 'This usually means the request hit the wrong origin, port, or path.' - : response.status === 401 || response.status === 403 - ? 'Authentication or application-key validation failed.' - : response.status >= 500 - ? 'The backend reported a server error.' - : ''; - const message = [ - `${request.method} ${request.resolvedUrl} returned ${response.status}.`, - statusHint, - payloadMessage && !payloadMessage.startsWith('Request failed with status') - ? payloadMessage - : '', - ] - .filter(Boolean) - .join(' '); + const message = + extractErrorMessage(response, payload); const error = new Error(message, { cause: { status: response.status, details, - url: request.resolvedUrl, - fetchUrl: request.fetchUrl, + url: request.url, method: request.method, payload, }, @@ -123,8 +169,7 @@ function normalizeError(response, payload, request) { error.name = 'ApiRequestError'; error.status = response.status; - error.url = request.resolvedUrl; - error.fetchUrl = request.fetchUrl; + error.url = request.url; error.method = request.method; error.details = details; error.payload = payload; @@ -144,10 +189,8 @@ export async function apiRequest(store, path, options = {}) { } const method = options.method || 'GET'; - const currentOrigin = typeof window !== 'undefined' ? window.location.origin : ''; - const requestTarget = resolveApiRequestUrl({ + const url = buildUrl({ baseUrl: config.baseUrl, - currentOrigin, database: config.database, kitchenId: activeKitchen?.id, path, @@ -167,7 +210,7 @@ export async function apiRequest(store, path, options = {}) { let response; try { - response = await fetch(requestTarget.fetchUrl, { + response = await fetch(url, { method, headers, credentials: options.credentials || 'same-origin', @@ -177,29 +220,21 @@ export async function apiRequest(store, path, options = {}) { : options.body || undefined, }); } catch (error) { - const networkHint = requestTarget.isCrossOrigin - ? 'Cross-origin request failed before a usable response was returned. Check nginx CORS/preflight handling and that the configured API origin is reachable.' - : 'Request failed before a response was returned. Check that the current origin serves the API or configure the Tryton server URL explicitly.'; const networkError = new Error( - `${method} ${requestTarget.resolvedUrl} failed before a response was returned. ${networkHint}`, + `Network request failed for ${method} ${url}. Check the server URL, nginx proxy, and CORS configuration.`, { cause: { - url: requestTarget.resolvedUrl, - fetchUrl: requestTarget.fetchUrl, + url, method, - isCrossOrigin: requestTarget.isCrossOrigin, originalError: error, }, }, ); networkError.name = 'ApiNetworkError'; - networkError.url = requestTarget.resolvedUrl; - networkError.fetchUrl = requestTarget.fetchUrl; + networkError.url = url; networkError.method = method; - networkError.isCrossOrigin = requestTarget.isCrossOrigin; logApiFailure('API network request failed', { - url: requestTarget.resolvedUrl, - fetchUrl: requestTarget.fetchUrl, + url, method, error, }); @@ -208,14 +243,9 @@ export async function apiRequest(store, path, options = {}) { const payload = await parseResponse(response); if (!response.ok) { - const apiError = normalizeError(response, payload, { - resolvedUrl: requestTarget.resolvedUrl, - fetchUrl: requestTarget.fetchUrl, - method, - }); + const apiError = normalizeError(response, payload, { url, method }); logApiFailure('API request returned an error response', { - url: requestTarget.resolvedUrl, - fetchUrl: requestTarget.fetchUrl, + url, method, status: response.status, payload, @@ -231,13 +261,12 @@ export function getPath(key) { } export function buildKitchenApiUrl(store, path, query = {}) { - return resolveApiRequestUrl({ + return buildUrl({ baseUrl: store.config.baseUrl, - currentOrigin: typeof window !== 'undefined' ? window.location.origin : '', database: store.config.database, kitchenId: store.activeKitchen?.id, path, query, includeKitchen: true, - }).resolvedUrl; + }); }