mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-10-13 11:23:47 +02:00
Add web config
This commit is contained in:
@@ -0,0 +1,477 @@
|
||||
:root {
|
||||
--primary-color: #4361ee;
|
||||
--secondary-color: #3a0ca3;
|
||||
--success-color: #4cc9f0;
|
||||
--danger-color: #f72585;
|
||||
--warning-color: #ff9e00;
|
||||
--light-color: #f8f9fa;
|
||||
--dark-color: #212529;
|
||||
--gray-color: #6c757d;
|
||||
--border-radius: 8px;
|
||||
--box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
--transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--dark-color);
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow);
|
||||
margin-bottom: 25px;
|
||||
overflow: hidden;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
font-size: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
background: var(--light-color);
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 15px 25px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: var(--gray-color);
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--primary-color);
|
||||
background: rgba(67, 97, 238, 0.05);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--primary-color);
|
||||
border-bottom-color: var(--primary-color);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
animation: fadeIn 0.5s ease;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.providers-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.provider-card {
|
||||
background: var(--light-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 20px;
|
||||
border: 1px solid #dee2e6;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.provider-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.provider-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.provider-logo {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
object-fit: contain;
|
||||
background: white;
|
||||
padding: 8px;
|
||||
border: 2px solid var(--primary-color);
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.provider-info h3 {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.provider-info .provider-id {
|
||||
color: var(--gray-color);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 1rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.2);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 25px;
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--secondary-color);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--danger-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #d63384;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: var(--success-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #2dc2e3;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 8px 15px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background: rgba(76, 201, 240, 0.2);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background: rgba(247, 37, 133, 0.2);
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
background: rgba(255, 158, 0, 0.2);
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.loader {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid var(--primary-color);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 15px;
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: rgba(67, 97, 238, 0.1);
|
||||
border-left: 4px solid var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: rgba(76, 201, 240, 0.1);
|
||||
border-left: 4px solid var(--success-color);
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: rgba(247, 37, 133, 0.1);
|
||||
border-left: 4px solid var(--danger-color);
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: var(--gray-color);
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 20px;
|
||||
color: #dee2e6;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.config-section h3 {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.json-editor {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
font-family: 'Courier New', monospace;
|
||||
padding: 15px;
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: var(--border-radius);
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tooltip .tooltip-text {
|
||||
visibility: hidden;
|
||||
width: 200px;
|
||||
background-color: var(--dark-color);
|
||||
color: white;
|
||||
text-align: center;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 10px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 125%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.tooltip:hover .tooltip-text {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Special styling for provider types */
|
||||
.client-credentials-only .form-group.hide-for-client {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.client-credentials-only .no-credentials-required {
|
||||
display: block;
|
||||
padding: 15px;
|
||||
background: rgba(76, 201, 240, 0.1);
|
||||
border-radius: var(--border-radius);
|
||||
border-left: 4px solid var(--success-color);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.no-credentials-required {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Modal styles */
|
||||
.modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: var(--border-radius);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.modal-content p {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal-content input[type="file"] {
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.modal-buttons {
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.providers-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
+8
-1230
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,810 @@
|
||||
// Global state
|
||||
let providers = [];
|
||||
let authStatus = {};
|
||||
let currentTab = 'credentials';
|
||||
const API_BASE = window.location.origin;
|
||||
|
||||
// DOM Elements
|
||||
const tabs = document.querySelectorAll('.tab');
|
||||
const tabContents = document.querySelectorAll('.tab-content');
|
||||
const credentialsContainer = document.getElementById('credentials-container');
|
||||
const proxyContainer = document.getElementById('proxy-container');
|
||||
const jsonEditor = document.getElementById('json-editor');
|
||||
const importModal = document.getElementById('import-modal');
|
||||
const importFile = document.getElementById('import-file');
|
||||
const serviceUrlSpan = document.getElementById('service-url');
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
serviceUrlSpan.textContent = window.location.origin;
|
||||
loadProviders();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
// Tab switching
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
const tabId = tab.dataset.tab;
|
||||
switchTab(tabId);
|
||||
});
|
||||
});
|
||||
|
||||
function switchTab(tabId) {
|
||||
// Update active tab
|
||||
tabs.forEach(t => t.classList.remove('active'));
|
||||
document.querySelector(`[data-tab="${tabId}"]`).classList.add('active');
|
||||
|
||||
// Update active content
|
||||
tabContents.forEach(c => c.classList.remove('active'));
|
||||
document.getElementById(`tab-${tabId}`).classList.add('active');
|
||||
|
||||
currentTab = tabId;
|
||||
|
||||
// Load proxy data if switching to proxy tab
|
||||
if (tabId === 'proxy') {
|
||||
loadProxyForms();
|
||||
}
|
||||
}
|
||||
|
||||
// Load providers from API
|
||||
async function loadProviders() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/providers`);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
|
||||
const data = await response.json();
|
||||
providers = data.providers;
|
||||
|
||||
// Load auth status for each provider
|
||||
await loadAuthStatus();
|
||||
|
||||
renderCredentialsForms();
|
||||
|
||||
} catch (error) {
|
||||
credentialsContainer.innerHTML = `
|
||||
<div class="alert alert-error">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<div>
|
||||
<strong>Failed to load providers</strong>
|
||||
<p>${error.message}</p>
|
||||
<p>Make sure the Ultimate Backend service is running.</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
console.error('Error loading providers:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load authentication status for all providers
|
||||
async function loadAuthStatus() {
|
||||
for (const provider of providers) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/providers/${provider.name}/auth/status`);
|
||||
if (response.ok) {
|
||||
authStatus[provider.name] = await response.json();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load auth status for ${provider.name}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render credentials forms
|
||||
async function renderCredentialsForms() {
|
||||
if (providers.length === 0) {
|
||||
credentialsContainer.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-tv"></i>
|
||||
<p>No providers configured</p>
|
||||
<p>Check your provider configuration files</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
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] || {};
|
||||
|
||||
// Create form groups HTML based on provider type
|
||||
const formGroupsHTML = isClientOnly ? '' : `
|
||||
<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> ${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>
|
||||
`;
|
||||
|
||||
// Create buttons HTML - only show save/delete for providers that need credentials
|
||||
const buttonsHTML = isClientOnly ? `
|
||||
<div class="btn-group">
|
||||
<button onclick="testAuth('${provider.name}')" class="btn btn-success">
|
||||
<i class="fas fa-check"></i> Test Connection
|
||||
</button>
|
||||
</div>
|
||||
` : `
|
||||
<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>
|
||||
`;
|
||||
|
||||
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>
|
||||
|
||||
${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>
|
||||
` : ''}
|
||||
|
||||
${formGroupsHTML}
|
||||
${buttonsHTML}
|
||||
</div>
|
||||
`;
|
||||
} 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
|
||||
async function loadProxyForms() {
|
||||
proxyContainer.innerHTML = await Promise.all(providers.map(async (provider) => {
|
||||
try {
|
||||
// Try to load existing proxy config
|
||||
const proxyResponse = await fetch(`${API_BASE}/api/providers/${provider.name}/proxy`);
|
||||
let existingProxy = null;
|
||||
|
||||
if (proxyResponse.ok) {
|
||||
const proxyData = await proxyResponse.json();
|
||||
if (proxyData.proxy_config) {
|
||||
existingProxy = proxyData.proxy_config;
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="provider-card" 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">Proxy Configuration</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="proxy-host-${provider.name}">
|
||||
<i class="fas fa-server"></i> Proxy Host
|
||||
</label>
|
||||
<input type="text"
|
||||
id="proxy-host-${provider.name}"
|
||||
class="form-control"
|
||||
placeholder="proxy.example.com"
|
||||
value="${existingProxy?.host || ''}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="proxy-port-${provider.name}">
|
||||
<i class="fas fa-plug"></i> Proxy Port
|
||||
</label>
|
||||
<input type="number"
|
||||
id="proxy-port-${provider.name}"
|
||||
class="form-control"
|
||||
placeholder="8080"
|
||||
min="1"
|
||||
max="65535"
|
||||
value="${existingProxy?.port || ''}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="proxy-type-${provider.name}">
|
||||
<i class="fas fa-network-wired"></i> Proxy Type
|
||||
</label>
|
||||
<select id="proxy-type-${provider.name}" class="form-control">
|
||||
<option value="http" ${(!existingProxy || existingProxy.proxy_type === 'http') ? 'selected' : ''}>HTTP</option>
|
||||
<option value="https" ${(existingProxy?.proxy_type === 'https') ? 'selected' : ''}>HTTPS</option>
|
||||
<option value="socks4" ${(existingProxy?.proxy_type === 'socks4') ? 'selected' : ''}>SOCKS4</option>
|
||||
<option value="socks5" ${(existingProxy?.proxy_type === 'socks5') ? 'selected' : ''}>SOCKS5</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="proxy-user-${provider.name}">
|
||||
<i class="fas fa-user"></i> Username (Optional)
|
||||
</label>
|
||||
<input type="text"
|
||||
id="proxy-user-${provider.name}"
|
||||
class="form-control"
|
||||
placeholder="proxyuser"
|
||||
value="${existingProxy?.auth?.username || ''}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="proxy-pass-${provider.name}">
|
||||
<i class="fas fa-lock"></i> Password (Optional)
|
||||
</label>
|
||||
<input type="password"
|
||||
id="proxy-pass-${provider.name}"
|
||||
class="form-control"
|
||||
placeholder="••••••••"
|
||||
value="${existingProxy?.auth?.password || ''}">
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button onclick="saveProxy('${provider.name}')" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> ${existingProxy ? 'Update' : 'Save'} Proxy
|
||||
</button>
|
||||
${existingProxy ? `
|
||||
<button onclick="deleteProxy('${provider.name}')" class="btn btn-danger">
|
||||
<i class="fas fa-trash"></i> Delete Proxy
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} catch (error) {
|
||||
console.error(`Error loading proxy for ${provider.name}:`, error);
|
||||
return ''; // Return empty string on error
|
||||
}
|
||||
})).then(htmls => htmls.join(''));
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function getStatusIcon(status) {
|
||||
if (!status) return '<i class="fas fa-question-circle"></i>';
|
||||
|
||||
switch (status.auth_state) {
|
||||
case 'user_authenticated':
|
||||
case 'client_authenticated':
|
||||
return '<i class="fas fa-check-circle"></i>';
|
||||
case 'credentials_only':
|
||||
return '<i class="fas fa-exclamation-triangle"></i>';
|
||||
default:
|
||||
return '<i class="fas fa-times-circle"></i>';
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusText(status) {
|
||||
if (!status) return 'Unknown';
|
||||
|
||||
switch (status.auth_state) {
|
||||
case 'user_authenticated':
|
||||
return 'Authenticated';
|
||||
case 'client_authenticated':
|
||||
return 'Client Authenticated';
|
||||
case 'credentials_only':
|
||||
return 'Credentials Saved';
|
||||
case 'not_authenticated':
|
||||
return 'Not Authenticated';
|
||||
default:
|
||||
return status.auth_state;
|
||||
}
|
||||
}
|
||||
|
||||
// API Functions
|
||||
async function saveCredentials(providerName) {
|
||||
// Find the provider from the global providers array
|
||||
const provider = providers.find(p => p.name === providerName);
|
||||
if (!provider) {
|
||||
showAlert('error', `Provider ${providerName} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if provider is client-only
|
||||
if (!provider.requires_user_credentials) {
|
||||
showAlert('info', `${provider.label} uses hardcoded client credentials - no manual setup needed`);
|
||||
return;
|
||||
}
|
||||
|
||||
const usernameInput = document.getElementById(`username-${providerName}`);
|
||||
const passwordInput = document.getElementById(`password-${providerName}`);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
statusEl.innerHTML = '<span class="loader"></span> Saving...';
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/providers/${providerName}/credentials`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(credentials)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
if (statusEl) {
|
||||
statusEl.className = 'status-indicator status-success';
|
||||
statusEl.innerHTML = '<i class="fas fa-check-circle"></i> Saved successfully';
|
||||
}
|
||||
|
||||
// Clear password field for security
|
||||
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
|
||||
const action = isMaskedUsername && !username ? 'Updated' : 'Saved';
|
||||
showAlert('success', `${action} credentials for ${provider.label}`);
|
||||
} else {
|
||||
if (statusEl) {
|
||||
statusEl.className = 'status-indicator status-error';
|
||||
statusEl.innerHTML = `<i class="fas fa-exclamation-circle"></i> ${result.error || 'Failed to save'}`;
|
||||
}
|
||||
showAlert('error', result.error || `Failed to save credentials for ${provider.label}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (statusEl) {
|
||||
statusEl.className = 'status-indicator status-error';
|
||||
statusEl.innerHTML = '<i class="fas fa-exclamation-circle"></i> Network error';
|
||||
}
|
||||
showAlert('error', `Network error: ${error.message}`);
|
||||
console.error('Save error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCredentials(providerName) {
|
||||
// Find the provider from the global providers array
|
||||
const provider = providers.find(p => p.name === providerName);
|
||||
if (!provider) {
|
||||
showAlert('error', `Provider ${providerName} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if provider is client-only
|
||||
if (!provider.requires_user_credentials) {
|
||||
showAlert('info', `${provider.label} uses hardcoded client credentials - nothing to delete`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Delete credentials for ${providerName}?`)) return;
|
||||
|
||||
const statusEl = document.getElementById(`status-${providerName}`);
|
||||
statusEl.innerHTML = '<span class="loader"></span> Deleting...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/providers/${providerName}/credentials`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
statusEl.className = 'status-indicator status-warning';
|
||||
statusEl.innerHTML = '<i class="fas fa-info-circle"></i> Credentials deleted';
|
||||
|
||||
// Clear fields
|
||||
document.getElementById(`username-${providerName}`).value = '';
|
||||
document.getElementById(`password-${providerName}`).value = '';
|
||||
|
||||
// Reload auth status
|
||||
await loadAuthStatus();
|
||||
renderCredentialsForms();
|
||||
|
||||
showAlert('success', `Credentials deleted for ${providerName}`);
|
||||
} else {
|
||||
const result = await response.json();
|
||||
statusEl.className = 'status-indicator status-error';
|
||||
statusEl.innerHTML = `<i class="fas fa-exclamation-circle"></i> ${result.error || 'Delete failed'}`;
|
||||
showAlert('error', result.error || `Failed to delete credentials for ${providerName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
statusEl.className = 'status-indicator status-error';
|
||||
statusEl.innerHTML = '<i class="fas fa-exclamation-circle"></i> Network error';
|
||||
showAlert('error', `Network error: ${error.message}`);
|
||||
console.error('Delete error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function testAuth(providerName) {
|
||||
const statusEl = document.getElementById(`status-${providerName}`);
|
||||
statusEl.innerHTML = '<span class="loader"></span> Testing...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/providers/${providerName}/auth/status`);
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
if (result.is_ready) {
|
||||
statusEl.className = 'status-indicator status-success';
|
||||
statusEl.innerHTML = '<i class="fas fa-check-circle"></i> Ready to use';
|
||||
showAlert('success', `${providerName} is ready to use`);
|
||||
} else {
|
||||
statusEl.className = 'status-indicator status-warning';
|
||||
statusEl.innerHTML = `<i class="fas fa-exclamation-triangle"></i> ${result.auth_state}`;
|
||||
showAlert('warning', `${providerName} status: ${result.auth_state}`);
|
||||
}
|
||||
} else {
|
||||
statusEl.className = 'status-indicator status-error';
|
||||
statusEl.innerHTML = '<i class="fas fa-exclamation-circle"></i> Test failed';
|
||||
showAlert('error', `Test failed for ${providerName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
statusEl.className = 'status-indicator status-error';
|
||||
statusEl.innerHTML = '<i class="fas fa-exclamation-circle"></i> 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 = `
|
||||
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i>
|
||||
<div>${message}</div>
|
||||
`;
|
||||
|
||||
// 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;
|
||||
Reference in New Issue
Block a user