From 5b46beb48d4a0fc81b5d90fb5f76d9be226085e6 Mon Sep 17 00:00:00 2001 From: Nirvana Date: Sun, 21 Dec 2025 14:38:00 +0100 Subject: [PATCH] Add web config --- resources/web/config.js | 461 ++++++++++++++++++++++++++-------------- 1 file changed, 305 insertions(+), 156 deletions(-) diff --git a/resources/web/config.js b/resources/web/config.js index bb3687a..90ef6e1 100644 --- a/resources/web/config.js +++ b/resources/web/config.js @@ -51,120 +51,6 @@ function switchTab(tabId) { } } -// Helper function to format timestamp to local date/time -function formatDateTime(timestamp) { - if (!timestamp) return null; - const date = new Date(timestamp * 1000); // Convert Unix timestamp to milliseconds - const day = String(date.getDate()).padStart(2, '0'); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const year = date.getFullYear(); - const hours = String(date.getHours()).padStart(2, '0'); - const minutes = String(date.getMinutes()).padStart(2, '0'); - return `${day}.${month}.${year} ${hours}:${minutes}`; -} - -// Helper function to format relative time -function formatRelativeTime(seconds) { - if (!seconds && seconds !== 0) return null; - - const absSeconds = Math.abs(seconds); - const isPast = seconds < 0; - - if (absSeconds < 60) { - return isPast ? 'just expired' : 'in less than a minute'; - } - - const minutes = Math.floor(absSeconds / 60); - if (minutes < 60) { - const text = minutes === 1 ? '1 minute' : `${minutes} minutes`; - return isPast ? `${text} ago` : `in ${text}`; - } - - const hours = Math.floor(minutes / 60); - if (hours < 24) { - const text = hours === 1 ? '1 hour' : `${hours} hours`; - return isPast ? `${text} ago` : `in ${text}`; - } - - const days = Math.floor(hours / 24); - const text = days === 1 ? '1 day' : `${days} days`; - return isPast ? `${text} ago` : `in ${text}`; -} - -// Helper function to get authentication type description -function getAuthTypeDescription(status) { - if (!status || !status.auth_type) { - return 'Unknown authentication'; - } - - const authType = status.auth_type; - - // Map auth types to readable descriptions - const authTypeMap = { - 'user_credentials': 'User Credentials authentication', - 'client_credentials': 'Client Credentials authentication', - 'anonymous': 'Anonymous authentication', - 'network_based': 'Network-based authentication', - 'device_registration': 'Device Registration authentication', - 'embedded_client': 'Embedded Client authentication' - }; - - return authTypeMap[authType] || `${authType.replace(/_/g, ' ')} authentication`; -} - -// Helper function to format token expiration info -function formatTokenExpiration(status) { - if (!status) return ''; - - let expirationHTML = ''; - - // Main token expiration - if (status.token_expires_at) { - const formattedDate = formatDateTime(status.token_expires_at); - const relativeTime = formatRelativeTime(status.token_expires_in_seconds); - - if (status.token_expires_in_seconds < 0) { - // Expired - expirationHTML += ` -
- Token expired ${relativeTime} -
- `; - } else { - // Valid - expirationHTML += ` -
- Token expires: ${formattedDate} (${relativeTime}) -
- `; - } - } - - // Refresh token expiration (if available) - if (status.refresh_token_expires_at) { - const formattedDate = formatDateTime(status.refresh_token_expires_at); - const relativeTime = formatRelativeTime(status.refresh_token_expires_in_seconds); - - if (status.refresh_token_expires_in_seconds < 0) { - // Expired - expirationHTML += ` -
- Refresh token expired ${relativeTime} -
- `; - } else { - // Valid - expirationHTML += ` -
- Refresh token expires: ${formattedDate} (${relativeTime}) -
- `; - } - } - - return expirationHTML; -} - // Load providers from API async function loadProviders() { try { @@ -208,6 +94,7 @@ async function loadAuthStatus() { } } +// Render credentials forms // Render credentials forms async function renderCredentialsForms() { if (providers.length === 0) { @@ -237,6 +124,7 @@ async function renderCredentialsForms() { // Get auth information from provider const auth = provider.auth || {}; + const supportedAuthTypes = auth.supported_auth_types || []; // Determine UI based on auth properties const needsUserCredentials = auth.needs_user_credentials || false; @@ -257,6 +145,7 @@ async function renderCredentialsForms() { // Create form content based on auth type let formContent = ''; + let authDescription = ''; if (needsUserCredentials) { // User credentials form @@ -285,10 +174,11 @@ async function renderCredentialsForms() { `; + authDescription = 'Requires username and password'; } else if (needsClientCredentials && !needsUserCredentials) { // Client credentials only (no user input needed) formContent = ` @@ -296,8 +186,10 @@ async function renderCredentialsForms() { Client Credentials Only

This provider uses hardcoded client credentials that don't require manual setup.

+ Authentication type: ${auth.preferred_auth_type || 'client_credentials'} `; + authDescription = 'Uses client credentials'; } else if (isAnonymous || isNetworkBased || usesEmbeddedClient) { // No credentials required formContent = ` @@ -305,8 +197,12 @@ async function renderCredentialsForms() { No Credentials Required

This provider uses ${auth.preferred_auth_type?.replace('_', ' ') || 'automatic'} authentication.

+ ${supportedAuthTypes.length > 0 ? ` + Supported auth types: ${supportedAuthTypes.join(', ')} + ` : ''} `; + authDescription = 'No credentials required'; } else if (usesDeviceRegistration) { // Device registration formContent = ` @@ -314,8 +210,10 @@ async function renderCredentialsForms() { Device Registration

This provider requires device registration. Follow provider-specific setup instructions.

+ Authentication type: ${auth.preferred_auth_type || 'device_registration'} `; + authDescription = 'Requires device registration'; } else { // Fallback for unknown auth types formContent = ` @@ -323,8 +221,12 @@ async function renderCredentialsForms() { Authentication Type Unknown

This provider's authentication method could not be determined.

+ ${supportedAuthTypes.length > 0 ? ` + Supported auth types: ${supportedAuthTypes.join(', ')} + ` : ''} `; + authDescription = 'Unknown authentication'; } // Create buttons based on auth type @@ -364,13 +266,12 @@ async function renderCredentialsForms() {

${provider.label}

${provider.name} • ${provider.country}
- ${getAuthTypeDescription(status)} + ${authDescription}
${getStatusIcon(status)} ${getStatusText(status)}
- ${formatTokenExpiration(status)} @@ -385,21 +286,8 @@ async function renderCredentialsForms() { }); // Wait for all promises and render - try { - const htmls = await Promise.all(credentialsPromises); - credentialsContainer.innerHTML = htmls.join(''); - } catch (error) { - console.error('Error rendering credentials forms:', error); - credentialsContainer.innerHTML = ` -
- -
- Failed to render credentials -

${error.message}

-
-
- `; - } + const htmls = await Promise.all(credentialsPromises); + credentialsContainer.innerHTML = htmls.join(''); } // Load proxy forms @@ -509,19 +397,15 @@ function getStatusIcon(status) { if (!status) return ''; switch (status.auth_state) { - case 'authenticated': case 'AUTHENTICATED': case 'user_authenticated': case 'client_authenticated': return ''; - case 'expired': case 'EXPIRED': case 'credentials_only': return ''; - case 'not_authenticated': case 'NOT_AUTHENTICATED': return ''; - case 'not_applicable': case 'NOT_APPLICABLE': return ''; default: @@ -532,19 +416,17 @@ function getStatusIcon(status) { function getStatusText(status) { if (!status) return 'Unknown'; - // Normalize auth_state to lowercase for comparison - const authState = (status.auth_state || '').toLowerCase(); - - switch (authState) { - case 'authenticated': + // Map Python AuthState values to readable text + switch (status.auth_state) { + case 'AUTHENTICATED': return 'Authenticated'; - case 'expired': + case 'EXPIRED': return 'Token Expired (can refresh)'; - case 'not_authenticated': + case 'NOT_AUTHENTICATED': return 'Not Authenticated'; - case 'not_applicable': + case 'NOT_APPLICABLE': return 'No Auth Required'; - case 'user_authenticated': + case 'user_authenticated': // Keep old values for backward compatibility return 'User Authenticated'; case 'client_authenticated': return 'Client Authenticated'; @@ -671,15 +553,12 @@ async function saveCredentials(providerName) { existingInfo.remove(); } - usernameInput.parentNode.insertBefore(infoText, passwordInput.parentNode); + usernameInput.parentNode.insertBefore(infoText, passwordInput); } // Reload auth status await loadAuthStatus(); - // Re-render to show updated status and expiration - await renderCredentialsForms(); - // Show success message const action = isMaskedUsername && !username ? 'Updated' : 'Saved'; showAlert('success', `${action} credentials for ${provider.label}`); @@ -745,6 +624,18 @@ async function deleteCredentials(providerName) { }); if (response.ok) { + if (statusEl) { + statusEl.className = 'status-indicator status-warning'; + statusEl.innerHTML = ' Credentials deleted'; + } + + // Clear fields if they exist + const usernameInput = document.getElementById(`username-${providerName}`); + const passwordInput = document.getElementById(`password-${providerName}`); + + if (usernameInput) usernameInput.value = ''; + if (passwordInput) passwordInput.value = ''; + // Reload auth status await loadAuthStatus(); renderCredentialsForms(); @@ -777,15 +668,13 @@ async function testAuth(providerName) { const result = await response.json(); if (response.ok) { - // Update auth status in memory - authStatus[providerName] = result; - - // Re-render to show updated status and expiration - await renderCredentialsForms(); - if (result.is_ready) { + statusEl.className = 'status-indicator status-success'; + statusEl.innerHTML = ' Ready to use'; showAlert('success', `${providerName} is ready to use`); } else { + statusEl.className = 'status-indicator status-warning'; + statusEl.innerHTML = ` ${result.auth_state}`; showAlert('warning', `${providerName} status: ${result.auth_state}`); } } else { @@ -794,4 +683,264 @@ async function testAuth(providerName) { showAlert('error', `Test failed for ${providerName}`); } } catch (error) { - statusEl.className = 'status-indicator status \ No newline at end of file + statusEl.className = 'status-indicator status-error'; + statusEl.innerHTML = ' Network error'; + showAlert('error', `Network error: ${error.message}`); + console.error('Test error:', error); + } +} + +async function saveProxy(providerName) { + const host = document.getElementById(`proxy-host-${providerName}`).value; + const port = document.getElementById(`proxy-port-${providerName}`).value; + const type = document.getElementById(`proxy-type-${providerName}`).value; + const user = document.getElementById(`proxy-user-${providerName}`).value; + const pass = document.getElementById(`proxy-pass-${providerName}`).value; + + if (!host || !port) { + showAlert('error', 'Please fill in at least Host and Port fields'); + return; + } + + const proxyConfig = { + host, + port: parseInt(port), + proxy_type: type + }; + + if (user) proxyConfig.username = user; + if (pass) proxyConfig.password = pass; + + try { + const response = await fetch(`${API_BASE}/api/providers/${providerName}/proxy`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(proxyConfig) + }); + + if (response.ok) { + showAlert('success', `Proxy configuration saved for ${providerName}`); + // Clear password field + document.getElementById(`proxy-pass-${providerName}`).value = ''; + } else { + const result = await response.json(); + showAlert('error', result.error || `Failed to save proxy for ${providerName}`); + } + } catch (error) { + showAlert('error', `Network error: ${error.message}`); + console.error('Proxy save error:', error); + } +} + +async function deleteProxy(providerName) { + if (!confirm(`Delete proxy configuration for ${providerName}?`)) return; + + try { + const response = await fetch(`${API_BASE}/api/providers/${providerName}/proxy`, { + method: 'DELETE' + }); + + if (response.ok) { + showAlert('success', `Proxy configuration deleted for ${providerName}`); + // Clear fields + document.getElementById(`proxy-host-${providerName}`).value = ''; + document.getElementById(`proxy-port-${providerName}`).value = ''; + document.getElementById(`proxy-user-${providerName}`).value = ''; + document.getElementById(`proxy-pass-${providerName}`).value = ''; + } else { + const result = await response.json(); + showAlert('error', result.error || `Failed to delete proxy for ${providerName}`); + } + } catch (error) { + showAlert('error', `Network error: ${error.message}`); + console.error('Proxy delete error:', error); + } +} + +// UI Helper Functions +function showAlert(type, message) { + const alert = document.createElement('div'); + alert.className = `alert alert-${type}`; + alert.innerHTML = ` + +
${message}
+ `; + + // Insert at beginning of card body + const cardBody = document.querySelector('.card-body'); + cardBody.insertBefore(alert, cardBody.firstChild); + + // Remove after 5 seconds + setTimeout(() => alert.remove(), 5000); +} + +function setupEventListeners() { + // Export button + document.getElementById('btn-export').addEventListener('click', exportConfig); + document.getElementById('btn-export-json').addEventListener('click', exportConfig); + + // Import button + document.getElementById('btn-import').addEventListener('click', () => importModal.style.display = 'flex'); + document.getElementById('btn-import-json').addEventListener('click', () => importModal.style.display = 'flex'); + + // Import modal + document.getElementById('btn-cancel-import').addEventListener('click', () => importModal.style.display = 'none'); + document.getElementById('btn-confirm-import').addEventListener('click', importConfig); + + // Apply JSON config + document.getElementById('btn-apply-json').addEventListener('click', applyJsonConfig); + + // Test connection + document.getElementById('btn-test-connection').addEventListener('click', testApiConnection); + + // Clear all + document.getElementById('btn-clear-all').addEventListener('click', clearAllConfigurations); +} + +async function exportConfig() { + try { + const response = await fetch(`${API_BASE}/api/config/export`); + if (!response.ok) throw new Error('Export failed'); + + const data = await response.json(); + + // Create download link + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `ultimate_backend_config_${new Date().toISOString().slice(0,10)}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + showAlert('success', 'Configuration exported successfully'); + } catch (error) { + showAlert('error', `Export failed: ${error.message}`); + } +} + +async function importConfig() { + const file = importFile.files[0]; + if (!file) { + showAlert('error', 'Please select a file first'); + return; + } + + try { + const text = await file.text(); + const config = JSON.parse(text); + + const response = await fetch(`${API_BASE}/api/config/import`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(config) + }); + + if (response.ok) { + showAlert('success', 'Configuration imported successfully'); + importModal.style.display = 'none'; + importFile.value = ''; + + // Reload providers + await loadProviders(); + loadProxyForms(); + } else { + const result = await response.json(); + showAlert('error', result.error || 'Import failed'); + } + } catch (error) { + showAlert('error', `Import failed: ${error.message}`); + } +} + +async function applyJsonConfig() { + const jsonText = jsonEditor.value.trim(); + if (!jsonText) { + showAlert('error', 'Please enter JSON configuration'); + return; + } + + try { + const config = JSON.parse(jsonText); + + const response = await fetch(`${API_BASE}/api/config/import`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(config) + }); + + if (response.ok) { + showAlert('success', 'Configuration applied successfully'); + jsonEditor.value = ''; + + // Reload providers + await loadProviders(); + loadProxyForms(); + } else { + const result = await response.json(); + showAlert('error', result.error || 'Failed to apply configuration'); + } + } catch (error) { + showAlert('error', `Invalid JSON: ${error.message}`); + } +} + +async function testApiConnection() { + try { + const response = await fetch(`${API_BASE}/api/providers`); + if (response.ok) { + showAlert('success', 'API connection successful'); + } else { + showAlert('error', `API returned ${response.status}`); + } + } catch (error) { + showAlert('error', `API connection failed: ${error.message}`); + } +} + +async function clearAllConfigurations() { + if (!confirm('Are you sure you want to clear ALL configurations? This cannot be undone.')) { + return; + } + + if (!confirm('This will delete all credentials and proxy settings. Are you REALLY sure?')) { + return; + } + + try { + // Get all providers first + const response = await fetch(`${API_BASE}/api/providers`); + const data = await response.json(); + + // Delete credentials for all providers + for (const provider of data.providers) { + // Skip client-only providers + if (provider.requires_user_credentials) { + await fetch(`${API_BASE}/api/providers/${provider.name}/credentials`, { + method: 'DELETE' + }); + } + + await fetch(`${API_BASE}/api/providers/${provider.name}/proxy`, { + method: 'DELETE' + }); + } + + showAlert('success', 'All configurations cleared'); + + // Reload providers + await loadProviders(); + loadProxyForms(); + } catch (error) { + showAlert('error', `Failed to clear configurations: ${error.message}`); + } +} + +// Make functions available globally +window.saveCredentials = saveCredentials; +window.deleteCredentials = deleteCredentials; +window.testAuth = testAuth; +window.saveProxy = saveProxy; +window.deleteProxy = deleteProxy; \ No newline at end of file