7b8f81336a
Build & Push / Pipeline Tests (push) Failing after 2m0s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Successful in 53s
Test / API Unit Tests (push) Successful in 1m8s
Test / Homepage Unit Tests (push) Successful in 48s
Test / Carplace Unit Tests (push) Successful in 42s
Test / Admin Unit Tests (push) Successful in 41s
Test / Dashboard Unit Tests (push) Successful in 43s
Test / API Integration Tests (push) Failing after 1m7s
161 lines
4.6 KiB
TypeScript
161 lines
4.6 KiB
TypeScript
export function normalizeApiBase(value: string): string {
|
|
const base = value.replace(/\/+$/, '')
|
|
if (/\/api$/.test(base)) return `${base}/v1`
|
|
return /\/api\/v1$/.test(base) ? base : `${base}/api/v1`
|
|
}
|
|
|
|
const DASHBOARD_PROXY_API_BASE = '/dashboard/api/v1'
|
|
|
|
function isLocalBrowserHost(hostname: string): boolean {
|
|
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'
|
|
}
|
|
|
|
function shouldUseDashboardProxy(configuredApiBase?: string): boolean {
|
|
if (typeof window === 'undefined') return false
|
|
if (!configuredApiBase) return true
|
|
if (configuredApiBase.startsWith('/')) return false
|
|
|
|
try {
|
|
const apiUrl = new URL(configuredApiBase)
|
|
const currentOrigin = window.location?.origin
|
|
const currentHostname = window.location?.hostname
|
|
if (!currentOrigin || !currentHostname) return false
|
|
|
|
if (isLocalBrowserHost(currentHostname) && isLocalBrowserHost(apiUrl.hostname)) {
|
|
return apiUrl.origin !== currentOrigin
|
|
}
|
|
|
|
if (isLocalBrowserHost(currentHostname)) return false
|
|
|
|
return apiUrl.origin !== currentOrigin
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
export function resolveApiBase(): string {
|
|
if (typeof window === 'undefined') {
|
|
return normalizeApiBase(process.env.API_INTERNAL_URL || 'http://localhost:4000/api/v1')
|
|
}
|
|
|
|
const configuredApiBase = process.env.NEXT_PUBLIC_API_URL
|
|
return normalizeApiBase(shouldUseDashboardProxy(configuredApiBase) ? DASHBOARD_PROXY_API_BASE : (configuredApiBase || DASHBOARD_PROXY_API_BASE))
|
|
}
|
|
|
|
export function resolveApiOrigin(): string | null {
|
|
if (typeof window === 'undefined') return null
|
|
|
|
try {
|
|
return new URL(resolveApiBase(), window.location.origin).origin
|
|
} catch {
|
|
return window.location.origin
|
|
}
|
|
}
|
|
|
|
export function resolveRealtimeSocketTarget(): { origin: string; path: string } | null {
|
|
if (typeof window === 'undefined') return null
|
|
|
|
const configuredApiBase = process.env.NEXT_PUBLIC_API_URL
|
|
if (configuredApiBase && !configuredApiBase.startsWith('/')) {
|
|
try {
|
|
return {
|
|
origin: new URL(normalizeApiBase(configuredApiBase)).origin,
|
|
path: '/socket.io',
|
|
}
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
const currentHostname = window.location?.hostname
|
|
if (currentHostname && isLocalBrowserHost(currentHostname)) {
|
|
return {
|
|
origin: 'http://localhost:4000',
|
|
path: '/socket.io',
|
|
}
|
|
}
|
|
|
|
return {
|
|
origin: resolveApiOrigin() ?? window.location.origin,
|
|
path: '/socket.io',
|
|
}
|
|
}
|
|
|
|
export const API_BASE = resolveApiBase()
|
|
|
|
export const EMPLOYEE_PROFILE_KEY = 'employee_profile'
|
|
|
|
export async function apiFetch<T>(path: string, options?: RequestInit): Promise<T> {
|
|
const isFormData = typeof FormData !== 'undefined' && options?.body instanceof FormData
|
|
|
|
const headers: Record<string, string> = {
|
|
...(options?.headers as Record<string, string> ?? {}),
|
|
}
|
|
|
|
if (!isFormData && options?.body !== undefined) {
|
|
headers['Content-Type'] = 'application/json'
|
|
}
|
|
|
|
const res = await fetch(`${resolveApiBase()}${path}`, {
|
|
...options,
|
|
headers,
|
|
credentials: 'include',
|
|
})
|
|
|
|
let json: any
|
|
try {
|
|
json = await res.json()
|
|
} catch {
|
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`)
|
|
}
|
|
|
|
if (!res.ok) {
|
|
const issues = Array.isArray(json?.issues)
|
|
? json.issues
|
|
.map((issue: any) => {
|
|
const path = Array.isArray(issue?.path) && issue.path.length ? `${issue.path.join('.')}: ` : ''
|
|
return issue?.message ? `${path}${issue.message}` : null
|
|
})
|
|
.filter(Boolean)
|
|
: []
|
|
const message = issues.length
|
|
? `${json?.message ?? 'Invalid request'}: ${issues.join('; ')}`
|
|
: (json?.message ?? json?.error ?? `Request failed with status ${res.status}`)
|
|
const err = new Error(message) as any
|
|
err.code = json?.error
|
|
err.statusCode = res.status
|
|
err.issues = json?.issues
|
|
throw err
|
|
}
|
|
|
|
return (json?.data ?? json) as T
|
|
}
|
|
|
|
export async function apiFetchServer<T>(path: string, token: string, options?: RequestInit): Promise<T> {
|
|
const isFormData = typeof FormData !== 'undefined' && options?.body instanceof FormData
|
|
const res = await fetch(`${resolveApiBase()}${path}`, {
|
|
...options,
|
|
headers: {
|
|
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
|
|
'Authorization': `Bearer ${token}`,
|
|
...(options?.headers as Record<string, string> ?? {}),
|
|
},
|
|
cache: 'no-store',
|
|
})
|
|
|
|
let json: any
|
|
try {
|
|
json = await res.json()
|
|
} catch {
|
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`)
|
|
}
|
|
|
|
if (!res.ok) {
|
|
const err = new Error(json?.message ?? `Request failed with status ${res.status}`) as any
|
|
err.statusCode = res.status
|
|
throw err
|
|
}
|
|
|
|
return (json?.data ?? json) as T
|
|
}
|