From 199b2c0e6c39f9a4687bf21fdc93053b844836b2 Mon Sep 17 00:00:00 2001 From: Nirvana Date: Thu, 11 Dec 2025 14:24:33 +0100 Subject: [PATCH] Add web config --- resources/web/config.html | 215 ++++++++++++++++++++++++++------------ service.py | 102 +++++++++++------- 2 files changed, 214 insertions(+), 103 deletions(-) diff --git a/resources/web/config.html b/resources/web/config.html index 177f115..4ef256a 100644 --- a/resources/web/config.html +++ b/resources/web/config.html @@ -674,7 +674,7 @@ } // Render credentials forms - function renderCredentialsForms() { + async function renderCredentialsForms() { if (providers.length === 0) { credentialsContainer.innerHTML = `
@@ -686,69 +686,101 @@ return; } - credentialsContainer.innerHTML = providers.map(provider => { - // Check if this provider requires user credentials - const isClientOnly = !provider.requires_user_credentials; - const providerCardClass = isClientOnly ? 'provider-card client-credentials-only' : 'provider-card'; + // Load credentials for all providers in parallel + const credentialsPromises = providers.map(async (provider) => { + try { + // Try to fetch existing credentials + const credsResponse = await fetch(`${API_BASE}/api/providers/${provider.name}/credentials`); + let existingCreds = null; - return ` -
-
- ${provider.logo ? `` : ''} -
-

${provider.label}

-
${provider.name} • ${provider.country}
-
- ${getStatusIcon(authStatus[provider.name])} - ${getStatusText(authStatus[provider.name])} + if (credsResponse.ok) { + const credsData = await credsResponse.json(); + if (credsData.has_credentials) { + existingCreds = credsData; + } + } + + // Check if this provider requires user credentials + const isClientOnly = !provider.requires_user_credentials; + const providerCardClass = isClientOnly ? 'provider-card client-credentials-only' : 'provider-card'; + + // Get the current auth status + const status = authStatus[provider.name] || {}; + + return ` +
+
+ ${provider.logo ? `` : ''} +
+

${provider.label}

+
${provider.name} • ${provider.country}
+
+ ${getStatusIcon(status)} + ${getStatusText(status)} +
-
- ${isClientOnly ? ` -
- - No manual credentials required -

This provider uses client credentials that are hardcoded in the application.

-
- ` : ''} + ${isClientOnly ? ` +
+ + No manual credentials required +

This provider uses client credentials that are hardcoded in the application.

+
+ ` : ''} -
- - -
+
+ + + ${existingCreds?.username_masked ? ` + + Credentials saved. Enter new values to update. + + ` : ''} +
-
- - -
+
+ + +
-
- - - +
+ + ${existingCreds ? ` + + ` : ''} + +
-
- `; - }).join(''); + `; + } catch (error) { + console.error(`Error loading credentials for ${provider.name}:`, error); + return ''; // Return empty string on error + } + }); + + // Wait for all promises and render + const htmls = await Promise.all(credentialsPromises); + credentialsContainer.innerHTML = htmls.join(''); } // Load proxy forms @@ -900,18 +932,39 @@ return; } - const username = document.getElementById(`username-${providerName}`).value; - const password = document.getElementById(`password-${providerName}`).value; + const usernameInput = document.getElementById(`username-${providerName}`); + const passwordInput = document.getElementById(`password-${providerName}`); - if (!username || !password) { - showAlert('error', 'Please enter both username and password'); + const username = usernameInput.value; + const password = passwordInput.value; + + // For updates, username might be readonly with masked value + // We need to check if user entered a new username + const isMaskedUsername = usernameInput.hasAttribute('readonly'); + + if (isMaskedUsername && !password) { + // User is keeping existing credentials, no changes + showAlert('info', 'No changes made to credentials'); return; } - const credentials = { - username: username, - password: password - }; + if (!isMaskedUsername && (!username || !password)) { + showAlert('error', 'Please enter both username and password for new credentials'); + return; + } + + // Prepare credentials data + const credentials = {}; + + // Only include username if it's not masked/readonly (new or changed) + if (!isMaskedUsername && username) { + credentials.username = username; + } + + // Only include password if provided (for updates, password can be empty) + if (password) { + credentials.password = password; + } const statusEl = document.getElementById(`status-${providerName}`); if (statusEl) { @@ -934,13 +987,43 @@ } // Clear password field for security - document.getElementById(`password-${providerName}`).value = ''; + passwordInput.value = ''; + + // If username was changed, mark it as readonly with masked value + if (!isMaskedUsername && username) { + // Create a masked version for display + let maskedUsername; + if (username.includes('@')) { + const parts = username.split('@'); + maskedUsername = parts[0].substring(0, 2) + '***@' + parts[1]; + } else { + maskedUsername = username.substring(0, 2) + '***'; + } + + usernameInput.value = maskedUsername; + usernameInput.readOnly = true; + usernameInput.style.backgroundColor = '#f5f5f5'; + + // Add info text + const infoText = document.createElement('small'); + infoText.innerHTML = ' Credentials saved. Enter new values to update.'; + infoText.style.cssText = 'color:#666; display:block; margin-top:5px;'; + + // Remove existing info if any + const existingInfo = usernameInput.nextElementSibling; + if (existingInfo && existingInfo.tagName === 'SMALL') { + existingInfo.remove(); + } + + usernameInput.parentNode.insertBefore(infoText, passwordInput); + } // Reload auth status await loadAuthStatus(); // Show success message - showAlert('success', `Credentials saved for ${provider.label}`); + const action = isMaskedUsername && !username ? 'Updated' : 'Saved'; + showAlert('success', `${action} credentials for ${provider.label}`); } else { if (statusEl) { statusEl.className = 'status-indicator status-error'; diff --git a/service.py b/service.py index 1b32485..08b6651 100644 --- a/service.py +++ b/service.py @@ -1238,9 +1238,15 @@ class UltimateService: @self.app.route('/api/providers//credentials', method='GET') def get_provider_credentials(provider): """ - Get current credentials for a provider (masked for security) + GET: Retrieve current credentials (masked for security) Example: GET /api/providers/joyn/credentials + Returns: { + "has_credentials": true, + "credential_type": "user_password", + "username_masked": "us***@example.com", + "username": "user@example.com" # Note: only included for pre-fill with user consent + } """ try: settings_manager = self._get_settings_manager() @@ -1251,41 +1257,51 @@ class UltimateService: # Get credentials credentials = settings_manager.get_provider_credentials(provider_name, country) + response_data = { + 'provider': provider, + 'has_credentials': credentials is not None, + 'credential_type': None, + 'username_masked': None, + 'username': None # We'll include this only if user explicitly allows + } + if credentials: - # Return masked credentials for security - cred_dict = { - 'has_credentials': True, - 'credential_type': credentials.credential_type, - 'username': getattr(credentials, 'username', None) if hasattr(credentials, - 'username') else None, - 'username_masked': None # We'll mask it - } + response_data['credential_type'] = credentials.credential_type + response_data['is_valid'] = credentials.validate() - # Mask username if present (show first 2 chars and last 2 chars) - if cred_dict['username']: - username = cred_dict['username'] - if len(username) > 4: - cred_dict['username_masked'] = username[:2] + '***' + username[-2:] - else: - cred_dict['username_masked'] = '***' + # Get username if it exists (for user_password credentials) + if hasattr(credentials, 'username') and credentials.username: + username = credentials.username - return { - 'success': True, - 'provider': provider, - 'credentials': cred_dict - } - else: - return { - 'success': True, - 'provider': provider, - 'credentials': None, - 'message': 'No credentials found' - } + # Create masked version for display + if '@' in username: # Email address + parts = username.split('@') + if len(parts[0]) > 2: + masked = parts[0][:2] + '***@' + parts[1] + else: + masked = '***@' + parts[1] + else: # Username + if len(username) > 4: + masked = username[:2] + '***' + username[-2:] + else: + masked = '***' - except Exception as api_err: - logger.error(f"API Error in GET /api/providers/{provider}/credentials: {str(api_err)}") + response_data['username_masked'] = masked + + # For pre-filling forms (security consideration - you can omit this) + # Only include if you trust your frontend and have HTTPS + response_data['username'] = username + + # Log for debugging (remove in production) + logger.debug( + f"GET credentials for {provider}: type={credentials.credential_type}, has_username={hasattr(credentials, 'username')}") + + return response_data + + except Exception as e: + logger.error(f"GET credentials error for {provider}: {e}", exc_info=True) response.status = 500 - return {'error': f'Internal server error: {str(api_err)}'} + return {'error': f'Internal server error: {str(e)}'} @self.app.route('/api/providers//credentials', method='POST') def save_provider_credentials(provider): @@ -1294,15 +1310,13 @@ class UltimateService: Accepts JSON body with credentials: - User/password: {"username": "...", "password": "..."} - - Example: POST /api/providers/joyn_de/credentials - Body: {"username": "user@example.com", "password": "secret123"} + - For updates: {"password": "..."} (keep existing username) """ try: # Parse JSON body try: credentials_data = request.json - logger.info(f"Received credentials data for {provider}: {credentials_data}") + logger.debug(f"Received credentials data for {provider}: {credentials_data}") except Exception as json_err: logger.error(f"Invalid JSON in request body: {json_err}") response.status = 400 @@ -1321,7 +1335,20 @@ class UltimateService: # Get settings manager settings_manager = self._get_settings_manager() - logger.info(f"Settings manager obtained: {settings_manager}") + + # Parse provider and country + provider_name, country = settings_manager.parse_provider_country(provider) + + # Check if we have existing credentials (for partial updates) + existing_credentials = settings_manager.get_provider_credentials(provider_name, country) + + if existing_credentials and 'username' not in credentials_data: + # Partial update - keep existing username, only update password + if hasattr(existing_credentials, 'username'): + credentials_data['username'] = existing_credentials.username + else: + response.status = 400 + return {'error': 'Cannot update - existing credentials do not have username'} # Save credentials success, message = settings_manager.save_provider_credentials_from_api( @@ -1336,7 +1363,8 @@ class UltimateService: return { 'success': True, 'provider': provider, - 'message': message + 'message': message, + 'action': 'updated' if existing_credentials else 'created' } else: # Determine appropriate status code