Spaces:
Configuration error
Configuration error
| /** | |
| * 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 | |
| }; | |
| } |