Files
docupulse/static/js/launch_progress.js
2025-06-20 12:51:37 +02:00

1813 lines
71 KiB
JavaScript

document.addEventListener('DOMContentLoaded', function() {
// Get the launch data from sessionStorage
const launchData = JSON.parse(sessionStorage.getItem('instanceLaunchData'));
if (!launchData) {
showError('No launch data found. Please start over.');
return;
}
// Initialize the steps
initializeSteps();
// Start the launch process
startLaunch(launchData);
});
function initializeSteps() {
const stepsContainer = document.getElementById('stepsContainer');
// Add DNS check step
const dnsStep = document.createElement('div');
dnsStep.className = 'step-item';
dnsStep.innerHTML = `
<div class="step-icon"><i class="fas fa-globe"></i></div>
<div class="step-content">
<h5>Checking DNS Records</h5>
<p class="step-status">Verifying domain configurations...</p>
</div>
`;
stepsContainer.appendChild(dnsStep);
// Add NGINX connection check step
const nginxStep = document.createElement('div');
nginxStep.className = 'step-item';
nginxStep.innerHTML = `
<div class="step-icon"><i class="fas fa-network-wired"></i></div>
<div class="step-content">
<h5>Checking NGINX Connection</h5>
<p class="step-status">Verifying connection to NGINX Proxy Manager...</p>
</div>
`;
stepsContainer.appendChild(nginxStep);
// Add SSL Certificate generation step
const sslStep = document.createElement('div');
sslStep.className = 'step-item';
sslStep.innerHTML = `
<div class="step-icon"><i class="fas fa-lock"></i></div>
<div class="step-content">
<h5>Generating SSL Certificate</h5>
<p class="step-status">Setting up secure HTTPS connection...</p>
</div>
`;
stepsContainer.appendChild(sslStep);
// Add Proxy Host creation step
const proxyStep = document.createElement('div');
proxyStep.className = 'step-item';
proxyStep.innerHTML = `
<div class="step-icon"><i class="fas fa-server"></i></div>
<div class="step-content">
<h5>Creating Proxy Host</h5>
<p class="step-status">Setting up NGINX proxy host configuration...</p>
</div>
`;
stepsContainer.appendChild(proxyStep);
// Add Portainer connection check step
const portainerStep = document.createElement('div');
portainerStep.className = 'step-item';
portainerStep.innerHTML = `
<div class="step-icon"><i class="fab fa-docker"></i></div>
<div class="step-content">
<h5>Checking Portainer Connection</h5>
<p class="step-status">Verifying connection to Portainer...</p>
</div>
`;
stepsContainer.appendChild(portainerStep);
// Add Docker Compose download step
const dockerComposeStep = document.createElement('div');
dockerComposeStep.className = 'step-item';
dockerComposeStep.innerHTML = `
<div class="step-icon"><i class="fas fa-file-code"></i></div>
<div class="step-content">
<h5>Downloading Docker Compose</h5>
<p class="step-status">Fetching docker-compose.yml from repository...</p>
</div>
`;
stepsContainer.appendChild(dockerComposeStep);
// Add Portainer stack deployment step
const stackDeployStep = document.createElement('div');
stackDeployStep.className = 'step-item';
stackDeployStep.innerHTML = `
<div class="step-icon"><i class="fab fa-docker"></i></div>
<div class="step-content">
<h5>Deploying Stack</h5>
<p class="step-status">Launching your application stack...</p>
</div>
`;
stepsContainer.appendChild(stackDeployStep);
// Add Save Instance Data step
const saveDataStep = document.createElement('div');
saveDataStep.className = 'step-item';
saveDataStep.innerHTML = `
<div class="step-icon"><i class="fas fa-save"></i></div>
<div class="step-content">
<h5>Saving Instance Data</h5>
<p class="step-status">Storing instance information...</p>
</div>
`;
stepsContainer.appendChild(saveDataStep);
// Add Health Check step
const healthStep = document.createElement('div');
healthStep.className = 'step-item';
healthStep.innerHTML = `
<div class="step-icon"><i class="fas fa-heartbeat"></i></div>
<div class="step-content">
<h5>Health Check</h5>
<p class="step-status">Verifying instance health...</p>
</div>
`;
stepsContainer.appendChild(healthStep);
// Add Authentication step
const authStep = document.createElement('div');
authStep.className = 'step-item';
authStep.innerHTML = `
<div class="step-icon"><i class="fas fa-key"></i></div>
<div class="step-content">
<h5>Instance Authentication</h5>
<p class="step-status">Setting up instance authentication...</p>
</div>
`;
stepsContainer.appendChild(authStep);
// Add Apply Company Information step
const companyStep = document.createElement('div');
companyStep.className = 'step-item';
companyStep.innerHTML = `
<div class="step-icon"><i class="fas fa-building"></i></div>
<div class="step-content">
<h5>Apply Company Information</h5>
<p class="step-status">Configuring company details...</p>
</div>
`;
stepsContainer.appendChild(companyStep);
// Add Apply Colors step
const colorsStep = document.createElement('div');
colorsStep.className = 'step-item';
colorsStep.innerHTML = `
<div class="step-icon"><i class="fas fa-palette"></i></div>
<div class="step-content">
<h5>Apply Colors</h5>
<p class="step-status">Configuring color scheme...</p>
</div>
`;
stepsContainer.appendChild(colorsStep);
// Add Update Admin Credentials step
const credentialsStep = document.createElement('div');
credentialsStep.className = 'step-item';
credentialsStep.innerHTML = `
<div class="step-icon"><i class="fas fa-user-shield"></i></div>
<div class="step-content">
<h5>Update Admin Credentials</h5>
<p class="step-status">Setting up admin account...</p>
</div>
`;
stepsContainer.appendChild(credentialsStep);
}
async function startLaunch(data) {
try {
// Step 1: Check DNS records
await updateStep(1, 'Checking DNS Records', 'Verifying domain configurations...');
const dnsResult = await checkDNSRecords(data.webAddresses);
// Check if any domains failed to resolve
const failedDomains = Object.entries(dnsResult.results)
.filter(([_, result]) => !result.resolved)
.map(([domain]) => domain);
if (failedDomains.length > 0) {
throw new Error(`DNS records not found for: ${failedDomains.join(', ')}`);
}
// Update the step to show success
const dnsStep = document.querySelectorAll('.step-item')[0];
dnsStep.classList.remove('active');
dnsStep.classList.add('completed');
// Create a details section for DNS results
const detailsSection = document.createElement('div');
detailsSection.className = 'mt-3';
detailsSection.innerHTML = `
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">DNS Check Results</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Domain</th>
<th>Status</th>
<th>IP Address</th>
<th>TTL</th>
</tr>
</thead>
<tbody>
${Object.entries(dnsResult.results).map(([domain, result]) => `
<tr>
<td>${domain}</td>
<td>
<span class="badge bg-${result.resolved ? 'success' : 'danger'}">
${result.resolved ? 'Resolved' : 'Not Found'}
</span>
</td>
<td>${result.ip || 'N/A'}</td>
<td>${result.ttl || 'N/A'}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
</div>
`;
// Add the details section after the status text
const statusText = dnsStep.querySelector('.step-status');
statusText.textContent = 'DNS records verified successfully';
statusText.after(detailsSection);
// Step 2: Check NGINX connection
await updateStep(2, 'Checking NGINX Connection', 'Verifying connection to NGINX Proxy Manager...');
const nginxResult = await checkNginxConnection();
if (!nginxResult.success) {
throw new Error(nginxResult.error || 'Failed to connect to NGINX Proxy Manager');
}
// Update the step to show success
const nginxStep = document.querySelectorAll('.step-item')[1];
nginxStep.classList.remove('active');
nginxStep.classList.add('completed');
nginxStep.querySelector('.step-status').textContent = 'Successfully connected to NGINX Proxy Manager';
// Step 3: Generate SSL Certificate
await updateStep(3, 'Generating SSL Certificate', 'Setting up secure HTTPS connection...');
const sslResult = await generateSSLCertificate(data.webAddresses);
if (!sslResult.success) {
throw new Error(sslResult.error || 'Failed to generate SSL certificate');
}
// Step 4: Create Proxy Host
await updateStep(4, 'Creating Proxy Host', 'Setting up NGINX proxy host configuration...');
const proxyResult = await createProxyHost(data.webAddresses, data.port, sslResult.data.certificate.id);
if (!proxyResult.success) {
throw new Error(proxyResult.error || 'Failed to create proxy host');
}
// Step 5: Check Portainer connection
await updateStep(5, 'Checking Portainer Connection', 'Verifying connection to Portainer...');
const portainerResult = await checkPortainerConnection();
if (!portainerResult.success) {
throw new Error(portainerResult.message || 'Failed to connect to Portainer');
}
// Update the step to show success
const portainerStep = document.querySelectorAll('.step-item')[4];
portainerStep.classList.remove('active');
portainerStep.classList.add('completed');
portainerStep.querySelector('.step-status').textContent = portainerResult.message;
// Step 6: Download Docker Compose
await updateStep(6, 'Downloading Docker Compose', 'Fetching docker-compose.yml from repository...');
const dockerComposeResult = await downloadDockerCompose(data.repository, data.branch);
if (!dockerComposeResult.success) {
throw new Error(dockerComposeResult.error || 'Failed to download docker-compose.yml');
}
// Update the step to show success
const dockerComposeStep = document.querySelectorAll('.step-item')[5];
dockerComposeStep.classList.remove('active');
dockerComposeStep.classList.add('completed');
dockerComposeStep.querySelector('.step-status').textContent = 'Successfully downloaded docker-compose.yml';
// Add download button
const downloadButton = document.createElement('button');
downloadButton.className = 'btn btn-sm btn-primary mt-2';
downloadButton.innerHTML = '<i class="fas fa-download me-1"></i> Download docker-compose.yml';
downloadButton.onclick = () => {
const blob = new Blob([dockerComposeResult.content], { type: 'text/yaml' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'docker-compose.yml';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
};
dockerComposeStep.querySelector('.step-content').appendChild(downloadButton);
// Step 7: Deploy Stack
await updateStep(7, 'Deploying Stack', 'Launching your application stack...');
const stackResult = await deployStack(dockerComposeResult.content, data.instanceName, data.port);
if (!stackResult.success) {
throw new Error(stackResult.error || 'Failed to deploy stack');
}
// Update the step to show success
const stackDeployStep = document.querySelectorAll('.step-item')[6];
stackDeployStep.classList.remove('active');
stackDeployStep.classList.add('completed');
stackDeployStep.querySelector('.step-status').textContent =
stackResult.data.status === 'existing' ?
'Using existing stack' :
'Successfully deployed stack';
// Add stack details
const stackDetails = document.createElement('div');
stackDetails.className = 'mt-3';
stackDetails.innerHTML = `
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">Stack Deployment Results</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Stack Name</td>
<td>${stackResult.data.name}</td>
</tr>
<tr>
<td>Stack ID</td>
<td>${stackResult.data.id}</td>
</tr>
<tr>
<td>Status</td>
<td>
<span class="badge bg-${stackResult.data.status === 'existing' ? 'info' : 'success'}">
${stackResult.data.status === 'existing' ? 'Existing' : 'Deployed'}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
`;
stackDeployStep.querySelector('.step-content').appendChild(stackDetails);
// Save instance data
await updateStep(8, 'Saving Instance Data', 'Storing instance information...');
try {
const instanceData = {
name: data.instanceName,
port: data.port,
domains: data.webAddresses,
stack_id: stackResult.data.id,
stack_name: stackResult.data.name,
status: stackResult.data.status,
repository: data.repository,
branch: data.branch
};
console.log('Saving instance data:', instanceData);
const saveResult = await saveInstanceData(instanceData);
console.log('Save result:', saveResult);
await updateStep(8, 'Saving Instance Data', 'Instance data saved successfully');
} catch (error) {
console.error('Error saving instance data:', error);
await updateStep(8, 'Saving Instance Data', `Error: ${error.message}`);
throw error;
}
// Update the step to show success
const saveDataStep = document.querySelectorAll('.step-item')[7];
saveDataStep.classList.remove('active');
saveDataStep.classList.add('completed');
saveDataStep.querySelector('.step-status').textContent = 'Successfully saved instance data';
// Add instance details
const instanceDetails = document.createElement('div');
instanceDetails.className = 'mt-3';
instanceDetails.innerHTML = `
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">Instance Information</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Internal Port</td>
<td>${data.port}</td>
</tr>
<tr>
<td>Domains</td>
<td><a href="https://${data.webAddresses.join(', ')}" target="_blank">${data.webAddresses.join(', ')}</a></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
`;
saveDataStep.querySelector('.step-content').appendChild(instanceDetails);
// After saving instance data, add the health check step
await updateStep(9, 'Health Check', 'Verifying instance health...');
const healthResult = await checkInstanceHealth(`https://${data.webAddresses[0]}`);
if (!healthResult.success) {
throw new Error(`Health check failed: ${healthResult.error}`);
}
// Add a retry button if health check fails
const healthStep = document.querySelectorAll('.step-item')[8];
if (!healthResult.success) {
const retryButton = document.createElement('button');
retryButton.className = 'btn btn-sm btn-warning mt-2';
retryButton.innerHTML = '<i class="fas fa-sync-alt me-1"></i> Retry Health Check';
retryButton.onclick = async () => {
retryButton.disabled = true;
retryButton.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> Checking...';
const retryResult = await checkInstanceHealth(`https://${data.webAddresses[0]}`);
if (retryResult.success) {
healthStep.classList.remove('failed');
healthStep.classList.add('completed');
retryButton.remove();
} else {
retryButton.disabled = false;
retryButton.innerHTML = '<i class="fas fa-sync-alt me-1"></i> Retry Health Check';
}
};
healthStep.querySelector('.step-content').appendChild(retryButton);
}
// After health check, add authentication step
await updateStep(10, 'Instance Authentication', 'Setting up instance authentication...');
const authResult = await authenticateInstance(`https://${data.webAddresses[0]}`, data.instanceId);
if (!authResult.success) {
throw new Error(`Authentication failed: ${authResult.error}`);
}
// Update the auth step to show success
const authStep = document.querySelectorAll('.step-item')[9];
authStep.classList.remove('active');
authStep.classList.add('completed');
authStep.querySelector('.step-status').textContent = authResult.alreadyAuthenticated ?
'Instance is already authenticated' :
'Successfully authenticated instance';
// Add authentication details
const authDetails = document.createElement('div');
authDetails.className = 'mt-3';
authDetails.innerHTML = `
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">Authentication Details</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Status</td>
<td><span class="badge bg-success">Authenticated</span></td>
</tr>
<tr>
<td>Default Admin</td>
<td>administrator@docupulse.com</td>
</tr>
<tr>
<td>Connection Type</td>
<td>Management API Key</td>
</tr>
<tr>
<td>Authentication Type</td>
<td>${authResult.alreadyAuthenticated ? 'Existing' : 'New'}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
`;
authStep.querySelector('.step-content').appendChild(authDetails);
// Step 11: Apply Company Information
await updateStep(11, 'Apply Company Information', 'Configuring company details...');
const companyResult = await applyCompanyInformation(`https://${data.webAddresses[0]}`, data.company);
if (!companyResult.success) {
throw new Error(`Company information application failed: ${companyResult.error}`);
}
// Update the company step to show success
const companyStep = document.querySelectorAll('.step-item')[10];
companyStep.classList.remove('active');
companyStep.classList.add('completed');
companyStep.querySelector('.step-status').textContent = 'Successfully applied company information';
// Add company details
const companyDetails = document.createElement('div');
companyDetails.className = 'mt-3';
companyDetails.innerHTML = `
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">Company Information Applied</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Company Name</td>
<td>${data.company.name || 'Not set'}</td>
</tr>
<tr>
<td>Industry</td>
<td>${data.company.industry || 'Not set'}</td>
</tr>
<tr>
<td>Email</td>
<td>${data.company.email || 'Not set'}</td>
</tr>
<tr>
<td>Website</td>
<td>${data.company.website || 'Not set'}</td>
</tr>
<tr>
<td>Address</td>
<td>${data.company.streetAddress || 'Not set'}, ${data.company.city || ''}, ${data.company.state || ''} ${data.company.zipCode || ''}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
`;
companyStep.querySelector('.step-content').appendChild(companyDetails);
// Step 12: Apply Colors
await updateStep(12, 'Apply Colors', 'Configuring color scheme...');
const colorsResult = await applyColors(`https://${data.webAddresses[0]}`, data.colors);
if (!colorsResult.success) {
throw new Error(`Colors application failed: ${colorsResult.error}`);
}
// Update the colors step to show success
const colorsStep = document.querySelectorAll('.step-item')[11];
colorsStep.classList.remove('active');
colorsStep.classList.add('completed');
colorsStep.querySelector('.step-status').textContent = 'Successfully applied color scheme';
// Add colors details
const colorsDetails = document.createElement('div');
colorsDetails.className = 'mt-3';
colorsDetails.innerHTML = `
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">Color Scheme Applied</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
<th>Preview</th>
</tr>
</thead>
<tbody>
<tr>
<td>Primary Color</td>
<td>${data.colors.primary || 'Not set'}</td>
<td><div style="width: 30px; height: 20px; background-color: ${data.colors.primary || '#ccc'}; border: 1px solid #ddd;"></div></td>
</tr>
<tr>
<td>Secondary Color</td>
<td>${data.colors.secondary || 'Not set'}</td>
<td><div style="width: 30px; height: 20px; background-color: ${data.colors.secondary || '#ccc'}; border: 1px solid #ddd;"></div></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
`;
colorsStep.querySelector('.step-content').appendChild(colorsDetails);
// Step 13: Update Admin Credentials
await updateStep(13, 'Update Admin Credentials', 'Setting up admin account...');
const credentialsResult = await updateAdminCredentials(`https://${data.webAddresses[0]}`, data.company.email);
if (!credentialsResult.success) {
throw new Error(`Admin credentials update failed: ${credentialsResult.error}`);
}
// Update the credentials step to show success
const credentialsStep = document.querySelectorAll('.step-item')[12];
credentialsStep.classList.remove('active');
credentialsStep.classList.add('completed');
credentialsStep.querySelector('.step-status').textContent = 'Successfully updated admin credentials';
// Add credentials details
const credentialsDetails = document.createElement('div');
credentialsDetails.className = 'mt-3';
credentialsDetails.innerHTML = `
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">Admin Credentials Updated</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Email Address</td>
<td>${credentialsResult.data.email || 'Not set'}</td>
</tr>
<tr>
<td>Password</td>
<td><code class="text-primary">${credentialsResult.data.password || 'Not set'}</code></td>
</tr>
<tr>
<td>Username</td>
<td>${credentialsResult.data.username || 'administrator'}</td>
</tr>
<tr>
<td>Role</td>
<td><span class="badge bg-primary">Administrator</span></td>
</tr>
</tbody>
</table>
</div>
<div class="alert alert-warning mt-3">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>Important:</strong> Please save these credentials securely. The password will not be shown again.
</div>
<div class="alert alert-info mt-2">
<i class="fas fa-info-circle me-2"></i>
<strong>Login URL:</strong> <a href="https://${data.webAddresses[0]}" target="_blank">https://${data.webAddresses[0]}</a>
</div>
</div>
</div>
`;
credentialsStep.querySelector('.step-content').appendChild(credentialsDetails);
} catch (error) {
console.error('Launch failed:', error);
await updateStep(13, 'Update Admin Credentials', `Error: ${error.message}`);
showError(error.message);
}
}
async function checkDNSRecords(domains) {
try {
const response = await fetch('/api/check-dns', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ domains })
});
if (!response.ok) {
throw new Error('Failed to check DNS records');
}
const result = await response.json();
console.log('DNS check result:', result);
return result;
} catch (error) {
console.error('Error checking DNS records:', error);
throw error;
}
}
async function checkNginxConnection() {
try {
// Get NGINX settings from the template
const nginxSettings = {
url: window.nginxSettings?.url || '',
username: window.nginxSettings?.username || '',
password: window.nginxSettings?.password || ''
};
// Debug log the settings (without password)
console.log('NGINX Settings:', {
url: nginxSettings.url,
username: nginxSettings.username,
hasPassword: !!nginxSettings.password
});
// Check if any required field is missing
if (!nginxSettings.url || !nginxSettings.username || !nginxSettings.password) {
return {
success: false,
error: 'NGINX settings are not configured. Please configure NGINX settings in the admin panel.'
};
}
// First, get the token
const tokenResponse = await fetch(`${nginxSettings.url}/api/tokens`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
identity: nginxSettings.username,
secret: nginxSettings.password
})
});
if (!tokenResponse.ok) {
const errorText = await tokenResponse.text();
console.error('Token Error Response:', errorText);
try {
const errorJson = JSON.parse(errorText);
throw new Error(`Failed to authenticate with NGINX: ${errorJson.message || errorText}`);
} catch (e) {
throw new Error(`Failed to authenticate with NGINX: ${errorText}`);
}
}
const tokenData = await tokenResponse.json();
const token = tokenData.token;
if (!token) {
throw new Error('No token received from NGINX Proxy Manager');
}
// Now test the connection using the token
const response = await fetch(`${nginxSettings.url}/api/nginx/proxy-hosts`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
});
if (!response.ok) {
const errorText = await response.text();
console.error('NGINX connection error:', errorText);
try {
const errorJson = JSON.parse(errorText);
throw new Error(errorJson.message || 'Failed to connect to NGINX Proxy Manager');
} catch (e) {
throw new Error(`Failed to connect to NGINX Proxy Manager: ${errorText}`);
}
}
return { success: true };
} catch (error) {
console.error('Error checking NGINX connection:', error);
return {
success: false,
error: error.message || 'Error checking NGINX connection'
};
}
}
async function checkPortainerConnection() {
try {
// Get Portainer settings from the template
const portainerSettings = {
url: window.portainerSettings?.url || '',
api_key: window.portainerSettings?.api_key || ''
};
// Debug log the settings (without API key)
console.log('Portainer Settings:', {
url: portainerSettings.url,
hasApiKey: !!portainerSettings.api_key
});
// Check if any required field is missing
if (!portainerSettings.url || !portainerSettings.api_key) {
console.error('Missing Portainer settings:', portainerSettings);
return {
success: false,
message: 'Portainer settings are not configured. Please configure Portainer settings in the admin panel.'
};
}
const response = await fetch('/api/admin/test-portainer-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
url: portainerSettings.url,
api_key: portainerSettings.api_key
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to connect to Portainer');
}
return {
success: true,
message: 'Successfully connected to Portainer'
};
} catch (error) {
console.error('Portainer connection error:', error);
return {
success: false,
message: error.message || 'Failed to connect to Portainer'
};
}
}
function updateStatus(step, message, type = 'info', details = '') {
const statusElement = document.getElementById(`${step}Status`);
const detailsElement = document.getElementById(`${step}Details`);
if (statusElement) {
// Remove any existing status classes
statusElement.classList.remove('text-info', 'text-success', 'text-danger');
// Add appropriate class based on type
switch (type) {
case 'success':
statusElement.classList.add('text-success');
break;
case 'error':
statusElement.classList.add('text-danger');
break;
default:
statusElement.classList.add('text-info');
}
statusElement.textContent = message;
}
if (detailsElement) {
detailsElement.innerHTML = details;
}
}
async function createProxyHost(domains, port, sslCertificateId) {
try {
// Get NGINX settings from the template
const nginxSettings = {
url: window.nginxSettings?.url || '',
username: window.nginxSettings?.username || '',
password: window.nginxSettings?.password || ''
};
console.log('NGINX Settings:', { ...nginxSettings, password: '***' });
// Update status to show we're getting the token
updateStatus('proxy', 'Getting authentication token...', 'info');
// First, get the JWT token from NGINX
const tokenResponse = await fetch(`${nginxSettings.url}/api/tokens`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
identity: nginxSettings.username,
secret: nginxSettings.password
})
});
console.log('Token Response Status:', tokenResponse.status);
console.log('Token Response Headers:', Object.fromEntries(tokenResponse.headers.entries()));
if (!tokenResponse.ok) {
const errorText = await tokenResponse.text();
console.error('Token Error Response:', errorText);
try {
const errorJson = JSON.parse(errorText);
throw new Error(`Failed to authenticate with NGINX: ${errorJson.message || errorText}`);
} catch (e) {
throw new Error(`Failed to authenticate with NGINX: ${errorText}`);
}
}
const tokenData = await tokenResponse.json();
console.log('Token Data:', { ...tokenData, token: tokenData.token ? '***' : null });
const token = tokenData.token;
if (!token) {
throw new Error('No token received from NGINX Proxy Manager');
}
// Store the token in sessionStorage for later use
sessionStorage.setItem('nginxToken', token);
// Check if a proxy host already exists with the same properties
const proxyHostsResponse = await fetch(`${nginxSettings.url}/api/nginx/proxy-hosts`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!proxyHostsResponse.ok) {
throw new Error('Failed to fetch existing proxy hosts');
}
const proxyHosts = await proxyHostsResponse.json();
const existingProxy = proxyHosts.find(ph => {
const sameDomains = Array.isArray(ph.domain_names) &&
ph.domain_names.length === domains.length &&
domains.every(d => ph.domain_names.includes(d));
return (
sameDomains &&
ph.forward_scheme === 'http' &&
ph.forward_host === '192.168.68.124' &&
parseInt(ph.forward_port) === parseInt(port)
);
});
let result;
if (existingProxy) {
console.log('Found existing proxy host:', existingProxy);
result = existingProxy;
} else {
// Update status to show we're creating the proxy host
updateStatus('proxy', 'Creating proxy host configuration...', 'info');
const proxyHostData = {
domain_names: domains,
forward_scheme: 'http',
forward_host: '192.168.68.124',
forward_port: parseInt(port),
ssl_forced: false,
caching_enabled: true,
block_exploits: true,
allow_websocket_upgrade: true,
http2_support: false,
hsts_enabled: false,
hsts_subdomains: false,
certificate_id: sslCertificateId,
meta: {
letsencrypt_agree: true,
dns_challenge: false
}
};
console.log('Creating proxy host with data:', proxyHostData);
// Create the proxy host with NGINX
const response = await fetch(`${nginxSettings.url}/api/nginx/proxy-hosts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(proxyHostData)
});
console.log('Proxy Host Response Status:', response.status);
console.log('Proxy Host Response Headers:', Object.fromEntries(response.headers.entries()));
if (!response.ok) {
const errorText = await response.text();
console.error('Proxy Host Error Response:', errorText);
try {
const errorJson = JSON.parse(errorText);
const errorMessage = errorJson.error?.message || errorText;
// Check if the error is about a domain already being in use
if (errorMessage.includes('is already in use')) {
const domain = errorMessage.split(' ')[0];
throw new Error(`Domain ${domain} is already configured in NGINX Proxy Manager. Please remove it from NGINX Proxy Manager and try again.`);
}
throw new Error(`Failed to create proxy host: ${errorMessage}`);
} catch (e) {
if (e.message.includes('is already configured in NGINX Proxy Manager')) {
throw e; // Re-throw the domain in use error
}
throw new Error(`Failed to create proxy host: ${errorText}`);
}
}
result = await response.json();
console.log('Proxy Host Creation Result:', result);
}
// Create a detailed success message with NGINX Proxy results
const successDetails = `
<div class="mt-3">
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">NGINX Proxy Results</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Proxy Host ID</td>
<td>${result.id || 'N/A'}</td>
</tr>
<tr>
<td>Domains</td>
<td>${domains.join(', ')}</td>
</tr>
<tr>
<td>Forward Scheme</td>
<td>http</td>
</tr>
<tr>
<td>Forward Host</td>
<td>192.168.68.124</td>
</tr>
<tr>
<td>Forward Port</td>
<td>${parseInt(port)}</td>
</tr>
<tr>
<td>SSL Status</td>
<td>
<span class="badge bg-success">Forced</span>
</td>
</tr>
<tr>
<td>SSL Certificate</td>
<td>
<span class="badge bg-success">Using Certificate ID: ${sslCertificateId}</span>
</td>
</tr>
<tr>
<td>Security Features</td>
<td>
<span class="badge bg-success me-1">Block Exploits</span>
<span class="badge bg-success me-1">HSTS</span>
<span class="badge bg-success">HTTP/2</span>
</td>
</tr>
<tr>
<td>Performance</td>
<td>
<span class="badge bg-success me-1">Caching</span>
<span class="badge bg-success">WebSocket</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
`;
// Update the proxy step to show success and add the results
const proxyStep = document.querySelectorAll('.step-item')[3];
proxyStep.classList.remove('active');
proxyStep.classList.add('completed');
const statusText = proxyStep.querySelector('.step-status');
statusText.textContent = existingProxy ? 'Using existing proxy host' : 'Successfully created proxy host';
statusText.after(document.createRange().createContextualFragment(successDetails));
return {
success: true,
data: result
};
} catch (error) {
console.error('Error creating proxy host:', error);
// Update status with error message
updateStatus('proxy', `Failed: ${error.message}`, 'error');
return {
success: false,
error: error.message
};
}
}
async function generateSSLCertificate(domains) {
try {
// Get NGINX settings from the template
const nginxSettings = {
url: window.nginxSettings?.url || '',
username: window.nginxSettings?.username || '',
password: window.nginxSettings?.password || '',
email: window.nginxSettings?.email || ''
};
// Get a fresh token from NGINX
const tokenResponse = await fetch(`${nginxSettings.url}/api/tokens`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
identity: nginxSettings.username,
secret: nginxSettings.password
})
});
if (!tokenResponse.ok) {
const errorText = await tokenResponse.text();
console.error('Token Error Response:', errorText);
throw new Error(`Failed to authenticate with NGINX: ${errorText}`);
}
const tokenData = await tokenResponse.json();
const token = tokenData.token;
if (!token) {
throw new Error('No token received from NGINX Proxy Manager');
}
// First, check if a certificate already exists for these domains
const checkResponse = await fetch(`${nginxSettings.url}/api/nginx/certificates`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (!checkResponse.ok) {
throw new Error('Failed to check existing certificates');
}
const existingCertificates = await checkResponse.json();
const existingCertificate = existingCertificates.find(cert => {
const certDomains = cert.domain_names || [];
return domains.every(domain => certDomains.includes(domain)) &&
certDomains.length === domains.length;
});
let result;
let usedExisting = false;
if (existingCertificate) {
console.log('Found existing certificate:', existingCertificate);
result = existingCertificate;
usedExisting = true;
} else {
// Create the SSL certificate directly with NGINX
const requestBody = {
domain_names: domains,
meta: {
letsencrypt_email: nginxSettings.email,
letsencrypt_agree: true,
dns_challenge: false
},
provider: 'letsencrypt'
};
console.log('Request Body:', requestBody);
const response = await fetch(`${nginxSettings.url}/api/nginx/certificates`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(requestBody)
});
console.log('Response Status:', response.status);
console.log('Response Headers:', Object.fromEntries(response.headers.entries()));
if (!response.ok) {
const errorText = await response.text();
console.error('Certificate creation error:', errorText);
throw new Error(`Failed to generate SSL certificate: ${errorText}`);
}
result = await response.json();
console.log('Certificate creation result:', result);
}
// Update the SSL step to show success
const sslStep = document.querySelectorAll('.step-item')[2];
sslStep.classList.remove('active');
sslStep.classList.add('completed');
const sslStatusText = sslStep.querySelector('.step-status');
sslStatusText.textContent = usedExisting ?
'Using existing SSL certificate' :
'SSL certificate generated successfully';
// Always add SSL certificate details
const sslDetails = document.createElement('div');
sslDetails.className = 'mt-3';
sslDetails.innerHTML = `
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">SSL Certificate Details</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Certificate ID</td>
<td>${result.id || 'N/A'}</td>
</tr>
<tr>
<td>Domains</td>
<td>${(result.domain_names || domains).join(', ')}</td>
</tr>
<tr>
<td>Provider</td>
<td>${result.provider || 'Let\'s Encrypt'}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
`;
sslStatusText.after(sslDetails);
return {
success: true,
data: {
certificate: result
}
};
} catch (error) {
console.error('Error generating SSL certificate:', error);
return {
success: false,
error: error.message
};
}
}
function scrollToStep(stepElement) {
const container = document.querySelector('.launch-steps-container');
const containerRect = container.getBoundingClientRect();
const elementRect = stepElement.getBoundingClientRect();
// Calculate the scroll position to center the element in the container
const scrollTop = elementRect.top - containerRect.top - (containerRect.height / 2) + (elementRect.height / 2);
// Smooth scroll to the element
container.scrollTo({
top: container.scrollTop + scrollTop,
behavior: 'smooth'
});
}
function updateStep(stepNumber, title, description) {
return new Promise((resolve) => {
// Update the current step in the header
document.getElementById('currentStep').textContent = title;
document.getElementById('stepDescription').textContent = description;
// Calculate progress based on total number of steps (13 steps total)
const totalSteps = 13;
const progress = ((stepNumber - 1) / (totalSteps - 1)) * 100;
const progressBar = document.getElementById('launchProgress');
progressBar.style.width = `${progress}%`;
progressBar.textContent = `${Math.round(progress)}%`;
// Update step items
const steps = document.querySelectorAll('.step-item');
steps.forEach((item, index) => {
const step = index + 1;
item.classList.remove('active', 'completed', 'failed');
if (step < stepNumber) {
item.classList.add('completed');
item.querySelector('.step-status').textContent = 'Completed';
} else if (step === stepNumber) {
item.classList.add('active');
item.querySelector('.step-status').textContent = description;
// Scroll to the active step
scrollToStep(item);
}
});
// Simulate some work being done
setTimeout(resolve, 1000);
});
}
function showError(message) {
const errorContainer = document.getElementById('errorContainer');
const errorMessage = document.getElementById('errorMessage');
errorMessage.textContent = message;
errorContainer.style.display = 'block';
// Update the current step to show error
const currentStep = document.querySelector('.step-item.active');
if (currentStep) {
currentStep.classList.add('failed');
currentStep.querySelector('.step-status').textContent = 'Failed: ' + message;
}
}
function retryLaunch() {
// Reload the page to start over
window.location.reload();
}
// Add new function to download docker-compose.yml
async function downloadDockerCompose(repo, branch) {
try {
const response = await fetch('/api/admin/download-docker-compose', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
repository: repo,
branch: branch
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to download docker-compose.yml');
}
const result = await response.json();
return {
success: true,
content: result.content
};
} catch (error) {
console.error('Error downloading docker-compose.yml:', error);
return {
success: false,
error: error.message
};
}
}
// Add new function to deploy stack
async function deployStack(dockerComposeContent, stackName, port) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10 * 60 * 1000); // 10 minutes timeout
const response = await fetch('/api/admin/deploy-stack', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
name: `docupulse_${port}`,
StackFileContent: dockerComposeContent,
Env: [
{
name: 'PORT',
value: port.toString()
},
{
name: 'ISMASTER',
value: 'false'
}
]
}),
signal: controller.signal
});
clearTimeout(timeoutId); // Clear the timeout if the request completes
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to deploy stack');
}
const result = await response.json();
return {
success: true,
data: result
};
} catch (error) {
console.error('Error deploying stack:', error);
return {
success: false,
error: error.message
};
}
}
// Add new function to save instance data
async function saveInstanceData(instanceData) {
try {
console.log('Saving instance data:', instanceData);
// First check if instance already exists
const checkResponse = await fetch('/instances');
const text = await checkResponse.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
// Look for an existing instance with the same name
const existingInstance = Array.from(doc.querySelectorAll('table tbody tr')).find(row => {
const nameCell = row.querySelector('td:first-child');
return nameCell && nameCell.textContent.trim() === instanceData.port;
});
if (existingInstance) {
console.log('Instance already exists:', instanceData.port);
return {
success: true,
data: {
name: instanceData.port,
company: 'loading...',
rooms_count: 0,
conversations_count: 0,
data_size: 0.0,
payment_plan: 'Basic',
main_url: `https://${instanceData.domains[0]}`,
status: 'inactive',
port: instanceData.port,
stack_id: instanceData.stack_id,
stack_name: instanceData.stack_name,
repository: instanceData.repository,
branch: instanceData.branch
}
};
}
// If instance doesn't exist, create it
const response = await fetch('/instances/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
name: instanceData.port,
company: 'loading...',
rooms_count: 0,
conversations_count: 0,
data_size: 0.0,
payment_plan: 'Basic',
main_url: `https://${instanceData.domains[0]}`,
status: 'inactive',
port: instanceData.port,
stack_id: instanceData.stack_id,
stack_name: instanceData.stack_name,
repository: instanceData.repository,
branch: instanceData.branch
})
});
if (!response.ok) {
const errorText = await response.text();
console.error('Error response:', errorText);
throw new Error(`Failed to save instance data: ${response.status} ${response.statusText}`);
}
const result = await response.json();
console.log('Instance data saved:', result);
return result;
} catch (error) {
console.error('Error saving instance data:', error);
throw error;
}
}
async function checkInstanceHealth(instanceUrl) {
const maxRetries = 5;
let currentAttempt = 1;
while (currentAttempt <= maxRetries) {
try {
// First get the instance ID from the database
const response = await fetch('/instances');
const text = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
// Find the instance row that matches our URL
const instanceRow = Array.from(doc.querySelectorAll('table tbody tr')).find(row => {
const urlCell = row.querySelector('td:nth-child(7) a'); // URL is in the 7th column
return urlCell && urlCell.textContent.trim() === instanceUrl;
});
if (!instanceRow) {
throw new Error('Instance not found in database');
}
// Get the instance ID from the status badge's data attribute
const statusBadge = instanceRow.querySelector('[data-instance-id]');
if (!statusBadge) {
throw new Error('Could not find instance ID');
}
const instanceId = statusBadge.dataset.instanceId;
// Now use the instance ID to check status
const statusResponse = await fetch(`/instances/${instanceId}/status`);
if (!statusResponse.ok) {
throw new Error(`Health check failed with status ${statusResponse.status}`);
}
const data = await statusResponse.json();
// Update the health check step
const healthStep = document.querySelectorAll('.step-item')[8]; // Adjust index based on your steps
healthStep.classList.remove('active');
healthStep.classList.add('completed');
const statusText = healthStep.querySelector('.step-status');
if (data.status === 'active') {
statusText.textContent = `Instance is healthy (Attempt ${currentAttempt}/${maxRetries})`;
return {
success: true,
data: data
};
} else {
throw new Error('Instance is not healthy');
}
} catch (error) {
console.error(`Health check attempt ${currentAttempt} failed:`, error);
// Update status to show current attempt
const healthStep = document.querySelectorAll('.step-item')[8];
const statusText = healthStep.querySelector('.step-status');
statusText.textContent = `Health check failed (Attempt ${currentAttempt}/${maxRetries}): ${error.message}`;
if (currentAttempt === maxRetries) {
return {
success: false,
error: `Health check failed after ${maxRetries} attempts: ${error.message}`
};
}
// Wait 5 seconds before next attempt
await new Promise(resolve => setTimeout(resolve, 5000));
currentAttempt++;
}
}
}
async function authenticateInstance(instanceUrl, instanceId) {
try {
// First check if instance is already authenticated
const instancesResponse = await fetch('/instances');
const text = await instancesResponse.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
// Find the instance with matching URL
const instanceRow = Array.from(doc.querySelectorAll('table tbody tr')).find(row => {
const urlCell = row.querySelector('td:nth-child(7) a'); // URL is in the 7th column
return urlCell && urlCell.textContent.trim() === instanceUrl;
});
if (!instanceRow) {
throw new Error('Instance not found in database');
}
// Get the instance ID from the status badge's data attribute
const statusBadge = instanceRow.querySelector('[data-instance-id]');
if (!statusBadge) {
throw new Error('Could not find instance ID');
}
const dbInstanceId = statusBadge.dataset.instanceId;
// Check if already authenticated
const authStatusResponse = await fetch(`/instances/${dbInstanceId}/auth-status`);
if (!authStatusResponse.ok) {
throw new Error('Failed to check authentication status');
}
const authStatus = await authStatusResponse.json();
if (authStatus.authenticated) {
console.log('Instance is already authenticated');
return {
success: true,
message: 'Instance is already authenticated',
alreadyAuthenticated: true
};
}
console.log('Attempting login to:', `${instanceUrl}/api/admin/login`);
// First login to get token
const loginResponse = await fetch(`${instanceUrl}/api/admin/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
email: 'administrator@docupulse.com',
password: 'changeme'
})
});
if (!loginResponse.ok) {
const errorText = await loginResponse.text();
throw new Error(`Login failed: ${errorText}`);
}
const loginData = await loginResponse.json();
if (loginData.status !== 'success' || !loginData.token) {
throw new Error('Login failed: Invalid response from server');
}
const token = loginData.token;
// Then create management API key
const keyResponse = await fetch(`${instanceUrl}/api/admin/management-api-key`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
name: `Connection from ${window.location.hostname}`
})
});
if (!keyResponse.ok) {
const errorText = await keyResponse.text();
throw new Error(`Failed to create API key: ${errorText}`);
}
const keyData = await keyResponse.json();
if (!keyData.api_key) {
throw new Error('No API key received from server');
}
// Save the token to our database
const saveResponse = await fetch(`/instances/${dbInstanceId}/save-token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ token: keyData.api_key })
});
if (!saveResponse.ok) {
const errorText = await saveResponse.text();
throw new Error(`Failed to save token: ${errorText}`);
}
return {
success: true,
message: 'Successfully authenticated instance',
alreadyAuthenticated: false
};
} catch (error) {
console.error('Authentication error:', error);
return {
success: false,
error: error.message
};
}
}
async function applyCompanyInformation(instanceUrl, company) {
try {
console.log('Applying company information to:', instanceUrl);
const response = await fetch('/api/admin/apply-company-information', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
instance_url: instanceUrl,
company_data: company
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to apply company information: ${errorText}`);
}
const result = await response.json();
console.log('Company information applied successfully:', result);
return {
success: true,
message: result.message,
data: result.data
};
} catch (error) {
console.error('Error applying company information:', error);
return {
success: false,
error: error.message
};
}
}
async function applyColors(instanceUrl, colors) {
try {
console.log('Applying colors to:', instanceUrl);
const response = await fetch('/api/admin/apply-colors', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
instance_url: instanceUrl,
colors_data: colors
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to apply colors: ${errorText}`);
}
const result = await response.json();
console.log('Colors applied successfully:', result);
return {
success: true,
message: result.message,
data: result.data
};
} catch (error) {
console.error('Error applying colors:', error);
return {
success: false,
error: error.message
};
}
}
async function updateAdminCredentials(instanceUrl, email) {
try {
console.log('Updating admin credentials for:', instanceUrl);
const response = await fetch('/api/admin/update-admin-credentials', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
instance_url: instanceUrl,
email: email
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to update admin credentials: ${errorText}`);
}
const result = await response.json();
console.log('Admin credentials updated successfully:', result);
return {
success: true,
message: result.message,
data: result.data
};
} catch (error) {
console.error('Error updating admin credentials:', error);
return {
success: false,
error: error.message
};
}
}