/** * Security Component for qBit Manage Web UI * Handles authentication settings and user management */ import { API } from '../api.js'; import { showToast } from '../utils/toast.js'; import { EYE_ICON_SVG, EYE_SLASH_ICON_SVG } from '../utils/icons.js'; export class SecurityComponent { constructor(containerId, apiInstance = null) { this.container = document.getElementById(containerId); this.api = apiInstance || new API(); this.currentSettings = null; } async init() { try { await this.loadSecuritySettings(); this.render(); this.bindEvents(); } catch (error) { showToast('Failed to load security settings', 'error'); } } async loadSecuritySettings() { try { const response = await this.api.makeRequest('/security', 'GET'); this.currentSettings = response; } catch (error) { // Use default settings if loading fails this.currentSettings = { enabled: false, method: 'none', bypass_auth_for_local: false, username: '', password_hash: '', api_key: '' }; } } render() { this.container.innerHTML = `

Security Settings

Configure authentication for the qBit Manage web interface. Choose between no authentication or basic HTTP authentication.

None: No authentication required
Basic: Browser popup for username/password authentication
API Only: No auth for web UI, API key required for API requests
When checked, requests from localhost and RFC 1918 private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) won't require authentication

Credentials

Username for authentication
Leave blank to keep current password
Must match the password above

API Key

${this.currentSettings.api_key ? ` ` : ''}
API key for programmatic access. Click "Generate New Key" to create a new one.
`; } bindEvents() { // Cache DOM elements for better performance this.authMethod = document.getElementById('auth-method'); this.localAddressesGroup = document.getElementById('local-addresses-group'); this.credentialsSection = document.getElementById('credentials-section'); this.generateApiKeyBtn = document.getElementById('generate-api-key'); this.clearApiKeyBtn = document.getElementById('clear-api-key'); this.copyApiKeyBtn = document.getElementById('copy-api-key'); this.saveBtn = document.getElementById('save-security-settings'); // Handle authentication method change if (this.authMethod) { this.authMethod.addEventListener('change', () => { this.handleMethodChange(this.authMethod.value); }); } // Handle password visibility toggles const passwordToggles = document.querySelectorAll('.password-toggle'); passwordToggles.forEach(toggle => { toggle.addEventListener('click', (e) => { const targetId = toggle.dataset.target; this.togglePasswordVisibility(targetId); }); }); // Handle generate API key if (this.generateApiKeyBtn) { this.generateApiKeyBtn.addEventListener('click', () => { this.generateApiKey(); }); } // Handle clear API key if (this.clearApiKeyBtn) { this.clearApiKeyBtn.addEventListener('click', () => { this.clearApiKey(); }); } // Handle copy API key if (this.copyApiKeyBtn) { this.copyApiKeyBtn.addEventListener('click', () => { this.copyApiKey(); }); } // Handle save settings if (this.saveBtn) { this.saveBtn.addEventListener('click', () => { this.saveSettings(); }); } } handleMethodChange(method) { const isNone = method === 'none'; const isApiOnly = method === 'api_only'; if (this.localAddressesGroup) { this.localAddressesGroup.style.display = isNone ? 'none' : ''; } if (this.credentialsSection) { this.credentialsSection.style.display = (isNone || isApiOnly) ? 'none' : ''; } } togglePasswordVisibility(targetId) { const input = document.getElementById(targetId); const button = document.querySelector(`[data-target="${targetId}"]`); if (!input || !button) { console.error('Password input or toggle button not found:', targetId); return; } const isPassword = input.type === 'password'; input.type = isPassword ? 'text' : 'password'; button.innerHTML = isPassword ? EYE_SLASH_ICON_SVG : EYE_ICON_SVG; } // Removed maskApiKey and toggleApiKeyVisibility methods - API key is always shown in full async generateApiKey() { if (!confirm('Are you sure you want to generate a new API key? The old key will no longer work.')) { return; } try { // Get current form values to send complete request const method = document.getElementById('auth-method').value; const allowLocalCheckbox = document.getElementById('allow-local'); const allowLocalIps = allowLocalCheckbox && allowLocalCheckbox.checked; const username = document.getElementById('username')?.value.trim() || ''; const password = document.getElementById('password')?.value || ''; const requestData = { enabled: method !== 'none', method: method, bypass_auth_for_local: allowLocalIps, username: username, password: password, generate_api_key: true }; const response = await this.api.makeRequest('/security', 'PUT', requestData); if (response.success) { this.currentSettings.api_key = response.api_key; const input = document.getElementById('api-key-display'); if (input) { input.value = response.api_key; } // Re-render to update button visibility this.render(); this.bindEvents(); showToast('New API key generated successfully', 'success'); } else { showToast(response.message || 'Failed to generate API key', 'error'); } } catch (error) { console.error('Failed to generate API key:', error); showToast('Failed to generate API key', 'error'); } } async clearApiKey() { if (!confirm('Are you sure you want to clear the API key? This will disable API access until a new key is generated.')) { return; } try { // Get current form values to send complete request const method = document.getElementById('auth-method').value; const allowLocalCheckbox = document.getElementById('allow-local'); const allowLocalIps = allowLocalCheckbox && allowLocalCheckbox.checked; const username = document.getElementById('username')?.value.trim() || ''; const password = document.getElementById('password')?.value || ''; const requestData = { enabled: method !== 'none', method: method, bypass_auth_for_local: allowLocalIps, username: username, password: password, clear_api_key: true }; const response = await this.api.makeRequest('/security', 'PUT', requestData); if (response.success) { this.currentSettings.api_key = ''; const input = document.getElementById('api-key-display'); if (input) { input.value = ''; } // Re-render to update button visibility this.render(); this.bindEvents(); showToast('API key cleared successfully', 'success'); } else { showToast(response.message || 'Failed to clear API key', 'error'); } } catch (error) { console.error('Failed to clear API key:', error); showToast('Failed to clear API key', 'error'); } } copyApiKey() { const input = document.getElementById('api-key-display'); if (!input) return; navigator.clipboard.writeText(input.value) .then(() => showToast('API key copied to clipboard', 'success')) .catch(() => showToast('Failed to copy API key', 'error')); } validatePasswordComplexity(password) { const minLength = 8; const hasUpper = /[A-Z]/.test(password); const hasLower = /[a-z]/.test(password); const hasNumber = /\d/.test(password); const hasSpecial = /[!@#$%^&*]/.test(password); // Count how many character types are present const typeCount = [hasUpper, hasLower, hasNumber, hasSpecial].filter(Boolean).length; return password.length >= minLength && typeCount >= 3; } async saveSettings() { try { const method = document.getElementById('auth-method').value; const allowLocalCheckbox = document.getElementById('allow-local'); const allowLocalIps = allowLocalCheckbox && allowLocalCheckbox.checked; const username = document.getElementById('username')?.value.trim() || ''; const password = document.getElementById('password')?.value || ''; const confirmPassword = document.getElementById('confirm-password')?.value || ''; // Validation if (method !== 'none' && method !== 'api_only') { if (!username) { showToast('Username is required', 'error'); return; } if (password && password !== confirmPassword) { showToast('Passwords do not match', 'error'); return; } // Password complexity validation if (password && !this.validatePasswordComplexity(password)) { showToast('Password must be at least 8 characters with at least 3 of: uppercase, lowercase, number, special character', 'error'); return; } } // Prepare request data const requestData = { enabled: method !== 'none', method: method, bypass_auth_for_local: allowLocalIps, username: username, password: password, generate_api_key: false }; // Make API request const response = await this.api.makeRequest('/security', 'PUT', requestData); if (response.success) { showToast('Security settings saved successfully', 'success'); // Update current settings this.currentSettings = { enabled: method !== 'none', method: method, bypass_auth_for_local: allowLocalIps, username: username, password_hash: password ? '***' : this.currentSettings.password_hash, api_key: this.currentSettings.api_key }; // Clear password fields const passwordField = document.getElementById('password'); const confirmPasswordField = document.getElementById('confirm-password'); if (passwordField) passwordField.value = ''; if (confirmPasswordField) confirmPasswordField.value = ''; } else { showToast(response.message || 'Failed to save settings', 'error'); } } catch (error) { console.error('Failed to save security settings:', error); showToast('Failed to save security settings', 'error'); } } async resetSettings() { if (confirm('Are you sure you want to reset security settings? This will disable authentication.')) { try { await this.loadSecuritySettings(); this.render(); this.bindEvents(); showToast('Security settings reset', 'info'); } catch (error) { console.error('Failed to reset settings:', error); showToast('Failed to reset settings', 'error'); } } } }