From e85d91d1f402e1c3fe42fad545fbdf871a84fe3f Mon Sep 17 00:00:00 2001 From: Kobe Date: Fri, 20 Jun 2025 19:34:37 +0200 Subject: [PATCH] improved launch process using cloudflare --- routes/__pycache__/main.cpython-313.pyc | Bin 92656 -> 100875 bytes routes/launch_api.py | 141 +++++++- routes/main.py | 221 +++++++++++- static/js/launch_progress.js | 428 ++++++++++++++++++++--- static/js/settings/connections.js | 94 +++++ templates/auth/login.html | 1 - templates/main/launch_progress.html | 7 + templates/settings/settings.html | 2 +- templates/settings/tabs/connections.html | 53 ++- 9 files changed, 878 insertions(+), 69 deletions(-) diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index 69281a0c5e16ab91ac0dbb1a9bafa149f1385015..832cd25adde9faf6c307e451b581411d1d70f3d9 100644 GIT binary patch delta 12155 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 AhX4Qo delta 6645 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 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: diff --git a/routes/main.py b/routes/main.py index 321c58a..846afb4 100644 --- a/routes/main.py +++ b/routes/main.py @@ -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/') @login_required def get_mail_details(mail_id): diff --git a/static/js/launch_progress.js b/static/js/launch_progress.js index a45a761..2725a50 100644 --- a/static/js/launch_progress.js +++ b/static/js/launch_progress.js @@ -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 = ` +
+
+
Checking Cloudflare Connection
+

Verifying Cloudflare API connection...

+
+ `; + stepsContainer.appendChild(cloudflareStep); + + // Add DNS record creation step + const dnsCreateStep = document.createElement('div'); + dnsCreateStep.className = 'step-item'; + dnsCreateStep.innerHTML = ` +
+
+
Creating DNS Records
+

Setting up domain DNS records in Cloudflare...

+
+ `; + 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 = ` +
+
+
DNS Record Creation Results
+
+ + + + + + + + + + ${Object.entries(dnsCreateResult.results).map(([domain, result]) => ` + + + + + + `).join('')} + +
DomainStatusMessage
${domain} + + ${result.status} + + ${result.message}
+
+
+
+ `; + 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 { diff --git a/static/js/settings/connections.js b/static/js/settings/connections.js index 4fb829d..f318a6c 100644 --- a/static/js/settings/connections.js +++ b/static/js/settings/connections.js @@ -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')); diff --git a/templates/auth/login.html b/templates/auth/login.html index 73396bd..962e45d 100644 --- a/templates/auth/login.html +++ b/templates/auth/login.html @@ -61,7 +61,6 @@

Forgot your password?

-

Don't have an account? Sign Up

diff --git a/templates/main/launch_progress.html b/templates/main/launch_progress.html index 20990de..7d88f01 100644 --- a/templates/main/launch_progress.html +++ b/templates/main/launch_progress.html @@ -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 }}'; diff --git a/templates/settings/settings.html b/templates/settings/settings.html index 939a914..b5d4c0a 100644 --- a/templates/settings/settings.html +++ b/templates/settings/settings.html @@ -134,7 +134,7 @@ {% if is_master %}
- {{ connections_tab(portainer_settings, nginx_settings, site_settings, git_settings) }} + {{ connections_tab(portainer_settings, nginx_settings, site_settings, git_settings, cloudflare_settings) }}
{% endif %} diff --git a/templates/settings/tabs/connections.html b/templates/settings/tabs/connections.html index 87a2a7c..e90a6a6 100644 --- a/templates/settings/tabs/connections.html +++ b/templates/settings/tabs/connections.html @@ -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) %} @@ -161,6 +161,57 @@ + + +
+
+
+
+ Cloudflare Connection +
+ +
+
+
+
+ + +
The email address associated with your Cloudflare account
+
+
+ + +
You can generate this in your Cloudflare account settings
+
+
+ + +
The zone ID for your domain in Cloudflare
+
+
+ + +
The IP address of this server for DNS management
+
+
+ +
+
+
+
+