Add web config

This commit is contained in:
Nirvana
2025-12-11 14:24:33 +01:00
parent f7bd59aadf
commit 199b2c0e6c
2 changed files with 214 additions and 103 deletions
+149 -66
View File
@@ -674,7 +674,7 @@
}
// Render credentials forms
function renderCredentialsForms() {
async function renderCredentialsForms() {
if (providers.length === 0) {
credentialsContainer.innerHTML = `
<div class="empty-state">
@@ -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 `
<div class="${providerCardClass}" data-provider="${provider.name}">
<div class="provider-header">
${provider.logo ? `<img src="${provider.logo}" alt="${provider.label}" class="provider-logo">` : ''}
<div class="provider-info">
<h3>${provider.label}</h3>
<div class="provider-id">${provider.name} • ${provider.country}</div>
<div id="status-${provider.name}" class="status-indicator">
${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 `
<div class="${providerCardClass}" data-provider="${provider.name}">
<div class="provider-header">
${provider.logo ? `<img src="${provider.logo}" alt="${provider.label}" class="provider-logo">` : ''}
<div class="provider-info">
<h3>${provider.label}</h3>
<div class="provider-id">${provider.name} • ${provider.country}</div>
<div id="status-${provider.name}" class="status-indicator">
${getStatusIcon(status)}
${getStatusText(status)}
</div>
</div>
</div>
</div>
${isClientOnly ? `
<div class="no-credentials-required">
<i class="fas fa-info-circle"></i>
<strong>No manual credentials required</strong>
<p>This provider uses client credentials that are hardcoded in the application.</p>
</div>
` : ''}
${isClientOnly ? `
<div class="no-credentials-required">
<i class="fas fa-info-circle"></i>
<strong>No manual credentials required</strong>
<p>This provider uses client credentials that are hardcoded in the application.</p>
</div>
` : ''}
<div class="form-group">
<label for="username-${provider.name}">
<i class="fas fa-user"></i> Username/Email
</label>
<input type="text"
id="username-${provider.name}"
class="form-control"
placeholder="user@example.com"
value="${authStatus[provider.name]?.username || ''}">
</div>
<div class="form-group">
<label for="username-${provider.name}">
<i class="fas fa-user"></i> Username/Email
</label>
<input type="text"
id="username-${provider.name}"
class="form-control"
placeholder="user@example.com"
value="${existingCreds?.username_masked || ''}"
${existingCreds?.username_masked ? 'readonly style="background-color:#f5f5f5;"' : ''}>
${existingCreds?.username_masked ? `
<small style="color:#666; display:block; margin-top:5px;">
<i class="fas fa-info-circle"></i> Credentials saved. Enter new values to update.
</small>
` : ''}
</div>
<div class="form-group">
<label for="password-${provider.name}">
<i class="fas fa-lock"></i> Password
</label>
<input type="password"
id="password-${provider.name}"
class="form-control"
placeholder="••••••••"
value="">
</div>
<div class="form-group">
<label for="password-${provider.name}">
<i class="fas fa-lock"></i> ${existingCreds ? 'New Password (leave blank to keep current)' : 'Password'}
</label>
<input type="password"
id="password-${provider.name}"
class="form-control"
placeholder="${existingCreds ? '•••••••• (optional)' : '••••••••'}"
value="">
</div>
<div class="btn-group">
<button onclick="saveCredentials('${provider.name}')" class="btn btn-primary">
<i class="fas fa-save"></i> Save
</button>
<button onclick="deleteCredentials('${provider.name}')" class="btn btn-danger">
<i class="fas fa-trash"></i> Delete
</button>
<button onclick="testAuth('${provider.name}')" class="btn btn-success">
<i class="fas fa-check"></i> Test
</button>
<div class="btn-group">
<button onclick="saveCredentials('${provider.name}')" class="btn btn-primary">
<i class="fas fa-save"></i> ${existingCreds ? 'Update' : 'Save'}
</button>
${existingCreds ? `
<button onclick="deleteCredentials('${provider.name}')" class="btn btn-danger">
<i class="fas fa-trash"></i> Delete
</button>
` : ''}
<button onclick="testAuth('${provider.name}')" class="btn btn-success">
<i class="fas fa-check"></i> Test
</button>
</div>
</div>
</div>
`;
}).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 = '<i class="fas fa-info-circle"></i> 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';
+65 -37
View File
@@ -1238,9 +1238,15 @@ class UltimateService:
@self.app.route('/api/providers/<provider>/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/<provider>/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