improved launch process using cloudflare

This commit is contained in:
2025-06-20 19:34:37 +02:00
parent bb139a2b95
commit e85d91d1f4
9 changed files with 878 additions and 69 deletions

View File

@@ -848,6 +848,125 @@ def deploy_stack():
current_app.logger.error(f"Error deploying stack: {str(e)}")
return jsonify({'error': str(e)}), 500
@launch_api.route('/check-stack-status', methods=['POST'])
@csrf.exempt
def check_stack_status():
try:
data = request.get_json()
if not data or 'stack_name' not in data:
return jsonify({'error': 'Missing stack_name field'}), 400
# Get Portainer settings
portainer_settings = KeyValueSettings.get_value('portainer_settings')
if not portainer_settings:
return jsonify({'error': 'Portainer settings not configured'}), 400
# Get Portainer endpoint ID (assuming it's the first endpoint)
endpoint_response = requests.get(
f"{portainer_settings['url'].rstrip('/')}/api/endpoints",
headers={
'X-API-Key': portainer_settings['api_key'],
'Accept': 'application/json'
},
timeout=30
)
if not endpoint_response.ok:
return jsonify({'error': 'Failed to get Portainer endpoints'}), 500
endpoints = endpoint_response.json()
if not endpoints:
return jsonify({'error': 'No Portainer endpoints found'}), 400
endpoint_id = endpoints[0]['Id']
# Get stack information
stacks_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks"
stacks_response = requests.get(
stacks_url,
headers={
'X-API-Key': portainer_settings['api_key'],
'Accept': 'application/json'
},
params={'filters': json.dumps({'Name': data['stack_name']})},
timeout=30
)
if not stacks_response.ok:
return jsonify({'error': 'Failed to get stack information'}), 500
stacks = stacks_response.json()
target_stack = None
for stack in stacks:
if stack['Name'] == data['stack_name']:
target_stack = stack
break
if not target_stack:
return jsonify({'error': f'Stack {data["stack_name"]} not found'}), 404
# Get stack services to check their status
services_url = f"{portainer_settings['url'].rstrip('/')}/api/endpoints/{endpoint_id}/docker/services"
services_response = requests.get(
services_url,
headers={
'X-API-Key': portainer_settings['api_key'],
'Accept': 'application/json'
},
params={'filters': json.dumps({'label': f'com.docker.stack.namespace={data["stack_name"]}'})},
timeout=30
)
if not services_response.ok:
return jsonify({'error': 'Failed to get stack services'}), 500
services = services_response.json()
# Check if all services are running
all_running = True
service_statuses = []
for service in services:
replicas_running = service.get('Spec', {}).get('Mode', {}).get('Replicated', {}).get('Replicas', 0)
replicas_actual = service.get('ServiceStatus', {}).get('RunningTasks', 0)
service_status = {
'name': service.get('Spec', {}).get('Name', 'Unknown'),
'replicas_expected': replicas_running,
'replicas_running': replicas_actual,
'status': 'running' if replicas_actual >= replicas_running else 'not_running'
}
service_statuses.append(service_status)
if replicas_actual < replicas_running:
all_running = False
# Determine overall stack status
if all_running and len(services) > 0:
status = 'active'
elif len(services) > 0:
status = 'partial'
else:
status = 'inactive'
return jsonify({
'success': True,
'data': {
'stack_name': data['stack_name'],
'stack_id': target_stack['Id'],
'status': status,
'services': service_statuses,
'total_services': len(services),
'running_services': len([s for s in service_statuses if s['status'] == 'running'])
}
})
except Exception as e:
current_app.logger.error(f"Error checking stack status: {str(e)}")
return jsonify({'error': str(e)}), 500
@launch_api.route('/save-instance', methods=['POST'])
@csrf.exempt
def save_instance():
@@ -1559,18 +1678,26 @@ def copy_smtp_settings():
if not jwt_token:
return jsonify({'error': 'No JWT token received'}), 400
# Prepare SMTP settings data for the API
api_smtp_data = {
'smtp_host': smtp_settings.get('smtp_host'),
'smtp_port': smtp_settings.get('smtp_port'),
'smtp_username': smtp_settings.get('smtp_username'),
'smtp_password': smtp_settings.get('smtp_password'),
'smtp_security': smtp_settings.get('smtp_security'),
'smtp_from_email': smtp_settings.get('smtp_from_email'),
'smtp_from_name': smtp_settings.get('smtp_from_name')
}
# Copy SMTP settings to the launched instance
smtp_response = requests.post(
f"{instance_url.rstrip('/')}/api/admin/key-value",
smtp_response = requests.put(
f"{instance_url.rstrip('/')}/api/admin/settings",
headers={
'Authorization': f'Bearer {jwt_token}',
'Accept': 'application/json',
'Content-Type': 'application/json'
},
json={
'key': 'smtp_settings',
'value': smtp_settings
},
json=api_smtp_data,
timeout=10
)
@@ -1582,7 +1709,7 @@ def copy_smtp_settings():
return jsonify({
'message': 'SMTP settings copied successfully',
'data': smtp_settings
'data': api_smtp_data
})
except Exception as e:

View File

@@ -391,12 +391,14 @@ def init_routes(main_bp):
portainer_settings = KeyValueSettings.get_value('portainer_settings')
nginx_settings = KeyValueSettings.get_value('nginx_settings')
git_settings = KeyValueSettings.get_value('git_settings')
cloudflare_settings = KeyValueSettings.get_value('cloudflare_settings')
return render_template('main/instances.html',
instances=instances,
portainer_settings=portainer_settings,
nginx_settings=nginx_settings,
git_settings=git_settings)
git_settings=git_settings,
cloudflare_settings=cloudflare_settings)
@main_bp.route('/instances/add', methods=['POST'])
@login_required
@@ -950,6 +952,7 @@ def init_routes(main_bp):
portainer_settings = KeyValueSettings.get_value('portainer_settings')
nginx_settings = KeyValueSettings.get_value('nginx_settings')
git_settings = KeyValueSettings.get_value('git_settings')
cloudflare_settings = KeyValueSettings.get_value('cloudflare_settings')
# Get management API key for the connections tab
management_api_key = ManagementAPIKey.query.filter_by(is_active=True).first()
@@ -1020,6 +1023,7 @@ def init_routes(main_bp):
portainer_settings=portainer_settings,
nginx_settings=nginx_settings,
git_settings=git_settings,
cloudflare_settings=cloudflare_settings,
csrf_token=generate_csrf())
@main_bp.route('/settings/update-smtp', methods=['POST'])
@@ -1678,6 +1682,77 @@ def init_routes(main_bp):
except Exception as e:
return jsonify({'error': f'Connection failed: {str(e)}'}), 400
@main_bp.route('/settings/save-cloudflare-connection', methods=['POST'])
@login_required
def save_cloudflare_connection():
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
data = request.get_json()
email = data.get('email')
api_key = data.get('api_key')
zone_id = data.get('zone_id')
server_ip = data.get('server_ip')
if not email or not api_key or not zone_id or not server_ip:
return jsonify({'error': 'Missing required fields'}), 400
try:
# Save Cloudflare settings
KeyValueSettings.set_value('cloudflare_settings', {
'email': email,
'api_key': api_key,
'zone_id': zone_id,
'server_ip': server_ip
})
return jsonify({'message': 'Settings saved successfully'})
except Exception as e:
return jsonify({'error': str(e)}), 500
@main_bp.route('/settings/test-cloudflare-connection', methods=['POST'])
@login_required
def test_cloudflare_connection():
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
data = request.get_json()
email = data.get('email')
api_key = data.get('api_key')
zone_id = data.get('zone_id')
server_ip = data.get('server_ip')
if not email or not api_key or not zone_id or not server_ip:
return jsonify({'error': 'Missing required fields'}), 400
try:
# Test Cloudflare connection by getting zone details
headers = {
'X-Auth-Email': email,
'X-Auth-Key': api_key,
'Content-Type': 'application/json'
}
# Try to get zone information
response = requests.get(
f'https://api.cloudflare.com/client/v4/zones/{zone_id}',
headers=headers,
timeout=10
)
if response.status_code == 200:
zone_data = response.json()
if zone_data.get('success'):
return jsonify({'message': 'Connection successful'})
else:
return jsonify({'error': f'API error: {zone_data.get("errors", [{}])[0].get("message", "Unknown error")}'}), 400
else:
return jsonify({'error': f'Connection failed: HTTP {response.status_code}'}), 400
except Exception as e:
return jsonify({'error': f'Connection failed: {str(e)}'}), 400
@main_bp.route('/instances/launch-progress')
@login_required
@require_password_change
@@ -1690,10 +1765,13 @@ def init_routes(main_bp):
nginx_settings = KeyValueSettings.get_value('nginx_settings')
# Get Portainer settings
portainer_settings = KeyValueSettings.get_value('portainer_settings')
# Get Cloudflare settings
cloudflare_settings = KeyValueSettings.get_value('cloudflare_settings')
return render_template('main/launch_progress.html',
nginx_settings=nginx_settings,
portainer_settings=portainer_settings)
portainer_settings=portainer_settings,
cloudflare_settings=cloudflare_settings)
@main_bp.route('/api/check-dns', methods=['POST'])
@login_required
@@ -1728,6 +1806,145 @@ def init_routes(main_bp):
'results': results
})
@main_bp.route('/api/check-cloudflare-connection', methods=['POST'])
@login_required
@require_password_change
def check_cloudflare_connection():
if not os.environ.get('MASTER', 'false').lower() == 'true':
return jsonify({'error': 'Unauthorized'}), 403
# Get Cloudflare settings
cloudflare_settings = KeyValueSettings.get_value('cloudflare_settings')
if not cloudflare_settings:
return jsonify({'error': 'Cloudflare settings not configured'}), 400
try:
# Test Cloudflare connection by getting zone details
headers = {
'X-Auth-Email': cloudflare_settings['email'],
'X-Auth-Key': cloudflare_settings['api_key'],
'Content-Type': 'application/json'
}
# Try to get zone information
response = requests.get(
f'https://api.cloudflare.com/client/v4/zones/{cloudflare_settings["zone_id"]}',
headers=headers,
timeout=10
)
if response.status_code == 200:
zone_data = response.json()
if zone_data.get('success'):
return jsonify({
'success': True,
'message': 'Cloudflare connection successful',
'zone_name': zone_data['result']['name']
})
else:
return jsonify({'error': f'API error: {zone_data.get("errors", [{}])[0].get("message", "Unknown error")}'}), 400
else:
return jsonify({'error': f'Connection failed: HTTP {response.status_code}'}), 400
except Exception as e:
return jsonify({'error': f'Connection failed: {str(e)}'}), 400
@main_bp.route('/api/create-dns-records', methods=['POST'])
@login_required
@require_password_change
def create_dns_records():
if not os.environ.get('MASTER', 'false').lower() == 'true':
return jsonify({'error': 'Unauthorized'}), 403
data = request.get_json()
if not data or 'domains' not in data:
return jsonify({'error': 'No domains provided'}), 400
domains = data['domains']
# Get Cloudflare settings
cloudflare_settings = KeyValueSettings.get_value('cloudflare_settings')
if not cloudflare_settings:
return jsonify({'error': 'Cloudflare settings not configured'}), 400
try:
headers = {
'X-Auth-Email': cloudflare_settings['email'],
'X-Auth-Key': cloudflare_settings['api_key'],
'Content-Type': 'application/json'
}
results = {}
for domain in domains:
# Check if DNS record already exists
response = requests.get(
f'https://api.cloudflare.com/client/v4/zones/{cloudflare_settings["zone_id"]}/dns_records',
headers=headers,
params={'name': domain},
timeout=10
)
if response.status_code == 200:
dns_data = response.json()
existing_records = dns_data.get('result', [])
# Filter for A records
a_records = [record for record in existing_records if record['type'] == 'A' and record['name'] == domain]
if a_records:
# Update existing A record
record_id = a_records[0]['id']
update_data = {
'type': 'A',
'name': domain,
'content': cloudflare_settings['server_ip'],
'ttl': 1, # Auto TTL
'proxied': True
}
update_response = requests.put(
f'https://api.cloudflare.com/client/v4/zones/{cloudflare_settings["zone_id"]}/dns_records/{record_id}',
headers=headers,
json=update_data,
timeout=10
)
if update_response.status_code == 200:
results[domain] = {'status': 'updated', 'message': 'DNS record updated'}
else:
results[domain] = {'status': 'error', 'message': f'Failed to update DNS record: {update_response.status_code}'}
else:
# Create new A record
create_data = {
'type': 'A',
'name': domain,
'content': cloudflare_settings['server_ip'],
'ttl': 1, # Auto TTL
'proxied': True
}
create_response = requests.post(
f'https://api.cloudflare.com/client/v4/zones/{cloudflare_settings["zone_id"]}/dns_records',
headers=headers,
json=create_data,
timeout=10
)
if create_response.status_code == 200:
results[domain] = {'status': 'created', 'message': 'DNS record created'}
else:
results[domain] = {'status': 'error', 'message': f'Failed to create DNS record: {create_response.status_code}'}
else:
results[domain] = {'status': 'error', 'message': f'Failed to check existing records: {response.status_code}'}
return jsonify({
'success': True,
'results': results
})
except Exception as e:
return jsonify({'error': f'DNS operation failed: {str(e)}'}), 400
@main_bp.route('/api/mails/<int:mail_id>')
@login_required
def get_mail_details(mail_id):

View File

@@ -16,6 +16,30 @@ document.addEventListener('DOMContentLoaded', function() {
function initializeSteps() {
const stepsContainer = document.getElementById('stepsContainer');
// Add Cloudflare connection check step
const cloudflareStep = document.createElement('div');
cloudflareStep.className = 'step-item';
cloudflareStep.innerHTML = `
<div class="step-icon"><i class="fas fa-cloud"></i></div>
<div class="step-content">
<h5>Checking Cloudflare Connection</h5>
<p class="step-status">Verifying Cloudflare API connection...</p>
</div>
`;
stepsContainer.appendChild(cloudflareStep);
// Add DNS record creation step
const dnsCreateStep = document.createElement('div');
dnsCreateStep.className = 'step-item';
dnsCreateStep.innerHTML = `
<div class="step-icon"><i class="fas fa-plus-circle"></i></div>
<div class="step-content">
<h5>Creating DNS Records</h5>
<p class="step-status">Setting up domain DNS records in Cloudflare...</p>
</div>
`;
stepsContainer.appendChild(dnsCreateStep);
// Add DNS check step
const dnsStep = document.createElement('div');
dnsStep.className = 'step-item';
@@ -199,8 +223,72 @@ function initializeSteps() {
async function startLaunch(data) {
try {
// Step 1: Check DNS records
await updateStep(1, 'Checking DNS Records', 'Verifying domain configurations...');
// Step 1: Check Cloudflare connection
await updateStep(1, 'Checking Cloudflare Connection', 'Verifying Cloudflare API connection...');
const cloudflareResult = await checkCloudflareConnection();
if (!cloudflareResult.success) {
throw new Error(cloudflareResult.error || 'Failed to connect to Cloudflare');
}
// Update the step to show success
const cloudflareStep = document.querySelectorAll('.step-item')[0];
cloudflareStep.classList.remove('active');
cloudflareStep.classList.add('completed');
cloudflareStep.querySelector('.step-status').textContent = `Successfully connected to Cloudflare (${cloudflareResult.zone_name})`;
// Step 2: Create DNS records
await updateStep(2, 'Creating DNS Records', 'Setting up domain DNS records in Cloudflare...');
const dnsCreateResult = await createDNSRecords(data.webAddresses);
if (!dnsCreateResult.success) {
throw new Error(dnsCreateResult.error || 'Failed to create DNS records');
}
// Update the step to show success
const dnsCreateStep = document.querySelectorAll('.step-item')[1];
dnsCreateStep.classList.remove('active');
dnsCreateStep.classList.add('completed');
dnsCreateStep.querySelector('.step-status').textContent = 'DNS records created successfully';
// Add DNS creation details
const dnsCreateDetails = document.createElement('div');
dnsCreateDetails.className = 'mt-3';
dnsCreateDetails.innerHTML = `
<div class="card">
<div class="card-body">
<h6 class="card-title mb-3">DNS Record Creation Results</h6>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Domain</th>
<th>Status</th>
<th>Message</th>
</tr>
</thead>
<tbody>
${Object.entries(dnsCreateResult.results).map(([domain, result]) => `
<tr>
<td>${domain}</td>
<td>
<span class="badge bg-${result.status === 'created' || result.status === 'updated' ? 'success' : 'danger'}">
${result.status}
</span>
</td>
<td>${result.message}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
</div>
`;
dnsCreateStep.querySelector('.step-content').appendChild(dnsCreateDetails);
// Step 3: Check DNS records
await updateStep(3, 'Checking DNS Records', 'Verifying domain configurations...');
const dnsResult = await checkDNSRecords(data.webAddresses);
// Check if any domains failed to resolve
@@ -213,7 +301,7 @@ async function startLaunch(data) {
}
// Update the step to show success
const dnsStep = document.querySelectorAll('.step-item')[0];
const dnsStep = document.querySelectorAll('.step-item')[2];
dnsStep.classList.remove('active');
dnsStep.classList.add('completed');
@@ -259,8 +347,8 @@ async function startLaunch(data) {
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...');
// Step 4: Check NGINX connection
await updateStep(4, 'Checking NGINX Connection', 'Verifying connection to NGINX Proxy Manager...');
const nginxResult = await checkNginxConnection();
if (!nginxResult.success) {
@@ -268,29 +356,29 @@ async function startLaunch(data) {
}
// Update the step to show success
const nginxStep = document.querySelectorAll('.step-item')[1];
const nginxStep = document.querySelectorAll('.step-item')[3];
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...');
// Step 5: Generate SSL Certificate
await updateStep(5, '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...');
// Step 6: Create Proxy Host
await updateStep(6, '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...');
// Step 7: Check Portainer connection
await updateStep(7, 'Checking Portainer Connection', 'Verifying connection to Portainer...');
const portainerResult = await checkPortainerConnection();
if (!portainerResult.success) {
@@ -298,13 +386,13 @@ async function startLaunch(data) {
}
// Update the step to show success
const portainerStep = document.querySelectorAll('.step-item')[4];
const portainerStep = document.querySelectorAll('.step-item')[6];
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...');
// Step 8: Download Docker Compose
await updateStep(8, 'Downloading Docker Compose', 'Fetching docker-compose.yml from repository...');
const dockerComposeResult = await downloadDockerCompose(data.repository, data.branch);
if (!dockerComposeResult.success) {
@@ -312,7 +400,7 @@ async function startLaunch(data) {
}
// Update the step to show success
const dockerComposeStep = document.querySelectorAll('.step-item')[5];
const dockerComposeStep = document.querySelectorAll('.step-item')[7];
dockerComposeStep.classList.remove('active');
dockerComposeStep.classList.add('completed');
dockerComposeStep.querySelector('.step-status').textContent = 'Successfully downloaded docker-compose.yml';
@@ -334,8 +422,8 @@ async function startLaunch(data) {
};
dockerComposeStep.querySelector('.step-content').appendChild(downloadButton);
// Step 7: Deploy Stack
await updateStep(7, 'Deploying Stack', 'Launching your application stack...');
// Step 9: Deploy Stack
await updateStep(9, 'Deploying Stack', 'Launching your application stack...');
const stackResult = await deployStack(dockerComposeResult.content, data.instanceName, data.port);
if (!stackResult.success) {
@@ -343,7 +431,7 @@ async function startLaunch(data) {
}
// Update the step to show success
const stackDeployStep = document.querySelectorAll('.step-item')[6];
const stackDeployStep = document.querySelectorAll('.step-item')[8];
stackDeployStep.classList.remove('active');
stackDeployStep.classList.add('completed');
stackDeployStep.querySelector('.step-status').textContent =
@@ -392,7 +480,7 @@ async function startLaunch(data) {
stackDeployStep.querySelector('.step-content').appendChild(stackDetails);
// Save instance data
await updateStep(8, 'Saving Instance Data', 'Storing instance information...');
await updateStep(10, 'Saving Instance Data', 'Storing instance information...');
try {
const instanceData = {
name: data.instanceName,
@@ -407,15 +495,15 @@ async function startLaunch(data) {
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');
await updateStep(10, '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}`);
await updateStep(10, 'Saving Instance Data', `Error: ${error.message}`);
throw error;
}
// Update the step to show success
const saveDataStep = document.querySelectorAll('.step-item')[7];
const saveDataStep = document.querySelectorAll('.step-item')[9];
saveDataStep.classList.remove('active');
saveDataStep.classList.add('completed');
saveDataStep.querySelector('.step-status').textContent = 'Successfully saved instance data';
@@ -453,7 +541,7 @@ async function startLaunch(data) {
saveDataStep.querySelector('.step-content').appendChild(instanceDetails);
// After saving instance data, add the health check step
await updateStep(9, 'Health Check', 'Verifying instance health...');
await updateStep(11, 'Health Check', 'Verifying instance health...');
const healthResult = await checkInstanceHealth(`https://${data.webAddresses[0]}`);
if (!healthResult.success) {
@@ -461,7 +549,7 @@ async function startLaunch(data) {
}
// Add a retry button if health check fails
const healthStep = document.querySelectorAll('.step-item')[8];
const healthStep = document.querySelectorAll('.step-item')[10];
if (!healthResult.success) {
const retryButton = document.createElement('button');
retryButton.className = 'btn btn-sm btn-warning mt-2';
@@ -483,7 +571,7 @@ async function startLaunch(data) {
}
// After health check, add authentication step
await updateStep(10, 'Instance Authentication', 'Setting up instance authentication...');
await updateStep(12, 'Instance Authentication', 'Setting up instance authentication...');
const authResult = await authenticateInstance(`https://${data.webAddresses[0]}`, data.instanceId);
if (!authResult.success) {
@@ -491,7 +579,7 @@ async function startLaunch(data) {
}
// Update the auth step to show success
const authStep = document.querySelectorAll('.step-item')[9];
const authStep = document.querySelectorAll('.step-item')[11];
authStep.classList.remove('active');
authStep.classList.add('completed');
authStep.querySelector('.step-status').textContent = authResult.alreadyAuthenticated ?
@@ -538,8 +626,8 @@ async function startLaunch(data) {
`;
authStep.querySelector('.step-content').appendChild(authDetails);
// Step 11: Apply Company Information
await updateStep(11, 'Apply Company Information', 'Configuring company details...');
// Step 13: Apply Company Information
await updateStep(13, 'Apply Company Information', 'Configuring company details...');
const companyResult = await applyCompanyInformation(`https://${data.webAddresses[0]}`, data.company);
if (!companyResult.success) {
@@ -547,7 +635,7 @@ async function startLaunch(data) {
}
// Update the company step to show success
const companyStep = document.querySelectorAll('.step-item')[10];
const companyStep = document.querySelectorAll('.step-item')[12];
companyStep.classList.remove('active');
companyStep.classList.add('completed');
companyStep.querySelector('.step-status').textContent = 'Successfully applied company information';
@@ -596,8 +684,8 @@ async function startLaunch(data) {
`;
companyStep.querySelector('.step-content').appendChild(companyDetails);
// Step 12: Apply Colors
await updateStep(12, 'Apply Colors', 'Configuring color scheme...');
// Step 14: Apply Colors
await updateStep(14, 'Apply Colors', 'Configuring color scheme...');
const colorsResult = await applyColors(`https://${data.webAddresses[0]}`, data.colors);
if (!colorsResult.success) {
@@ -605,7 +693,7 @@ async function startLaunch(data) {
}
// Update the colors step to show success
const colorsStep = document.querySelectorAll('.step-item')[11];
const colorsStep = document.querySelectorAll('.step-item')[13];
colorsStep.classList.remove('active');
colorsStep.classList.add('completed');
colorsStep.querySelector('.step-status').textContent = 'Successfully applied color scheme';
@@ -645,8 +733,8 @@ async function startLaunch(data) {
`;
colorsStep.querySelector('.step-content').appendChild(colorsDetails);
// Step 13: Update Admin Credentials
await updateStep(13, 'Update Admin Credentials', 'Setting up admin account...');
// Step 15: Update Admin Credentials
await updateStep(15, 'Update Admin Credentials', 'Setting up admin account...');
const credentialsResult = await updateAdminCredentials(`https://${data.webAddresses[0]}`, data.company.email);
if (!credentialsResult.success) {
@@ -654,7 +742,7 @@ async function startLaunch(data) {
}
// Update the credentials step to show success
const credentialsStep = document.querySelectorAll('.step-item')[12];
const credentialsStep = document.querySelectorAll('.step-item')[14];
credentialsStep.classList.remove('active');
credentialsStep.classList.add('completed');
@@ -719,8 +807,8 @@ async function startLaunch(data) {
`;
credentialsStep.querySelector('.step-content').appendChild(credentialsDetails);
// Step 14: Copy SMTP Settings
await updateStep(14, 'Copy SMTP Settings', 'Configuring email settings...');
// Step 16: Copy SMTP Settings
await updateStep(16, 'Copy SMTP Settings', 'Configuring email settings...');
const smtpResult = await copySmtpSettings(`https://${data.webAddresses[0]}`);
if (!smtpResult.success) {
@@ -728,7 +816,7 @@ async function startLaunch(data) {
}
// Update the SMTP step to show success
const smtpStep = document.querySelectorAll('.step-item')[13];
const smtpStep = document.querySelectorAll('.step-item')[15];
smtpStep.classList.remove('active');
smtpStep.classList.add('completed');
smtpStep.querySelector('.step-status').textContent = 'Successfully copied SMTP settings';
@@ -789,8 +877,8 @@ async function startLaunch(data) {
`;
smtpStep.querySelector('.step-content').appendChild(smtpDetails);
// Step 15: Send Completion Email
await updateStep(15, 'Send Completion Email', 'Sending notification to client...');
// Step 17: Send Completion Email
await updateStep(17, 'Send Completion Email', 'Sending notification to client...');
const emailResult = await sendCompletionEmail(`https://${data.webAddresses[0]}`, data.company, credentialsResult.data);
if (!emailResult.success) {
@@ -798,7 +886,7 @@ async function startLaunch(data) {
}
// Update the email step to show success
const emailStep = document.querySelectorAll('.step-item')[14];
const emailStep = document.querySelectorAll('.step-item')[16];
emailStep.classList.remove('active');
emailStep.classList.add('completed');
emailStep.querySelector('.step-status').textContent = 'Successfully sent completion email';
@@ -983,31 +1071,128 @@ Thank you for choosing DocuPulse!
} catch (error) {
console.error('Launch failed:', error);
await updateStep(15, 'Send Completion Email', `Error: ${error.message}`);
await updateStep(17, 'Send Completion Email', `Error: ${error.message}`);
showError(error.message);
}
}
async function checkDNSRecords(domains) {
const maxRetries = 30; // 30 retries * 10 seconds = 5 minutes
const baseDelay = 10000; // 10 seconds base delay
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch('/api/check-dns', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': window.csrfToken
},
body: JSON.stringify({ domains: domains })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to check DNS records');
}
const result = await response.json();
// Check if all domains are resolved
const allResolved = Object.values(result.results).every(result => result.resolved);
if (allResolved) {
console.log(`DNS records resolved successfully on attempt ${attempt}`);
return result;
}
// If not all domains are resolved and this isn't the last attempt, wait and retry
if (attempt < maxRetries) {
const delay = baseDelay * Math.pow(1.2, attempt - 1); // Exponential backoff
const failedDomains = Object.entries(result.results)
.filter(([_, result]) => !result.resolved)
.map(([domain]) => domain);
console.log(`Attempt ${attempt}/${maxRetries}: DNS not yet propagated for ${failedDomains.join(', ')}. Waiting ${Math.round(delay/1000)}s before retry...`);
// Update the step description to show retry progress
const currentStep = document.querySelector('.step-item.active');
if (currentStep) {
const statusElement = currentStep.querySelector('.step-status');
statusElement.textContent = `Waiting for DNS propagation... (Attempt ${attempt}/${maxRetries})`;
}
await new Promise(resolve => setTimeout(resolve, delay));
} else {
// Last attempt failed
console.log(`DNS records failed to resolve after ${maxRetries} attempts`);
return result;
}
} catch (error) {
console.error(`Error checking DNS records (attempt ${attempt}):`, error);
if (attempt === maxRetries) {
throw error;
}
// Wait before retrying on error
const delay = baseDelay * Math.pow(1.2, attempt - 1);
console.log(`DNS check failed, retrying in ${Math.round(delay/1000)}s...`);
// Update the step description to show retry progress
const currentStep = document.querySelector('.step-item.active');
if (currentStep) {
const statusElement = currentStep.querySelector('.step-status');
statusElement.textContent = `DNS check failed, retrying... (Attempt ${attempt}/${maxRetries})`;
}
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
async function checkCloudflareConnection() {
try {
const response = await fetch('/api/check-dns', {
const response = await fetch('/api/check-cloudflare-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ domains })
'X-CSRF-Token': window.csrfToken
}
});
if (!response.ok) {
throw new Error('Failed to check DNS records');
const error = await response.json();
throw new Error(error.error || 'Failed to check Cloudflare connection');
}
const result = await response.json();
console.log('DNS check result:', result);
return result;
return await response.json();
} catch (error) {
console.error('Error checking DNS records:', error);
console.error('Error checking Cloudflare connection:', error);
throw error;
}
}
async function createDNSRecords(domains) {
try {
const response = await fetch('/api/create-dns-records', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': window.csrfToken
},
body: JSON.stringify({ domains: domains })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to create DNS records');
}
return await response.json();
} catch (error) {
console.error('Error creating DNS records:', error);
throw error;
}
}
@@ -1390,7 +1575,7 @@ async function createProxyHost(domains, port, sslCertificateId) {
`;
// Update the proxy step to show success and add the results
const proxyStep = document.querySelectorAll('.step-item')[3];
const proxyStep = document.querySelectorAll('.step-item')[5];
proxyStep.classList.remove('active');
proxyStep.classList.add('completed');
const statusText = proxyStep.querySelector('.step-status');
@@ -1509,7 +1694,7 @@ async function generateSSLCertificate(domains) {
}
// Update the SSL step to show success
const sslStep = document.querySelectorAll('.step-item')[2];
const sslStep = document.querySelectorAll('.step-item')[4];
sslStep.classList.remove('active');
sslStep.classList.add('completed');
const sslStatusText = sslStep.querySelector('.step-status');
@@ -1589,8 +1774,8 @@ function updateStep(stepNumber, title, description) {
document.getElementById('currentStep').textContent = title;
document.getElementById('stepDescription').textContent = description;
// Calculate progress based on total number of steps (14 steps total)
const totalSteps = 14;
// Calculate progress based on total number of steps (17 steps total)
const totalSteps = 17;
const progress = ((stepNumber - 1) / (totalSteps - 1)) * 100;
const progressBar = document.getElementById('launchProgress');
progressBar.style.width = `${progress}%`;
@@ -1674,7 +1859,11 @@ async function downloadDockerCompose(repo, branch) {
// Add new function to deploy stack
async function deployStack(dockerComposeContent, stackName, port) {
const maxRetries = 30; // 30 retries * 10 seconds = 5 minutes
const baseDelay = 10000; // 10 seconds base delay
try {
// First, attempt to deploy the stack
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10 * 60 * 1000); // 10 minutes timeout
@@ -1709,10 +1898,135 @@ async function deployStack(dockerComposeContent, stackName, port) {
}
const result = await response.json();
// If deployment was successful, wait for stack to come online
if (result.success || result.data) {
console.log('Stack deployment initiated, waiting for stack to come online...');
// Update status to show we're waiting for stack to come online
const currentStep = document.querySelector('.step-item.active');
if (currentStep) {
const statusElement = currentStep.querySelector('.step-status');
statusElement.textContent = 'Stack deployed, waiting for services to start...';
}
// Wait and retry to check if stack is online
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// Check stack status via Portainer API
const stackCheckResponse = await fetch('/api/admin/check-stack-status', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
stack_name: `docupulse_${port}`
})
});
if (stackCheckResponse.ok) {
const stackStatus = await stackCheckResponse.json();
if (stackStatus.success && stackStatus.data.status === 'active') {
console.log(`Stack came online successfully on attempt ${attempt}`);
// Update status to show success
if (currentStep) {
const statusElement = currentStep.querySelector('.step-status');
statusElement.textContent = 'Stack deployed and online successfully';
}
return {
success: true,
data: {
...result.data || result,
status: 'active',
attempt: attempt
}
};
}
}
// If not online yet and this isn't the last attempt, wait and retry
if (attempt < maxRetries) {
const delay = baseDelay * Math.pow(1.2, attempt - 1); // Exponential backoff
console.log(`Attempt ${attempt}/${maxRetries}: Stack not yet online. Waiting ${Math.round(delay/1000)}s before retry...`);
// Update the step description to show retry progress
if (currentStep) {
const statusElement = currentStep.querySelector('.step-status');
statusElement.textContent = `Waiting for stack to come online... (Attempt ${attempt}/${maxRetries})`;
}
await new Promise(resolve => setTimeout(resolve, delay));
} else {
// Last attempt failed - stack might be online but API check failed
console.log(`Stack status check failed after ${maxRetries} attempts, but deployment was successful`);
// Update status to show partial success
if (currentStep) {
const statusElement = currentStep.querySelector('.step-status');
statusElement.textContent = 'Stack deployed (status check timeout)';
}
return {
success: true,
data: {
...result.data || result,
status: 'deployed',
note: 'Status check timeout - stack may be online'
}
};
}
} catch (error) {
console.error(`Stack status check attempt ${attempt} failed:`, error);
if (attempt === maxRetries) {
// Last attempt failed, but deployment was successful
console.log('Stack status check failed after all attempts, but deployment was successful');
if (currentStep) {
const statusElement = currentStep.querySelector('.step-status');
statusElement.textContent = 'Stack deployed (status check failed)';
}
return {
success: true,
data: {
...result.data || result,
status: 'deployed',
note: 'Status check failed - stack may be online'
}
};
}
// Wait before retrying on error
const delay = baseDelay * Math.pow(1.2, attempt - 1);
console.log(`Stack check failed, retrying in ${Math.round(delay/1000)}s...`);
if (currentStep) {
const statusElement = currentStep.querySelector('.step-status');
statusElement.textContent = `Stack check failed, retrying... (Attempt ${attempt}/${maxRetries})`;
}
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// If we get here, deployment was successful but we couldn't verify status
return {
success: true,
data: result
data: {
...result.data || result,
status: 'deployed',
note: 'Deployment successful, status unknown'
}
};
} catch (error) {
console.error('Error deploying stack:', error);
return {

View File

@@ -435,6 +435,100 @@ async function saveGitConnection(event, provider) {
saveModal.show();
}
// Test Cloudflare Connection
async function testCloudflareConnection() {
const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal'));
const messageElement = document.getElementById('saveConnectionMessage');
messageElement.textContent = 'Testing connection...';
messageElement.className = '';
saveModal.show();
try {
const email = document.getElementById('cloudflareEmail').value;
const apiKey = document.getElementById('cloudflareApiKey').value;
const zoneId = document.getElementById('cloudflareZone').value;
const serverIp = document.getElementById('cloudflareServerIp').value;
if (!email || !apiKey || !zoneId || !serverIp) {
throw new Error('Please fill in all required fields');
}
const data = {
email: email,
api_key: apiKey,
zone_id: zoneId,
server_ip: serverIp
};
const response = await fetch('/settings/test-cloudflare-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Connection test failed');
}
messageElement.textContent = 'Connection test successful!';
messageElement.className = 'text-success';
} catch (error) {
messageElement.textContent = error.message || 'Connection test failed';
messageElement.className = 'text-danger';
}
}
// Save Cloudflare Connection
async function saveCloudflareConnection(event) {
event.preventDefault();
const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal'));
const messageElement = document.getElementById('saveConnectionMessage');
messageElement.textContent = '';
messageElement.className = '';
try {
const email = document.getElementById('cloudflareEmail').value;
const apiKey = document.getElementById('cloudflareApiKey').value;
const zoneId = document.getElementById('cloudflareZone').value;
const serverIp = document.getElementById('cloudflareServerIp').value;
if (!email || !apiKey || !zoneId || !serverIp) {
throw new Error('Please fill in all required fields');
}
const response = await fetch('/settings/save-cloudflare-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify({
email: email,
api_key: apiKey,
zone_id: zoneId,
server_ip: serverIp
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to save settings');
}
messageElement.textContent = 'Settings saved successfully!';
messageElement.className = 'text-success';
} catch (error) {
messageElement.textContent = error.message || 'Failed to save settings';
messageElement.className = 'text-danger';
}
saveModal.show();
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
const gitSettings = JSON.parse(document.querySelector('meta[name="git-settings"]').getAttribute('content'));

View File

@@ -61,7 +61,6 @@
<div class="text-center mt-3">
<p class="mb-1"><a href="{{ url_for('auth.forgot_password') }}" class="text-decoration-none">Forgot your password?</a></p>
<p class="mb-0">Don't have an account? <a href="{{ url_for('auth.register') }}" class="text-decoration-none">Sign Up</a></p>
</div>
</div>
</div>

View File

@@ -69,6 +69,13 @@
api_key: '{{ portainer_settings.api_key if portainer_settings else "" }}'
};
window.cloudflareSettings = {
email: '{{ cloudflare_settings.email if cloudflare_settings else "" }}',
api_key: '{{ cloudflare_settings.api_key if cloudflare_settings else "" }}',
zone_id: '{{ cloudflare_settings.zone_id if cloudflare_settings else "" }}',
server_ip: '{{ cloudflare_settings.server_ip if cloudflare_settings else "" }}'
};
// Pass CSRF token to JavaScript
window.csrfToken = '{{ csrf_token }}';
</script>

View File

@@ -134,7 +134,7 @@
{% if is_master %}
<!-- Connections Tab -->
<div class="tab-pane fade {% if active_tab == 'connections' %}show active{% endif %}" id="connections" role="tabpanel" aria-labelledby="connections-tab">
{{ connections_tab(portainer_settings, nginx_settings, site_settings, git_settings) }}
{{ connections_tab(portainer_settings, nginx_settings, site_settings, git_settings, cloudflare_settings) }}
</div>
{% endif %}
</div>

View File

@@ -1,6 +1,6 @@
{% from "settings/components/connection_modals.html" import connection_modals %}
{% macro connections_tab(portainer_settings, nginx_settings, site_settings, git_settings) %}
{% macro connections_tab(portainer_settings, nginx_settings, site_settings, git_settings, cloudflare_settings) %}
<!-- Meta tags for JavaScript -->
<meta name="management-api-key" content="{{ site_settings.management_api_key }}">
<meta name="git-settings" content="{{ git_settings|tojson|safe }}">
@@ -161,6 +161,57 @@
</div>
</div>
</div>
<!-- Cloudflare Connection Card -->
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-cloud me-2"></i>Cloudflare Connection
</h5>
<button class="btn btn-sm btn-outline-primary" onclick="testCloudflareConnection()">
<i class="fas fa-plug me-1"></i>Test Connection
</button>
</div>
<div class="card-body">
<form id="cloudflareForm" onsubmit="saveCloudflareConnection(event)">
<div class="mb-3">
<label for="cloudflareEmail" class="form-label">Email Address</label>
<input type="email" class="form-control" id="cloudflareEmail" name="cloudflareEmail"
placeholder="Enter your Cloudflare email" required
value="{{ cloudflare_settings.email if cloudflare_settings and cloudflare_settings.email else '' }}">
<div class="form-text">The email address associated with your Cloudflare account</div>
</div>
<div class="mb-3">
<label for="cloudflareApiKey" class="form-label">API Key</label>
<input type="password" class="form-control" id="cloudflareApiKey" name="cloudflareApiKey"
placeholder="Enter your Cloudflare API key" required
value="{{ cloudflare_settings.api_key if cloudflare_settings and cloudflare_settings.api_key else '' }}">
<div class="form-text">You can generate this in your Cloudflare account settings</div>
</div>
<div class="mb-3">
<label for="cloudflareZone" class="form-label">Zone ID</label>
<input type="text" class="form-control" id="cloudflareZone" name="cloudflareZone"
placeholder="Enter your Cloudflare zone ID" required
value="{{ cloudflare_settings.zone_id if cloudflare_settings and cloudflare_settings.zone_id else '' }}">
<div class="form-text">The zone ID for your domain in Cloudflare</div>
</div>
<div class="mb-3">
<label for="cloudflareServerIp" class="form-label">Server IP Address</label>
<input type="text" class="form-control" id="cloudflareServerIp" name="cloudflareServerIp"
placeholder="Enter your server IP address (e.g., 192.168.1.100)" required
value="{{ cloudflare_settings.server_ip if cloudflare_settings and cloudflare_settings.server_ip else '' }}">
<div class="form-text">The IP address of this server for DNS management</div>
</div>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>Save Cloudflare Settings
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Save Connection Modal -->