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.
272 lines
8.4 KiB
JavaScript
272 lines
8.4 KiB
JavaScript
// Authentication Module for LLM Proxy Dashboard
|
|
|
|
class AuthManager {
|
|
constructor() {
|
|
this.isAuthenticated = false;
|
|
this.token = null;
|
|
this.user = null;
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
// Check for existing session
|
|
const savedToken = localStorage.getItem('dashboard_token');
|
|
const savedUser = localStorage.getItem('dashboard_user');
|
|
|
|
if (savedToken && savedUser) {
|
|
this.token = savedToken;
|
|
this.user = JSON.parse(savedUser);
|
|
this.isAuthenticated = true;
|
|
this.showDashboard();
|
|
} else {
|
|
this.showLogin();
|
|
}
|
|
|
|
// Setup login form
|
|
this.setupLoginForm();
|
|
this.setupLogout();
|
|
}
|
|
|
|
setupLoginForm() {
|
|
const loginForm = document.getElementById('login-form');
|
|
if (!loginForm) return;
|
|
|
|
loginForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const username = document.getElementById('username').value;
|
|
const password = document.getElementById('password').value;
|
|
|
|
await this.login(username, password);
|
|
});
|
|
}
|
|
|
|
setupLogout() {
|
|
const logoutBtn = document.getElementById('logout-btn');
|
|
if (!logoutBtn) return;
|
|
|
|
logoutBtn.addEventListener('click', () => {
|
|
this.logout();
|
|
});
|
|
}
|
|
|
|
setToken(newToken) {
|
|
if (!newToken) return;
|
|
this.token = newToken;
|
|
localStorage.setItem('dashboard_token', this.token);
|
|
}
|
|
|
|
async login(username, password) {
|
|
const errorElement = document.getElementById('login-error');
|
|
const loginBtn = document.querySelector('.login-btn');
|
|
|
|
try {
|
|
loginBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Authenticating...';
|
|
loginBtn.disabled = true;
|
|
|
|
const response = await fetch('/api/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username, password })
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
this.token = result.data.token;
|
|
this.user = result.data.user;
|
|
|
|
localStorage.setItem('dashboard_token', this.token);
|
|
localStorage.setItem('dashboard_user', JSON.stringify(this.user));
|
|
|
|
this.isAuthenticated = true;
|
|
this.showDashboard();
|
|
this.showToast('Successfully logged in!', 'success');
|
|
} else {
|
|
throw new Error(result.error || 'Invalid credentials');
|
|
}
|
|
} catch (error) {
|
|
errorElement.style.display = 'flex';
|
|
errorElement.querySelector('span').textContent = error.message;
|
|
loginBtn.innerHTML = '<i class="fas fa-sign-in-alt"></i> Sign In';
|
|
loginBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
async logout() {
|
|
// Revoke server-side session first
|
|
try {
|
|
if (this.token) {
|
|
await fetch('/api/auth/logout', {
|
|
method: 'POST',
|
|
headers: { 'Authorization': `Bearer ${this.token}` }
|
|
});
|
|
}
|
|
} catch (e) {
|
|
// Best-effort — still clear local state even if server call fails
|
|
console.warn('Server logout failed:', e);
|
|
}
|
|
|
|
// Clear localStorage
|
|
localStorage.removeItem('dashboard_token');
|
|
localStorage.removeItem('dashboard_user');
|
|
|
|
// Reset state
|
|
this.isAuthenticated = false;
|
|
this.token = null;
|
|
this.user = null;
|
|
|
|
// Show login screen
|
|
this.showLogin();
|
|
|
|
// Show logout message
|
|
this.showToast('Successfully logged out', 'info');
|
|
}
|
|
|
|
showLogin() {
|
|
const loginScreen = document.getElementById('login-screen');
|
|
const dashboard = document.getElementById('dashboard');
|
|
|
|
if (loginScreen) loginScreen.style.display = 'flex';
|
|
if (dashboard) dashboard.style.display = 'none';
|
|
|
|
// Clear form
|
|
const loginForm = document.getElementById('login-form');
|
|
if (loginForm) loginForm.reset();
|
|
|
|
// Hide error
|
|
const errorElement = document.getElementById('login-error');
|
|
if (errorElement) errorElement.style.display = 'none';
|
|
|
|
// Reset button
|
|
const loginBtn = document.querySelector('.login-btn');
|
|
if (loginBtn) {
|
|
loginBtn.innerHTML = '<i class="fas fa-sign-in-alt"></i> Sign In';
|
|
loginBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
showDashboard() {
|
|
const loginScreen = document.getElementById('login-screen');
|
|
const dashboard = document.getElementById('dashboard');
|
|
|
|
if (loginScreen) loginScreen.style.display = 'none';
|
|
if (dashboard) dashboard.style.display = 'flex';
|
|
|
|
// Update user info in sidebar
|
|
this.updateUserInfo();
|
|
|
|
// Initialize dashboard components
|
|
if (typeof window.initDashboard === 'function') {
|
|
window.initDashboard();
|
|
}
|
|
}
|
|
|
|
updateUserInfo() {
|
|
const userNameElement = document.querySelector('.user-name');
|
|
const userRoleElement = document.querySelector('.user-role');
|
|
|
|
if (userNameElement && this.user) {
|
|
userNameElement.textContent = this.user.name || this.user.username || 'User';
|
|
}
|
|
|
|
if (userRoleElement && this.user) {
|
|
const roleLabels = { admin: 'Administrator', viewer: 'Viewer' };
|
|
userRoleElement.textContent = roleLabels[this.user.role] || this.user.role || 'User';
|
|
}
|
|
}
|
|
|
|
getAuthHeaders() {
|
|
if (!this.token) return {};
|
|
|
|
return {
|
|
'Authorization': `Bearer ${this.token}`,
|
|
'Content-Type': 'application/json'
|
|
};
|
|
}
|
|
|
|
async fetchWithAuth(url, options = {}) {
|
|
const headers = this.getAuthHeaders();
|
|
|
|
const response = await fetch(url, {
|
|
...options,
|
|
headers: {
|
|
...headers,
|
|
...options.headers
|
|
}
|
|
});
|
|
|
|
if (response.status === 401) {
|
|
// Token expired or invalid
|
|
this.logout();
|
|
throw new Error('Authentication required');
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
showToast(message, type = 'info') {
|
|
// Create toast container if it doesn't exist
|
|
let container = document.querySelector('.toast-container');
|
|
if (!container) {
|
|
container = document.createElement('div');
|
|
container.className = 'toast-container';
|
|
document.body.appendChild(container);
|
|
}
|
|
|
|
// Create toast
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast ${type}`;
|
|
|
|
// Set icon based on type
|
|
let icon = 'info-circle';
|
|
switch (type) {
|
|
case 'success':
|
|
icon = 'check-circle';
|
|
break;
|
|
case 'error':
|
|
icon = 'exclamation-circle';
|
|
break;
|
|
case 'warning':
|
|
icon = 'exclamation-triangle';
|
|
break;
|
|
}
|
|
|
|
toast.innerHTML = `
|
|
<i class="fas fa-${icon} toast-icon"></i>
|
|
<div class="toast-content">
|
|
<div class="toast-title">${type.charAt(0).toUpperCase() + type.slice(1)}</div>
|
|
<div class="toast-message">${message}</div>
|
|
</div>
|
|
<button class="toast-close">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
`;
|
|
|
|
// Add close functionality
|
|
const closeBtn = toast.querySelector('.toast-close');
|
|
closeBtn.addEventListener('click', () => {
|
|
toast.remove();
|
|
});
|
|
|
|
// Add to container
|
|
container.appendChild(toast);
|
|
|
|
// Auto-remove after 5 seconds
|
|
setTimeout(() => {
|
|
if (toast.parentNode) {
|
|
toast.remove();
|
|
}
|
|
}, 5000);
|
|
}
|
|
}
|
|
|
|
// Initialize auth manager when DOM is loaded
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
window.authManager = new AuthManager();
|
|
});
|
|
|
|
// Export for use in other modules
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = AuthManager;
|
|
} |