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