This commit updates the frontend API client to intercept authentication errors (like a stale session after a server restart) and immediately clear the local storage and show the login screen. It also adds an onsubmit handler to the login form in index.html to prevent the browser from defaulting to a GET request that puts credentials in the URL if JavaScript fails to initialize or encounters an error.
135 lines
3.9 KiB
JavaScript
135 lines
3.9 KiB
JavaScript
// Unified API client for the dashboard
|
|
|
|
class ApiClient {
|
|
constructor() {
|
|
this.baseUrl = '/api';
|
|
}
|
|
|
|
async request(path, options = {}) {
|
|
const url = `${this.baseUrl}${path}`;
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
...options.headers
|
|
};
|
|
|
|
// Add auth token if available
|
|
if (window.authManager && window.authManager.token) {
|
|
headers['Authorization'] = `Bearer ${window.authManager.token}`;
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
...options,
|
|
headers
|
|
});
|
|
|
|
const text = await response.text();
|
|
|
|
let result;
|
|
try {
|
|
result = JSON.parse(text);
|
|
} catch (parseErr) {
|
|
throw new Error(`JSON parse failed for ${url}: ${parseErr.message}`);
|
|
}
|
|
|
|
if (!response.ok || !result.success) {
|
|
// Handle authentication errors (session expired, server restarted, etc.)
|
|
if (response.status === 401 ||
|
|
result.error === 'Session expired or invalid' ||
|
|
result.error === 'Not authenticated' ||
|
|
result.error === 'Admin access required') {
|
|
|
|
if (window.authManager) {
|
|
// Try to logout to clear local state and show login screen
|
|
window.authManager.logout();
|
|
}
|
|
}
|
|
throw new Error(result.error || `HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
// Handling X-Refreshed-Token header
|
|
if (response.headers.get('X-Refreshed-Token') && window.authManager) {
|
|
window.authManager.token = response.headers.get('X-Refreshed-Token');
|
|
if (window.authManager.setToken) {
|
|
window.authManager.setToken(window.authManager.token);
|
|
}
|
|
}
|
|
|
|
return result.data;
|
|
}
|
|
|
|
async get(path) {
|
|
return this.request(path, { method: 'GET' });
|
|
}
|
|
|
|
async post(path, body) {
|
|
return this.request(path, {
|
|
method: 'POST',
|
|
body: JSON.stringify(body)
|
|
});
|
|
}
|
|
|
|
async put(path, body) {
|
|
return this.request(path, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(body)
|
|
});
|
|
}
|
|
|
|
async delete(path) {
|
|
return this.request(path, { method: 'DELETE' });
|
|
}
|
|
|
|
// Helper for formatting large numbers
|
|
formatNumber(num) {
|
|
if (num >= 1000000) {
|
|
return (num / 1000000).toFixed(1) + 'M';
|
|
}
|
|
if (num >= 1000) {
|
|
return (num / 1000).toFixed(1) + 'K';
|
|
}
|
|
return num.toString();
|
|
}
|
|
|
|
// Helper for formatting currency
|
|
formatCurrency(amount) {
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 4
|
|
}).format(amount);
|
|
}
|
|
|
|
// Helper for relative time
|
|
formatTimeAgo(dateStr) {
|
|
if (!dateStr) return 'Never';
|
|
const date = luxon.DateTime.fromISO(dateStr);
|
|
return date.toRelative();
|
|
}
|
|
|
|
// Helper for escaping HTML
|
|
escapeHtml(unsafe) {
|
|
if (unsafe === undefined || unsafe === null) return '';
|
|
return unsafe.toString()
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
}
|
|
|
|
window.api = new ApiClient();
|
|
|
|
// Helper: waits until chartManager is available (defensive against load-order edge cases)
|
|
window.waitForChartManager = async (timeoutMs = 3000) => {
|
|
if (window.chartManager) return window.chartManager;
|
|
const start = Date.now();
|
|
while (Date.now() - start < timeoutMs) {
|
|
await new Promise(r => setTimeout(r, 50));
|
|
if (window.chartManager) return window.chartManager;
|
|
}
|
|
console.warn('chartManager not available after', timeoutMs, 'ms');
|
|
return null;
|
|
};
|