diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index 69281a0..832cd25 100644 Binary files a/routes/__pycache__/main.cpython-313.pyc and b/routes/__pycache__/main.cpython-313.pyc differ diff --git a/routes/launch_api.py b/routes/launch_api.py index bee33d0..ed8daae 100644 --- a/routes/launch_api.py +++ b/routes/launch_api.py @@ -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: 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
+
+
+ +
+
+
+
+