This commit introduces: - AES-256-GCM encryption for LLM provider API keys in the database. - HMAC-SHA256 signed session tokens with activity-based refresh logic. - Standardized frontend XSS protection using a global escapeHtml utility. - Hardened security headers and request body size limits. - Improved database integrity with foreign key enforcement and atomic transactions. - Integration tests for the full encrypted key storage and proxy usage lifecycle.
124 lines
3.4 KiB
JavaScript
124 lines
3.4 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) {
|
|
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;
|
|
};
|