306 lines
7.8 KiB
JavaScript
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;
|