EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
api.js
Go to the documentation of this file.
1// src/lib/api.js
2// Robust API helper that automatically detects environment and constructs proper absolute URLs
3
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';
9 }
10
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';
14 }
15
16 // 3️⃣ Otherwise, build automatically from current domain + /api
17 return `${window.location.origin.replace(/\/$/, '')}/api`;
18})();
19
20console.log('[api.js] Resolved API_BASE:', API_BASE);
21
22// Helper to get clean base (used by apiUrl)
23const getAbsoluteBase = () => {
24 let base = API_BASE;
25
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(/\/?$/, '/');
29 }
30
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(/\/?$/, '/');
35};
36
37
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());
43 }
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;
49 }
50 try {
51 const full = new URL(cleanPath, absBase);
52 return full;
53 } catch (err) {
54 console.error('[apiUrl] Failed to construct URL:', cleanPath, absBase, err);
55 return new URL('/', absBase);
56 }
57};
58
59// Centralized fetch helper — automatically attaches JWT and handles JSON
60export const apiFetch = async (path, options = {}) => {
61 const token = localStorage.getItem('token');
62 const headers = {
63 ...(options.headers || {}),
64 ...(token ? { Authorization: `Bearer ${token}` } : {}),
65 };
66
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';
70 }
71
72 // Decode JWT to check if we're impersonating
73 let isImpersonating = false;
74 let jwtTenantId = null;
75 if (token) {
76 try {
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;
81 } catch (err) {
82 console.warn('[apiFetch] Failed to decode JWT:', err);
83 }
84 }
85
86 let urlObj = apiUrl(path);
87
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'
102 );
103
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);
108 }
109
110 // Logging request details
111 console.log('[apiFetch] Request:', {
112 url: urlObj.toString(),
113 method: options.method || 'GET',
114 isImpersonating,
115 jwtTenantId,
116 selectedTenantId: isImpersonating ? null : selectedTenantId,
117 headers: { ...headers, Authorization: token ? 'Bearer <redacted>' : undefined },
118 options
119 });
120
121 let res;
122 try {
123 res = await fetch(urlObj.toString(), {
124 ...options,
125 headers,
126 credentials: 'include',
127 });
128 } catch (err) {
129 console.error('[apiFetch] Network error:', err, {
130 url: urlObj.toString(),
131 method: options.method || 'GET',
132 isImpersonating,
133 jwtTenantId
134 });
135 throw err;
136 }
137
138 if (!res.ok) {
139 console.warn('[apiFetch] Request failed:', {
140 url: urlObj.toString(),
141 status: res.status,
142 statusText: res.statusText,
143 method: options.method || 'GET',
144 isImpersonating,
145 jwtTenantId
146 });
147 }
148
149 return res;
150};