2// Robust API helper that automatically detects environment and constructs proper absolute URLs
4// Detect the API base dynamically or via .env
5export const API_BASE = (() => {
6 // 1️⃣ Prefer explicit env setting (for production)
7 if (import.meta.env.VITE_API_URL?.trim()) {
8 return import.meta.env.VITE_API_URL.trim().replace(/\/$/, '') || 'https://everydaytech.au/api';
11 // 2️⃣ Fallback: if we’re on localhost (vite dev), point to backend port 3000
12 if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
13 return 'http://localhost:3000/api';
16 // 3️⃣ Otherwise, build automatically from current domain + /api
17 return `${window.location.origin.replace(/\/$/, '')}/api`;
20console.log('[api.js] Resolved API_BASE:', API_BASE);
22// Helper to get clean base (used by apiUrl)
23const getAbsoluteBase = () => {
26 // If it's an absolute URL, ensure it ends with a slash
27 if (base.startsWith('http://') || base.startsWith('https://')) {
28 return base.replace(/\/?$/, '/');
31 // Otherwise build from origin and ensure trailing slash
32 const origin = window.location.origin.replace(/\/$/, '');
33 const clean = base.startsWith('/') ? base : `/${base}`;
34 return `${origin}${clean}`.replace(/\/?$/, '/');
38// Build a full API URL from relative path (e.g. '/tickets' → https://demo.everydayoffice.au/api/tickets)
39export const apiUrl = (path = '/') => {
40 // Always produce a valid URL, even for empty or malformed paths
41 if (!path || typeof path !== 'string' || path.trim() === '') {
42 return new URL('/', getAbsoluteBase());
44 let cleanPath = String(path).replace(/^\//, '');
45 const absBase = getAbsoluteBase();
46 // If path is only a query string (e.g. '?search=foo'), prepend a slash
47 if (cleanPath.startsWith('?')) {
48 cleanPath = '/' + cleanPath;
51 const full = new URL(cleanPath, absBase);
54 console.error('[apiUrl] Failed to construct URL:', cleanPath, absBase, err);
55 return new URL('/', absBase);
59// Centralized fetch helper — automatically attaches JWT and handles JSON
60export const apiFetch = async (path, options = {}) => {
61 const token = localStorage.getItem('token');
63 ...(options.headers || {}),
64 ...(token ? { Authorization: `Bearer ${token}` } : {}),
67 // If body is present and it's a string (JSON), automatically add Content-Type
68 if (options.body && typeof options.body === 'string') {
69 headers['Content-Type'] = headers['Content-Type'] || 'application/json';
72 // Decode JWT to check if we're impersonating
73 let isImpersonating = false;
74 let jwtTenantId = null;
77 const payload = token.split('.')[1];
78 const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
79 isImpersonating = !!decoded.impersonated;
80 jwtTenantId = decoded.tenant_id || decoded.tenantId || null;
82 console.warn('[apiFetch] Failed to decode JWT:', err);
86 let urlObj = apiUrl(path);
88 // Do NOT add tenant_id query param when impersonating - backend reads it from JWT
89 // Only add tenant_id for global/root-only endpoints that need explicit tenant override
90 const pathNoQuery = String(path).split('?')[0].replace(/\\/g, '/');
91 const isGlobalEndpoint = (
92 pathNoQuery === 'tenants' ||
93 pathNoQuery === '/tenants' ||
94 pathNoQuery === 'tenants/' ||
95 pathNoQuery === '/tenants/' ||
96 pathNoQuery === 'tenants/current' ||
97 pathNoQuery === '/tenants/current' ||
98 pathNoQuery === 'auth/impersonate' ||
99 pathNoQuery === '/auth/impersonate' ||
100 pathNoQuery === 'auth/exit-impersonation' ||
101 pathNoQuery === '/auth/exit-impersonation'
104 // Legacy support: Only use localStorage tenant selection if NOT impersonating
105 let selectedTenantId = (localStorage.getItem('selectedTenantId') || '').trim().replace(/['"]/g, '');
106 if (!isImpersonating && selectedTenantId && selectedTenantId !== '' && !isGlobalEndpoint) {
107 urlObj.searchParams.set('tenant_id', selectedTenantId);
110 // Logging request details
111 console.log('[apiFetch] Request:', {
112 url: urlObj.toString(),
113 method: options.method || 'GET',
116 selectedTenantId: isImpersonating ? null : selectedTenantId,
117 headers: { ...headers, Authorization: token ? 'Bearer <redacted>' : undefined },
123 res = await fetch(urlObj.toString(), {
126 credentials: 'include',
129 console.error('[apiFetch] Network error:', err, {
130 url: urlObj.toString(),
131 method: options.method || 'GET',
139 console.warn('[apiFetch] Request failed:', {
140 url: urlObj.toString(),
142 statusText: res.statusText,
143 method: options.method || 'GET',