/** * API Client Wrapper * Centralized API communication with auth handling */ const api = { /** * Get the auth token from localStorage */ getToken() { return localStorage.getItem('access_token'); }, /** * Set the auth token */ setToken(token) { localStorage.setItem('access_token', token); }, /** * Clear the auth token */ clearToken() { localStorage.removeItem('access_token'); this.clearCache(); }, /** * Get data with caching - Returns Response obj or Mock Response * @param {string} url - API endpoint * @param {number} ttlMinutes - Time to live in minutes (default 60) */ async getCached(url, ttlMinutes = 60) { const cacheKey = 'cache_' + url; const cachedItem = localStorage.getItem(cacheKey); if (cachedItem) { try { const { data, timestamp } = JSON.parse(cachedItem); const age = (Date.now() - timestamp) / 1000 / 60; if (age < ttlMinutes) { console.log(`[Cache] Hit for ${url}`); // Return a mock response-like object return { ok: true, status: 200, json: async () => data }; } else { console.log(`[Cache] Expired for ${url}`); localStorage.removeItem(cacheKey); } } catch (e) { console.error('[Cache] Error parsing cache', e); localStorage.removeItem(cacheKey); } } console.log(`[Cache] Miss for ${url}`); const response = await this.get(url); if (response && response.ok) { try { // Clone response to read body and still return it to caller const clone = response.clone(); const data = await clone.json(); localStorage.setItem(cacheKey, JSON.stringify({ data: data, timestamp: Date.now() })); } catch (e) { console.warn('[Cache] failed to save to localStorage', e); } } return response; }, /** * Invalidate specific cache key */ invalidateCache(url) { localStorage.removeItem('cache_' + url); console.log(`[Cache] Invalidated ${url}`); }, /** * Clear all API cache */ clearCache() { Object.keys(localStorage).forEach(key => { if (key.startsWith('cache_')) { localStorage.removeItem(key); } }); console.log('[Cache] Cleared all cache'); }, /** * Check if user is authenticated (token or Authelia) */ isAuthenticated() { return !!this.getToken() || this._autheliaAuth; }, /** * Check authentication - works with both JWT and Authelia * Call this on page load to verify auth status */ async checkAuth() { const url = '/api/auth/me'; // 1. Try Cache (Short TTL: 5 min) const cacheKey = 'cache_' + url; const cachedItem = localStorage.getItem(cacheKey); if (cachedItem) { try { const { data, timestamp } = JSON.parse(cachedItem); if ((Date.now() - timestamp) / 1000 / 60 < 5) { this._autheliaAuth = true; return data; } } catch (e) { localStorage.removeItem(cacheKey); } } // 2. Fetch from Network const response = await fetch(url, { headers: this.getToken() ? { 'Authorization': `Bearer ${this.getToken()}` } : {} }); if (response.ok) { this._autheliaAuth = true; const data = await response.json(); // Save to Cache localStorage.setItem(cacheKey, JSON.stringify({ data: data, timestamp: Date.now() })); return data; } this._autheliaAuth = false; return null; }, /** * Redirect to login if not authenticated * Returns user object if authenticated, null otherwise */ async requireAuth() { const user = await this.checkAuth(); if (!user) { window.location.href = '/login'; return null; } return user; }, /** * Make an API request */ async request(method, url, data = null) { const options = { method, headers: {} }; const token = this.getToken(); if (token) { options.headers['Authorization'] = `Bearer ${token}`; } if (data) { options.headers['Content-Type'] = 'application/json'; options.body = JSON.stringify(data); } const response = await fetch(url, options); // Handle 401 - redirect to login if (response.status === 401) { this.clearToken(); window.location.href = '/login'; return null; } return response; }, /** * GET request */ async get(url) { return this.request('GET', url); }, /** * POST request */ async post(url, data) { return this.request('POST', url, data); }, /** * PUT request */ async put(url, data) { return this.request('PUT', url, data); }, /** * PATCH request */ async patch(url, data) { return this.request('PATCH', url, data); }, /** * DELETE request */ async delete(url) { return this.request('DELETE', url); }, /** * Get current user info */ async getCurrentUser() { const response = await this.get('/api/auth/me'); if (response && response.ok) { return await response.json(); } return null; }, /** * Login */ async login(email, password) { const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) }); if (response.ok) { const data = await response.json(); this.setToken(data.access_token); return { success: true }; } const error = await response.json(); return { success: false, error: error.detail || 'Login fallito' }; }, /** * Register */ async register(email, password, name) { const response = await fetch('/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password, name }) }); if (response.ok) { const data = await response.json(); this.setToken(data.access_token); return { success: true }; } const error = await response.json(); return { success: false, error: error.detail || 'Registrazione fallita' }; }, /** * Logout */ async logout() { // Fetch config to check for external logout URL let logoutUrl = '/login'; try { const configRes = await this.get('/api/auth/config'); if (configRes && configRes.ok) { const config = await configRes.json(); if (config.authelia_enabled && config.logout_url) { logoutUrl = config.logout_url; } } } catch (e) { console.error('Error fetching logout config', e); } await this.post('/api/auth/logout', {}); this.clearToken(); window.location.href = logoutUrl; } }; // Make globally available window.api = api;