secutorpro's picture
réorganiser tout en pro achitecte pas un lambda ok tu es un boss
52fac7f verified
Raw
History Blame Contribute Delete
12.3 kB
/**
* 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
};
}