/** * API CLIENT SYSTEM * Professional REST API Client */ class APIClient { constructor(baseURL, options = {}) { this.baseURL = baseURL; this.options = { timeout: 10000, retries: 3, retryDelay: 1000, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, ...options }; this.requestInterceptors = []; this.responseInterceptors = []; this.authToken = null; } // Request Interceptors useRequestInterceptor(interceptor) { this.requestInterceptors.push(interceptor); } // Response Interceptors useResponseInterceptor(interceptor) { this.responseInterceptors.push(interceptor); } // Authentication setAuthToken(token) { this.authToken = token; this.options.headers['Authorization'] = `Bearer ${token}`; } // HTTP Methods async get(url, options = {}) { return this.request('GET', url, null, options); } async post(url, data = null, options = {}) { return this.request('POST', url, data, options); } async put(url, data = null, options = {}) { return this.request('PUT', url, data, options); } async patch(url, data = null, options = {}) { return this.request('PATCH', url, data, options); } async delete(url, options = {}) { return this.request('DELETE', url, null, options); } // Main Request Method async request(method, url, data = null, options = {}) { const config = { method, headers: { ...this.options.headers }, ...options }; // Add auth token if available if (this.authToken) { config.headers['Authorization'] = `Bearer ${this.authToken}`; } // Add data for POST/PUT/PATCH requests if (data && ['POST', 'PUT', 'PATCH'].includes(method)) { config.body = JSON.stringify(data); } // Apply request interceptors let finalUrl = this.baseURL + url; for (const interceptor of this.requestInterceptors) { const result = interceptor({ url: finalUrl, method, headers: config.headers, body: config.body }); if (result) { finalUrl = result.url || finalUrl; config.headers = result.headers || config.headers; config.body = result.body || config.body; } } let attempt = 0; const maxAttempts = options.retries || this.options.retries; while (attempt <= maxAttempts) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.options.timeout); const response = await fetch(finalUrl, { ...config, signal: controller.signal }); clearTimeout(timeoutId); // Handle HTTP errors if (!response.ok) { throw new HTTPError(response.status, response.statusText, await response.text()); } // Parse response const contentType = response.headers.get('content-type'); let result; if (contentType && contentType.includes('application/json')) { result = await response.json(); } else { result = await response.text(); } // Apply response interceptors for (const interceptor of this.responseInterceptors) { result = interceptor(result, response) || result; } return result; } catch (error) { attempt++; // Don't retry on certain errors if (error instanceof HTTPError && [400, 401, 403, 422].includes(error.status)) { throw error; } // Retry logic if (attempt <= maxAttempts) { console.warn(`Request failed (attempt ${attempt}/${maxAttempts}), retrying...`, error); await this.delay(this.options.retryDelay * attempt); continue; } throw error; } } } delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // Utility Methods setHeader(key, value) { this.options.headers[key] = value; } removeHeader(key) { delete this.options.headers[key]; } // File Upload async upload(url, file, onProgress = null) { const formData = new FormData(); formData.append('file', file); const config = { method: 'POST', headers: { // Don't set Content-Type for FormData, let browser set it with boundary } }; if (onProgress) { return this.uploadWithProgress(url, formData, onProgress, config); } return this.request('POST', url, formData, config); } async uploadWithProgress(url, formData, onProgress, config) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.upload.addEventListener('progress', (event) => { if (event.lengthComputable) { const percentComplete = (event.loaded / event.total) * 100; onProgress(percentComplete); } }); xhr.addEventListener('load', () => { if (xhr.status >= 200 && xhr.status < 300) { try { const result = JSON.parse(xhr.responseText); resolve(result); } catch (e) { resolve(xhr.responseText); } } else { reject(new HTTPError(xhr.status, xhr.statusText, xhr.responseText)); } }); xhr.addEventListener('error', () => { reject(new Error('Upload failed')); }); xhr.open('POST', this.baseURL + url); // Add auth header if (this.authToken) { xhr.setRequestHeader('Authorization', `Bearer ${this.authToken}`); } xhr.send(formData); }); } } // HTTP Error Class class HTTPError extends Error { constructor(status, statusText, body) { super(`${status} ${statusText}`); this.status = status; this.statusText = statusText; this.body = body; this.name = 'HTTPError'; } } // Specialized API Classes class PatrolAPI extends APIClient { constructor() { super('/api/v1'); this.setupInterceptors(); } setupInterceptors() { // Add auth token from storage this.useRequestInterceptor((config) => { const token = localStorage.getItem('authToken'); if (token) { config.headers['Authorization'] = `Bearer ${token}`; } return config; }); // Handle token refresh on 401 this.useResponseInterceptor(async (response, originalResponse) => { if (originalResponse.status === 401) { const refreshed = await this.refreshToken(); if (refreshed) { // Retry original request return this.request('GET', '/health'); } } return response; }); } // Control Points async getControlPoints(filters = {}) { const params = new URLSearchParams(filters); return this.get(`/control-points?${params}`); } async getControlPoint(id) { return this.get(`/control-points/${id}`); } async createControlPoint(data) { return this.post('/control-points', data); } async updateControlPoint(id, data) { return this.put(`/control-points/${id}`, data); } async deleteControlPoint(id) { return this.delete(`/control-points/${id}`); } // Rounds async getRounds(filters = {}) { const params = new URLSearchParams(filters); return this.get(`/rounds?${params}`); } async getRound(id) { return this.get(`/rounds/${id}`); } async createRound(data) { return this.post('/rounds', data); } async updateRound(id, data) { return this.put(`/rounds/${id}`, data); } async startRound(id) { return this.post(`/rounds/${id}/start`); } async completeRound(id) { return this.post(`/rounds/${id}/complete`); } // Guards async getGuards(filters = {}) { const params = new URLSearchParams(filters); return this.get(`/guards?${params}`); } async getGuard(id) { return this.get(`/guards/${id}`); } async createGuard(data) { return this.post('/guards', data); } async updateGuard(id, data) { return this.put(`/guards/${id}`, data); } // Companies async getCompanies(filters = {}) { const params = new URLSearchParams(filters); return this.get(`/companies?${params}`); } async getCompany(id) { return this.get(`/companies/${id}`); } async createCompany(data) { return this.post('/companies', data); } // Reports async generateReport(type, params = {}) { return this.post('/reports/generate', { type, params }); } async getReports(filters = {}) { const params = new URLSearchParams(filters); return this.get(`/reports?${params}`); } // NFC Scanning async scanNFC(nfcId, roundId) { return this.post('/nfc/scan', { nfcId, roundId }); } // Incidents async getIncidents(filters = {}) { const params = new URLSearchParams(filters); return this.get(`/incidents?${params}`); } async createIncident(data) { return this.post('/incidents', data); } async updateIncident(id, data) { return this.put(`/incidents/${id}`, data); } // Authentication async login(credentials) { const response = await this.post('/auth/login', credentials); if (response.token) { this.setAuthToken(response.token); localStorage.setItem('authToken', response.token); } return response; } async logout() { try { await this.post('/auth/logout'); } finally { this.clearAuth(); } } async refreshToken() { try { const response = await this.post('/auth/refresh'); if (response.token) { this.setAuthToken(response.token); localStorage.setItem('authToken', response.token); return true; } } catch (error) { this.clearAuth(); return false; } } clearAuth() { this.authToken = null; localStorage.removeItem('authToken'); delete this.options.headers['Authorization']; } } // Real-time API for WebSocket connections class RealtimeAPI { constructor(url) { this.url = url; this.ws = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.reconnectInterval = 1000; this.listeners = new Map(); } connect() { if (this.ws?.readyState === WebSocket.OPEN) return; this.ws = new WebSocket(this.url); this.ws.onopen = () => { console.log('WebSocket connected'); this.reconnectAttempts = 0; this.emit('connected'); }; this.ws.onmessage = (event) => { try { const data = JSON.parse(event.data); this.emit(data.type, data); } catch (error) { console.error('Failed to parse WebSocket message:', error); } }; this.ws.onclose = () => { console.log('WebSocket disconnected'); this.emit('disconnected'); this.reconnect(); }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); this.emit('error', error); }; } reconnect() { if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; const delay = this.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1); setTimeout(() => { console.log(`Reconnecting... (attempt ${this.reconnectAttempts})`); this.connect(); }, delay); } else { console.error('Max reconnection attempts reached'); this.emit('maxReconnectAttemptsReached'); } } send(type, data) { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type, data })); } } on(event, callback) { if (!this.listeners.has(event)) { this.listeners.set(event, new Set()); } this.listeners.get(event).add(callback); } off(event, callback) { const listeners = this.listeners.get(event); if (listeners) { listeners.delete(callback); } } emit(event, data) { const listeners = this.listeners.get(event); if (listeners) { listeners.forEach(callback => callback(data)); } } disconnect() { if (this.ws) { this.ws.close(); this.ws = null; } } } // Export classes if (typeof module !== 'undefined' && module.exports) { module.exports = { APIClient, HTTPError, PatrolAPI, RealtimeAPI }; }