mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-29 04:22:42 +02:00
Add web config
This commit is contained in:
+149
-66
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user