Files
org-parking/frontend/js/api.js
2026-02-12 22:44:44 +01:00

306 lines
7.8 KiB
JavaScript

/**
* 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;