diff --git a/app.py b/app.py index 530ca12..3de6e49 100644 --- a/app.py +++ b/app.py @@ -9,6 +9,7 @@ from routes.room_files import room_files_bp from routes.room_members import room_members_bp from routes.trash import trash_bp from routes.admin_api import admin_api +from routes.launch_api import launch_api from tasks import cleanup_trash import click from utils import timeago @@ -103,6 +104,7 @@ def create_app(): app.register_blueprint(room_members_bp, url_prefix='/api/rooms') app.register_blueprint(trash_bp, url_prefix='/api/trash') app.register_blueprint(admin_api, url_prefix='/api/admin') + app.register_blueprint(launch_api, url_prefix='/api/admin') @app.cli.command("cleanup-trash") def cleanup_trash_command(): diff --git a/routes/admin_api.py b/routes/admin_api.py index f4e7dca..6837007 100644 --- a/routes/admin_api.py +++ b/routes/admin_api.py @@ -11,9 +11,6 @@ import jwt from werkzeug.security import generate_password_hash import secrets from flask_login import login_user -import requests -import json -import base64 admin_api = Blueprint('admin_api', __name__) @@ -529,705 +526,4 @@ def resend_setup_mail(current_user, user_id): db.session.add(mail) db.session.commit() - return jsonify({'message': 'Setup mail queued for resending'}) - -# Connection Settings -@admin_api.route('/test-portainer-connection', methods=['POST']) -@csrf.exempt -def test_portainer_connection(): - data = request.get_json() - url = data.get('url') - api_key = data.get('api_key') - - if not url or not api_key: - return jsonify({'error': 'Missing required fields'}), 400 - - try: - # Test Portainer connection - response = requests.get( - f"{url.rstrip('/')}/api/status", - headers={ - 'X-API-Key': api_key, - 'Accept': 'application/json' - }, - timeout=5 - ) - - if response.status_code == 200: - return jsonify({'message': 'Connection successful'}) - else: - return jsonify({'error': 'Failed to connect to Portainer'}), 400 - - except Exception as e: - return jsonify({'error': str(e)}), 500 - -@admin_api.route('/test-nginx-connection', methods=['POST']) -@csrf.exempt -def test_nginx_connection(): - data = request.get_json() - url = data.get('url') - username = data.get('username') - password = data.get('password') - - if not url or not username or not password: - return jsonify({'error': 'Missing required fields'}), 400 - - try: - # First, get the JWT token - token_response = requests.post( - f"{url.rstrip('/')}/api/tokens", - json={ - 'identity': username, - 'secret': password - }, - headers={'Content-Type': 'application/json'}, - timeout=5 - ) - - if token_response.status_code != 200: - return jsonify({'error': 'Failed to authenticate with NGINX Proxy Manager'}), 400 - - token_data = token_response.json() - token = token_data.get('token') - - if not token: - return jsonify({'error': 'No token received from NGINX Proxy Manager'}), 400 - - # Now test the connection using the token - response = requests.get( - f"{url.rstrip('/')}/api/nginx/proxy-hosts", - headers={ - 'Authorization': f'Bearer {token}', - 'Accept': 'application/json' - }, - timeout=5 - ) - - if response.status_code == 200: - return jsonify({'message': 'Connection successful'}) - else: - return jsonify({'error': 'Failed to connect to NGINX Proxy Manager'}), 400 - - except Exception as e: - return jsonify({'error': str(e)}), 500 - -@admin_api.route('/save-portainer-connection', methods=['POST']) -@csrf.exempt -@token_required -def save_portainer_connection(current_user): - if not current_user.is_admin: - return jsonify({'error': 'Unauthorized'}), 403 - - data = request.get_json() - url = data.get('url') - api_key = data.get('api_key') - - if not url or not api_key: - return jsonify({'error': 'Missing required fields'}), 400 - - try: - # Save Portainer settings - KeyValueSettings.set_value('portainer_settings', { - 'url': url, - 'api_key': api_key - }) - - return jsonify({'message': 'Settings saved successfully'}) - - except Exception as e: - return jsonify({'error': str(e)}), 500 - -@admin_api.route('/save-nginx-connection', methods=['POST']) -@csrf.exempt -@token_required -def save_nginx_connection(current_user): - if not current_user.is_admin: - return jsonify({'error': 'Unauthorized'}), 403 - - data = request.get_json() - url = data.get('url') - username = data.get('username') - password = data.get('password') - - if not url or not username or not password: - return jsonify({'error': 'Missing required fields'}), 400 - - try: - # Save NGINX Proxy Manager settings - KeyValueSettings.set_value('nginx_settings', { - 'url': url, - 'username': username, - 'password': password - }) - - return jsonify({'message': 'Settings saved successfully'}) - - except Exception as e: - return jsonify({'error': str(e)}), 500 - -@admin_api.route('/generate-gitea-token', methods=['POST']) -@csrf.exempt -def generate_gitea_token(): - """Generate a new Gitea API token""" - data = request.get_json() - if not data or 'url' not in data or 'username' not in data or 'password' not in data: - return jsonify({'message': 'Missing required fields'}), 400 - - try: - headers = { - 'Content-Type': 'application/json' - } - - if data.get('otp'): - headers['X-Gitea-OTP'] = data['otp'] - - # Generate token with required scopes - token_data = { - 'name': f'docupulse_{datetime.utcnow().strftime("%Y%m%d_%H%M%S")}', - 'scopes': [ - 'read:activitypub', - 'read:issue', - 'write:misc', - 'read:notification', - 'read:organization', - 'read:package', - 'read:repository', - 'read:user' - ] - } - - # Make request to Gitea API - response = requests.post( - f'{data["url"]}/api/v1/users/{data["username"]}/tokens', - headers=headers, - json=token_data, - auth=(data['username'], data['password']) - ) - - if response.status_code == 201: - token_data = response.json() - return jsonify({ - 'token': token_data['sha1'], - 'name': token_data['name'], - 'token_last_eight': token_data['token_last_eight'] - }), 200 - else: - return jsonify({'message': f'Failed to generate token: {response.json().get("message", "Unknown error")}'}), 400 - - except Exception as e: - return jsonify({'message': f'Failed to generate token: {str(e)}'}), 400 - -@admin_api.route('/list-gitea-repos', methods=['POST']) -@csrf.exempt -def list_gitea_repos(): - """List repositories from Gitea""" - data = request.get_json() - if not data or 'url' not in data or 'token' not in data: - return jsonify({'message': 'Missing required fields'}), 400 - - try: - # Try different authentication methods - headers = { - 'Accept': 'application/json' - } - - # First try token in Authorization header - headers['Authorization'] = f'token {data["token"]}' - - # Get user's repositories - response = requests.get( - f'{data["url"]}/api/v1/user/repos', - headers=headers - ) - - # If that fails, try token as query parameter - if response.status_code != 200: - response = requests.get( - f'{data["url"]}/api/v1/user/repos?token={data["token"]}', - headers={'Accept': 'application/json'} - ) - - if response.status_code == 200: - return jsonify({ - 'repositories': response.json() - }), 200 - else: - return jsonify({'message': f'Failed to list repositories: {response.json().get("message", "Unknown error")}'}), 400 - - except Exception as e: - return jsonify({'message': f'Failed to list repositories: {str(e)}'}), 400 - -@admin_api.route('/list-gitea-branches', methods=['POST']) -@csrf.exempt -def list_gitea_branches(): - """List branches from a Gitea repository""" - data = request.get_json() - if not data or 'url' not in data or 'token' not in data or 'repo' not in data: - return jsonify({'message': 'Missing required fields'}), 400 - - try: - # Try different authentication methods - headers = { - 'Accept': 'application/json' - } - - # First try token in Authorization header - headers['Authorization'] = f'token {data["token"]}' - - # Get repository branches - response = requests.get( - f'{data["url"]}/api/v1/repos/{data["repo"]}/branches', - headers=headers - ) - - # If that fails, try token as query parameter - if response.status_code != 200: - response = requests.get( - f'{data["url"]}/api/v1/repos/{data["repo"]}/branches?token={data["token"]}', - headers={'Accept': 'application/json'} - ) - - if response.status_code == 200: - return jsonify({ - 'branches': response.json() - }), 200 - else: - return jsonify({'message': f'Failed to list branches: {response.json().get("message", "Unknown error")}'}), 400 - - except Exception as e: - return jsonify({'message': f'Failed to list branches: {str(e)}'}), 400 - -@admin_api.route('/list-gitlab-repos', methods=['POST']) -@csrf.exempt -def list_gitlab_repos(): - """List repositories from GitLab""" - data = request.get_json() - if not data or 'url' not in data or 'token' not in data: - return jsonify({'message': 'Missing required fields'}), 400 - - try: - headers = { - 'PRIVATE-TOKEN': data['token'], - 'Accept': 'application/json' - } - - # Get user's projects (repositories) - response = requests.get( - f'{data["url"]}/api/v4/projects', - headers=headers, - params={'membership': 'true'} # Only get projects where user is a member - ) - - if response.status_code == 200: - return jsonify({ - 'repositories': response.json() - }), 200 - else: - return jsonify({'message': f'Failed to list repositories: {response.json().get("message", "Unknown error")}'}), 400 - - except Exception as e: - return jsonify({'message': f'Failed to list repositories: {str(e)}'}), 400 - -@admin_api.route('/test-git-connection', methods=['POST']) -@csrf.exempt -def test_git_connection(): - """Test the connection to a Git repository""" - data = request.get_json() - if not data or 'provider' not in data or 'url' not in data or 'username' not in data or 'token' not in data or 'repo' not in data: - return jsonify({'message': 'Missing required fields'}), 400 - - try: - if data['provider'] == 'gitea': - # Test Gitea connection with different authentication methods - headers = { - 'Accept': 'application/json' - } - - # First try token in Authorization header - headers['Authorization'] = f'token {data["token"]}' - - # Try to get repository information - response = requests.get( - f'{data["url"]}/api/v1/repos/{data["repo"]}', - headers=headers - ) - - # If that fails, try token as query parameter - if response.status_code != 200: - response = requests.get( - f'{data["url"]}/api/v1/repos/{data["repo"]}?token={data["token"]}', - headers={'Accept': 'application/json'} - ) - - if response.status_code == 200: - return jsonify({'message': 'Connection successful'}), 200 - else: - return jsonify({'message': f'Connection failed: {response.json().get("message", "Unknown error")}'}), 400 - - elif data['provider'] == 'gitlab': - # Test GitLab connection - headers = { - 'PRIVATE-TOKEN': data['token'], - 'Accept': 'application/json' - } - - # Try to get repository information - response = requests.get( - f'{data["url"]}/api/v4/projects/{data["repo"].replace("/", "%2F")}', - headers=headers - ) - - if response.status_code == 200: - return jsonify({'message': 'Connection successful'}), 200 - else: - return jsonify({'message': f'Connection failed: {response.json().get("message", "Unknown error")}'}), 400 - - else: - return jsonify({'message': 'Invalid Git provider'}), 400 - - except Exception as e: - return jsonify({'message': f'Connection failed: {str(e)}'}), 400 - -@admin_api.route('/save-git-connection', methods=['POST']) -@csrf.exempt -@token_required -def save_git_connection(current_user): - if not current_user.is_admin: - return jsonify({'error': 'Unauthorized'}), 403 - - data = request.get_json() - provider = data.get('provider') - url = data.get('url') - username = data.get('username') - token = data.get('token') - repo = data.get('repo') - - if not provider or not url or not username or not token or not repo: - return jsonify({'error': 'Missing required fields'}), 400 - - if provider not in ['gitea', 'gitlab']: - return jsonify({'error': 'Invalid provider'}), 400 - - try: - # Save Git settings - KeyValueSettings.set_value('git_settings', { - 'provider': provider, - 'url': url, - 'username': username, - 'token': token, - 'repo': repo - }) - - return jsonify({'message': 'Settings saved successfully'}) - - except Exception as e: - return jsonify({'error': str(e)}), 500 - -@admin_api.route('/create-proxy-host', methods=['POST']) -@csrf.exempt -@token_required -def create_proxy_host(current_user): - if not current_user.is_admin: - return jsonify({'error': 'Unauthorized'}), 403 - - data = request.get_json() - domains = data.get('domains') - scheme = data.get('scheme', 'http') - forward_ip = data.get('forward_ip') - forward_port = data.get('forward_port') - - if not domains or not forward_ip or not forward_port: - return jsonify({'error': 'Missing required fields'}), 400 - - try: - # Get NGINX settings - nginx_settings = KeyValueSettings.get_value('nginx_settings') - if not nginx_settings: - return jsonify({'error': 'NGINX settings not configured'}), 400 - - # First, get the JWT token - token_response = requests.post( - f"{nginx_settings['url'].rstrip('/')}/api/tokens", - json={ - 'identity': nginx_settings['username'], - 'secret': nginx_settings['password'] - }, - headers={'Content-Type': 'application/json'}, - timeout=5 - ) - - if token_response.status_code != 200: - return jsonify({'error': 'Failed to authenticate with NGINX Proxy Manager'}), 400 - - token_data = token_response.json() - token = token_data.get('token') - - if not token: - return jsonify({'error': 'No token received from NGINX Proxy Manager'}), 400 - - # Create the proxy host - proxy_host_data = { - 'domain_names': domains, - 'forward_scheme': scheme, - 'forward_host': forward_ip, - 'forward_port': int(forward_port), - 'ssl_forced': True, - 'caching_enabled': True, - 'block_exploits': True, - 'allow_websocket_upgrade': True, - 'http2_support': True, - 'hsts_enabled': True, - 'hsts_subdomains': True, - 'meta': { - 'letsencrypt_agree': True, - 'dns_challenge': False - } - } - - response = requests.post( - f"{nginx_settings['url'].rstrip('/')}/api/nginx/proxy-hosts", - json=proxy_host_data, - headers={ - 'Authorization': f'Bearer {token}', - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }, - timeout=5 - ) - - if response.status_code == 200: - return jsonify({ - 'message': 'Proxy host created successfully', - 'data': response.json() - }) - else: - error_data = response.json() - return jsonify({ - 'error': f'Failed to create proxy host: {error_data.get("message", "Unknown error")}' - }), 400 - - except Exception as e: - return jsonify({'error': str(e)}), 500 - -@admin_api.route('/create-ssl-certificate', methods=['POST']) -@csrf.exempt -@token_required -def create_ssl_certificate(current_user): - try: - data = request.get_json() - current_app.logger.info(f"Received request data: {data}") - - domains = data.get('domains') - proxy_host_id = data.get('proxy_host_id') - nginx_url = data.get('nginx_url') - - current_app.logger.info(f"Extracted data - domains: {domains}, proxy_host_id: {proxy_host_id}, nginx_url: {nginx_url}") - - if not all([domains, proxy_host_id, nginx_url]): - missing_fields = [] - if not domains: missing_fields.append('domains') - if not proxy_host_id: missing_fields.append('proxy_host_id') - if not nginx_url: missing_fields.append('nginx_url') - - current_app.logger.error(f"Missing required fields: {missing_fields}") - return jsonify({ - 'success': False, - 'error': f'Missing required fields: {", ".join(missing_fields)}' - }), 400 - - # Get NGINX settings - nginx_settings = KeyValueSettings.get_value('nginx_settings') - if not nginx_settings: - return jsonify({ - 'success': False, - 'error': 'NGINX settings not configured' - }), 400 - - # First, get the JWT token - token_response = requests.post( - f"{nginx_settings['url'].rstrip('/')}/api/tokens", - json={ - 'identity': nginx_settings['username'], - 'secret': nginx_settings['password'] - }, - headers={'Content-Type': 'application/json'}, - timeout=5 - ) - - if token_response.status_code != 200: - return jsonify({ - 'success': False, - 'error': 'Failed to authenticate with NGINX Proxy Manager' - }), 400 - - token_data = token_response.json() - token = token_data.get('token') - - if not token: - return jsonify({ - 'success': False, - 'error': 'No token received from NGINX Proxy Manager' - }), 400 - - # Create the SSL certificate - ssl_request_data = { - 'provider': 'letsencrypt', - 'domain_names': domains, - 'meta': { - 'letsencrypt_agree': True, - 'dns_challenge': False - } - } - current_app.logger.info(f"Making SSL certificate request to {nginx_url}/api/nginx/ssl with data: {ssl_request_data}") - - ssl_response = requests.post( - f"{nginx_url}/api/nginx/ssl", - headers={ - 'Authorization': f'Bearer {token}', - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }, - json=ssl_request_data - ) - - current_app.logger.info(f"SSL certificate response status: {ssl_response.status_code}") - current_app.logger.info(f"SSL certificate response headers: {dict(ssl_response.headers)}") - - if not ssl_response.ok: - error_text = ssl_response.text - current_app.logger.error(f"Failed to create SSL certificate: {error_text}") - return jsonify({ - 'success': False, - 'error': f'Failed to create SSL certificate: {error_text}' - }), ssl_response.status_code - - ssl_data = ssl_response.json() - current_app.logger.info(f"SSL certificate created successfully: {ssl_data}") - - # Get the certificate ID - cert_id = ssl_data.get('id') - if not cert_id: - current_app.logger.error("No certificate ID received in response") - return jsonify({ - 'success': False, - 'error': 'No certificate ID received' - }), 500 - - # Update the proxy host with the certificate - update_request_data = { - 'ssl_certificate_id': cert_id - } - current_app.logger.info(f"Updating proxy host {proxy_host_id} with data: {update_request_data}") - - update_response = requests.put( - f"{nginx_url}/api/nginx/proxy-hosts/{proxy_host_id}", - headers={ - 'Authorization': f'Bearer {token}', - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }, - json=update_request_data - ) - - current_app.logger.info(f"Update response status: {update_response.status_code}") - current_app.logger.info(f"Update response headers: {dict(update_response.headers)}") - - if not update_response.ok: - error_text = update_response.text - current_app.logger.error(f"Failed to update proxy host: {error_text}") - return jsonify({ - 'success': False, - 'error': f'Failed to update proxy host: {error_text}' - }), update_response.status_code - - update_data = update_response.json() - current_app.logger.info(f"Proxy host updated successfully: {update_data}") - - return jsonify({ - 'success': True, - 'data': { - 'certificate': ssl_data, - 'proxy_host': update_data - } - }) - - except Exception as e: - current_app.logger.error(f"Error in create_ssl_certificate: {str(e)}") - return jsonify({ - 'success': False, - 'error': str(e) - }), 500 - -@admin_api.route('/download-docker-compose', methods=['POST']) -@csrf.exempt -def download_docker_compose(): - """Download docker-compose.yml from the repository""" - data = request.get_json() - if not data or 'repository' not in data or 'branch' not in data: - return jsonify({'message': 'Missing required fields'}), 400 - - try: - # Get Git settings - git_settings = KeyValueSettings.get_value('git_settings') - if not git_settings: - return jsonify({'message': 'Git settings not configured'}), 400 - - # Determine the provider and set up the appropriate API call - if git_settings['provider'] == 'gitea': - # For Gitea - headers = { - 'Accept': 'application/json', - 'Authorization': f'token {git_settings["token"]}' - } - - # Try to get the file content - response = requests.get( - f'{git_settings["url"]}/api/v1/repos/{data["repository"]}/contents/docker-compose.yml', - headers=headers, - params={'ref': data['branch']} - ) - - if response.status_code != 200: - # Try token as query parameter if header auth fails - response = requests.get( - f'{git_settings["url"]}/api/v1/repos/{data["repository"]}/contents/docker-compose.yml?token={git_settings["token"]}', - headers={'Accept': 'application/json'}, - params={'ref': data['branch']} - ) - - elif git_settings['provider'] == 'gitlab': - # For GitLab - headers = { - 'PRIVATE-TOKEN': git_settings['token'], - 'Accept': 'application/json' - } - - # Get the file content - response = requests.get( - f'{git_settings["url"]}/api/v4/projects/{data["repository"].replace("/", "%2F")}/repository/files/docker-compose.yml/raw', - headers=headers, - params={'ref': data['branch']} - ) - - else: - return jsonify({'message': 'Unsupported Git provider'}), 400 - - if response.status_code == 200: - # For Gitea, we need to decode the content from base64 - if git_settings['provider'] == 'gitea': - content = base64.b64decode(response.json()['content']).decode('utf-8') - else: - content = response.text - - return jsonify({ - 'success': True, - 'content': content - }) - else: - return jsonify({ - 'message': f'Failed to download docker-compose.yml: {response.json().get("message", "Unknown error")}' - }), 400 - - except Exception as e: - current_app.logger.error(f"Error downloading docker-compose.yml: {str(e)}") - return jsonify({'message': f'Error downloading docker-compose.yml: {str(e)}'}), 500 \ No newline at end of file + return jsonify({'message': 'Setup mail queued for resending'}) \ No newline at end of file diff --git a/routes/launch_api.py b/routes/launch_api.py new file mode 100644 index 0000000..a1b2415 --- /dev/null +++ b/routes/launch_api.py @@ -0,0 +1,712 @@ +from flask import jsonify, request, current_app, Blueprint +from models import ( + KeyValueSettings +) +from extensions import db, csrf +from datetime import datetime +import requests +import base64 +from routes.admin_api import token_required + +launch_api = Blueprint('launch_api', __name__) + +# Connection Settings +@launch_api.route('/test-portainer-connection', methods=['POST']) +@csrf.exempt +def test_portainer_connection(): + data = request.get_json() + url = data.get('url') + api_key = data.get('api_key') + + if not url or not api_key: + return jsonify({'error': 'Missing required fields'}), 400 + + try: + # Test Portainer connection + response = requests.get( + f"{url.rstrip('/')}/api/status", + headers={ + 'X-API-Key': api_key, + 'Accept': 'application/json' + }, + timeout=5 + ) + + if response.status_code == 200: + return jsonify({'message': 'Connection successful'}) + else: + return jsonify({'error': 'Failed to connect to Portainer'}), 400 + + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@launch_api.route('/test-nginx-connection', methods=['POST']) +@csrf.exempt +def test_nginx_connection(): + data = request.get_json() + url = data.get('url') + username = data.get('username') + password = data.get('password') + + if not url or not username or not password: + return jsonify({'error': 'Missing required fields'}), 400 + + try: + # First, get the JWT token + token_response = requests.post( + f"{url.rstrip('/')}/api/tokens", + json={ + 'identity': username, + 'secret': password + }, + headers={'Content-Type': 'application/json'}, + timeout=5 + ) + + if token_response.status_code != 200: + return jsonify({'error': 'Failed to authenticate with NGINX Proxy Manager'}), 400 + + token_data = token_response.json() + token = token_data.get('token') + + if not token: + return jsonify({'error': 'No token received from NGINX Proxy Manager'}), 400 + + # Now test the connection using the token + response = requests.get( + f"{url.rstrip('/')}/api/nginx/proxy-hosts", + headers={ + 'Authorization': f'Bearer {token}', + 'Accept': 'application/json' + }, + timeout=5 + ) + + if response.status_code == 200: + return jsonify({'message': 'Connection successful'}) + else: + return jsonify({'error': 'Failed to connect to NGINX Proxy Manager'}), 400 + + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@launch_api.route('/save-portainer-connection', methods=['POST']) +@csrf.exempt +@token_required +def save_portainer_connection(current_user): + if not current_user.is_admin: + return jsonify({'error': 'Unauthorized'}), 403 + + data = request.get_json() + url = data.get('url') + api_key = data.get('api_key') + + if not url or not api_key: + return jsonify({'error': 'Missing required fields'}), 400 + + try: + # Save Portainer settings + KeyValueSettings.set_value('portainer_settings', { + 'url': url, + 'api_key': api_key + }) + + return jsonify({'message': 'Settings saved successfully'}) + + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@launch_api.route('/save-nginx-connection', methods=['POST']) +@csrf.exempt +@token_required +def save_nginx_connection(current_user): + if not current_user.is_admin: + return jsonify({'error': 'Unauthorized'}), 403 + + data = request.get_json() + url = data.get('url') + username = data.get('username') + password = data.get('password') + + if not url or not username or not password: + return jsonify({'error': 'Missing required fields'}), 400 + + try: + # Save NGINX Proxy Manager settings + KeyValueSettings.set_value('nginx_settings', { + 'url': url, + 'username': username, + 'password': password + }) + + return jsonify({'message': 'Settings saved successfully'}) + + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@launch_api.route('/generate-gitea-token', methods=['POST']) +@csrf.exempt +def generate_gitea_token(): + """Generate a new Gitea API token""" + data = request.get_json() + if not data or 'url' not in data or 'username' not in data or 'password' not in data: + return jsonify({'message': 'Missing required fields'}), 400 + + try: + headers = { + 'Content-Type': 'application/json' + } + + if data.get('otp'): + headers['X-Gitea-OTP'] = data['otp'] + + # Generate token with required scopes + token_data = { + 'name': f'docupulse_{datetime.utcnow().strftime("%Y%m%d_%H%M%S")}', + 'scopes': [ + 'read:activitypub', + 'read:issue', + 'write:misc', + 'read:notification', + 'read:organization', + 'read:package', + 'read:repository', + 'read:user' + ] + } + + # Make request to Gitea API + response = requests.post( + f'{data["url"]}/api/v1/users/{data["username"]}/tokens', + headers=headers, + json=token_data, + auth=(data['username'], data['password']) + ) + + if response.status_code == 201: + token_data = response.json() + return jsonify({ + 'token': token_data['sha1'], + 'name': token_data['name'], + 'token_last_eight': token_data['token_last_eight'] + }), 200 + else: + return jsonify({'message': f'Failed to generate token: {response.json().get("message", "Unknown error")}'}), 400 + + except Exception as e: + return jsonify({'message': f'Failed to generate token: {str(e)}'}), 400 + +@launch_api.route('/list-gitea-repos', methods=['POST']) +@csrf.exempt +def list_gitea_repos(): + """List repositories from Gitea""" + data = request.get_json() + if not data or 'url' not in data or 'token' not in data: + return jsonify({'message': 'Missing required fields'}), 400 + + try: + # Try different authentication methods + headers = { + 'Accept': 'application/json' + } + + # First try token in Authorization header + headers['Authorization'] = f'token {data["token"]}' + + # Get user's repositories + response = requests.get( + f'{data["url"]}/api/v1/user/repos', + headers=headers + ) + + # If that fails, try token as query parameter + if response.status_code != 200: + response = requests.get( + f'{data["url"]}/api/v1/user/repos?token={data["token"]}', + headers={'Accept': 'application/json'} + ) + + if response.status_code == 200: + return jsonify({ + 'repositories': response.json() + }), 200 + else: + return jsonify({'message': f'Failed to list repositories: {response.json().get("message", "Unknown error")}'}), 400 + + except Exception as e: + return jsonify({'message': f'Failed to list repositories: {str(e)}'}), 400 + +@launch_api.route('/list-gitea-branches', methods=['POST']) +@csrf.exempt +def list_gitea_branches(): + """List branches from a Gitea repository""" + data = request.get_json() + if not data or 'url' not in data or 'token' not in data or 'repo' not in data: + return jsonify({'message': 'Missing required fields'}), 400 + + try: + # Try different authentication methods + headers = { + 'Accept': 'application/json' + } + + # First try token in Authorization header + headers['Authorization'] = f'token {data["token"]}' + + # Get repository branches + response = requests.get( + f'{data["url"]}/api/v1/repos/{data["repo"]}/branches', + headers=headers + ) + + # If that fails, try token as query parameter + if response.status_code != 200: + response = requests.get( + f'{data["url"]}/api/v1/repos/{data["repo"]}/branches?token={data["token"]}', + headers={'Accept': 'application/json'} + ) + + if response.status_code == 200: + return jsonify({ + 'branches': response.json() + }), 200 + else: + return jsonify({'message': f'Failed to list branches: {response.json().get("message", "Unknown error")}'}), 400 + + except Exception as e: + return jsonify({'message': f'Failed to list branches: {str(e)}'}), 400 + +@launch_api.route('/list-gitlab-repos', methods=['POST']) +@csrf.exempt +def list_gitlab_repos(): + """List repositories from GitLab""" + data = request.get_json() + if not data or 'url' not in data or 'token' not in data: + return jsonify({'message': 'Missing required fields'}), 400 + + try: + headers = { + 'PRIVATE-TOKEN': data['token'], + 'Accept': 'application/json' + } + + # Get user's projects (repositories) + response = requests.get( + f'{data["url"]}/api/v4/projects', + headers=headers, + params={'membership': 'true'} # Only get projects where user is a member + ) + + if response.status_code == 200: + return jsonify({ + 'repositories': response.json() + }), 200 + else: + return jsonify({'message': f'Failed to list repositories: {response.json().get("message", "Unknown error")}'}), 400 + + except Exception as e: + return jsonify({'message': f'Failed to list repositories: {str(e)}'}), 400 + +@launch_api.route('/test-git-connection', methods=['POST']) +@csrf.exempt +def test_git_connection(): + """Test the connection to a Git repository""" + data = request.get_json() + if not data or 'provider' not in data or 'url' not in data or 'username' not in data or 'token' not in data or 'repo' not in data: + return jsonify({'message': 'Missing required fields'}), 400 + + try: + if data['provider'] == 'gitea': + # Test Gitea connection with different authentication methods + headers = { + 'Accept': 'application/json' + } + + # First try token in Authorization header + headers['Authorization'] = f'token {data["token"]}' + + # Try to get repository information + response = requests.get( + f'{data["url"]}/api/v1/repos/{data["repo"]}', + headers=headers + ) + + # If that fails, try token as query parameter + if response.status_code != 200: + response = requests.get( + f'{data["url"]}/api/v1/repos/{data["repo"]}?token={data["token"]}', + headers={'Accept': 'application/json'} + ) + + if response.status_code == 200: + return jsonify({'message': 'Connection successful'}), 200 + else: + return jsonify({'message': f'Connection failed: {response.json().get("message", "Unknown error")}'}), 400 + + elif data['provider'] == 'gitlab': + # Test GitLab connection + headers = { + 'PRIVATE-TOKEN': data['token'], + 'Accept': 'application/json' + } + + # Try to get repository information + response = requests.get( + f'{data["url"]}/api/v4/projects/{data["repo"].replace("/", "%2F")}', + headers=headers + ) + + if response.status_code == 200: + return jsonify({'message': 'Connection successful'}), 200 + else: + return jsonify({'message': f'Connection failed: {response.json().get("message", "Unknown error")}'}), 400 + + else: + return jsonify({'message': 'Invalid Git provider'}), 400 + + except Exception as e: + return jsonify({'message': f'Connection failed: {str(e)}'}), 400 + +@launch_api.route('/save-git-connection', methods=['POST']) +@csrf.exempt +@token_required +def save_git_connection(current_user): + if not current_user.is_admin: + return jsonify({'error': 'Unauthorized'}), 403 + + data = request.get_json() + provider = data.get('provider') + url = data.get('url') + username = data.get('username') + token = data.get('token') + repo = data.get('repo') + + if not provider or not url or not username or not token or not repo: + return jsonify({'error': 'Missing required fields'}), 400 + + if provider not in ['gitea', 'gitlab']: + return jsonify({'error': 'Invalid provider'}), 400 + + try: + # Save Git settings + KeyValueSettings.set_value('git_settings', { + 'provider': provider, + 'url': url, + 'username': username, + 'token': token, + 'repo': repo + }) + + return jsonify({'message': 'Settings saved successfully'}) + + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@launch_api.route('/create-proxy-host', methods=['POST']) +@csrf.exempt +@token_required +def create_proxy_host(current_user): + if not current_user.is_admin: + return jsonify({'error': 'Unauthorized'}), 403 + + data = request.get_json() + domains = data.get('domains') + scheme = data.get('scheme', 'http') + forward_ip = data.get('forward_ip') + forward_port = data.get('forward_port') + + if not domains or not forward_ip or not forward_port: + return jsonify({'error': 'Missing required fields'}), 400 + + try: + # Get NGINX settings + nginx_settings = KeyValueSettings.get_value('nginx_settings') + if not nginx_settings: + return jsonify({'error': 'NGINX settings not configured'}), 400 + + # First, get the JWT token + token_response = requests.post( + f"{nginx_settings['url'].rstrip('/')}/api/tokens", + json={ + 'identity': nginx_settings['username'], + 'secret': nginx_settings['password'] + }, + headers={'Content-Type': 'application/json'}, + timeout=5 + ) + + if token_response.status_code != 200: + return jsonify({'error': 'Failed to authenticate with NGINX Proxy Manager'}), 400 + + token_data = token_response.json() + token = token_data.get('token') + + if not token: + return jsonify({'error': 'No token received from NGINX Proxy Manager'}), 400 + + # Create the proxy host + proxy_host_data = { + 'domain_names': domains, + 'forward_scheme': scheme, + 'forward_host': forward_ip, + 'forward_port': int(forward_port), + 'ssl_forced': True, + 'caching_enabled': True, + 'block_exploits': True, + 'allow_websocket_upgrade': True, + 'http2_support': True, + 'hsts_enabled': True, + 'hsts_subdomains': True, + 'meta': { + 'letsencrypt_agree': True, + 'dns_challenge': False + } + } + + response = requests.post( + f"{nginx_settings['url'].rstrip('/')}/api/nginx/proxy-hosts", + json=proxy_host_data, + headers={ + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + timeout=5 + ) + + if response.status_code == 200: + return jsonify({ + 'message': 'Proxy host created successfully', + 'data': response.json() + }) + else: + error_data = response.json() + return jsonify({ + 'error': f'Failed to create proxy host: {error_data.get("message", "Unknown error")}' + }), 400 + + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@launch_api.route('/create-ssl-certificate', methods=['POST']) +@csrf.exempt +@token_required +def create_ssl_certificate(current_user): + try: + data = request.get_json() + current_app.logger.info(f"Received request data: {data}") + + domains = data.get('domains') + proxy_host_id = data.get('proxy_host_id') + nginx_url = data.get('nginx_url') + + current_app.logger.info(f"Extracted data - domains: {domains}, proxy_host_id: {proxy_host_id}, nginx_url: {nginx_url}") + + if not all([domains, proxy_host_id, nginx_url]): + missing_fields = [] + if not domains: missing_fields.append('domains') + if not proxy_host_id: missing_fields.append('proxy_host_id') + if not nginx_url: missing_fields.append('nginx_url') + + current_app.logger.error(f"Missing required fields: {missing_fields}") + return jsonify({ + 'success': False, + 'error': f'Missing required fields: {", ".join(missing_fields)}' + }), 400 + + # Get NGINX settings + nginx_settings = KeyValueSettings.get_value('nginx_settings') + if not nginx_settings: + return jsonify({ + 'success': False, + 'error': 'NGINX settings not configured' + }), 400 + + # First, get the JWT token + token_response = requests.post( + f"{nginx_settings['url'].rstrip('/')}/api/tokens", + json={ + 'identity': nginx_settings['username'], + 'secret': nginx_settings['password'] + }, + headers={'Content-Type': 'application/json'}, + timeout=5 + ) + + if token_response.status_code != 200: + return jsonify({ + 'success': False, + 'error': 'Failed to authenticate with NGINX Proxy Manager' + }), 400 + + token_data = token_response.json() + token = token_data.get('token') + + if not token: + return jsonify({ + 'success': False, + 'error': 'No token received from NGINX Proxy Manager' + }), 400 + + # Create the SSL certificate + ssl_request_data = { + 'provider': 'letsencrypt', + 'domain_names': domains, + 'meta': { + 'letsencrypt_agree': True, + 'dns_challenge': False + } + } + current_app.logger.info(f"Making SSL certificate request to {nginx_url}/api/nginx/ssl with data: {ssl_request_data}") + + ssl_response = requests.post( + f"{nginx_url}/api/nginx/ssl", + headers={ + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + json=ssl_request_data + ) + + current_app.logger.info(f"SSL certificate response status: {ssl_response.status_code}") + current_app.logger.info(f"SSL certificate response headers: {dict(ssl_response.headers)}") + + if not ssl_response.ok: + error_text = ssl_response.text + current_app.logger.error(f"Failed to create SSL certificate: {error_text}") + return jsonify({ + 'success': False, + 'error': f'Failed to create SSL certificate: {error_text}' + }), ssl_response.status_code + + ssl_data = ssl_response.json() + current_app.logger.info(f"SSL certificate created successfully: {ssl_data}") + + # Get the certificate ID + cert_id = ssl_data.get('id') + if not cert_id: + current_app.logger.error("No certificate ID received in response") + return jsonify({ + 'success': False, + 'error': 'No certificate ID received' + }), 500 + + # Update the proxy host with the certificate + update_request_data = { + 'ssl_certificate_id': cert_id + } + current_app.logger.info(f"Updating proxy host {proxy_host_id} with data: {update_request_data}") + + update_response = requests.put( + f"{nginx_url}/api/nginx/proxy-hosts/{proxy_host_id}", + headers={ + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + json=update_request_data + ) + + current_app.logger.info(f"Update response status: {update_response.status_code}") + current_app.logger.info(f"Update response headers: {dict(update_response.headers)}") + + if not update_response.ok: + error_text = update_response.text + current_app.logger.error(f"Failed to update proxy host: {error_text}") + return jsonify({ + 'success': False, + 'error': f'Failed to update proxy host: {error_text}' + }), update_response.status_code + + update_data = update_response.json() + current_app.logger.info(f"Proxy host updated successfully: {update_data}") + + return jsonify({ + 'success': True, + 'data': { + 'certificate': ssl_data, + 'proxy_host': update_data + } + }) + + except Exception as e: + current_app.logger.error(f"Error in create_ssl_certificate: {str(e)}") + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + +@launch_api.route('/download-docker-compose', methods=['POST']) +@csrf.exempt +def download_docker_compose(): + """Download docker-compose.yml from the repository""" + data = request.get_json() + if not data or 'repository' not in data or 'branch' not in data: + return jsonify({'message': 'Missing required fields'}), 400 + + try: + # Get Git settings + git_settings = KeyValueSettings.get_value('git_settings') + if not git_settings: + return jsonify({'message': 'Git settings not configured'}), 400 + + # Determine the provider and set up the appropriate API call + if git_settings['provider'] == 'gitea': + # For Gitea + headers = { + 'Accept': 'application/json', + 'Authorization': f'token {git_settings["token"]}' + } + + # Try to get the file content + response = requests.get( + f'{git_settings["url"]}/api/v1/repos/{data["repository"]}/contents/docker-compose.yml', + headers=headers, + params={'ref': data['branch']} + ) + + if response.status_code != 200: + # Try token as query parameter if header auth fails + response = requests.get( + f'{git_settings["url"]}/api/v1/repos/{data["repository"]}/contents/docker-compose.yml?token={git_settings["token"]}', + headers={'Accept': 'application/json'}, + params={'ref': data['branch']} + ) + + elif git_settings['provider'] == 'gitlab': + # For GitLab + headers = { + 'PRIVATE-TOKEN': git_settings['token'], + 'Accept': 'application/json' + } + + # Get the file content + response = requests.get( + f'{git_settings["url"]}/api/v4/projects/{data["repository"].replace("/", "%2F")}/repository/files/docker-compose.yml/raw', + headers=headers, + params={'ref': data['branch']} + ) + + else: + return jsonify({'message': 'Unsupported Git provider'}), 400 + + if response.status_code == 200: + # For Gitea, we need to decode the content from base64 + if git_settings['provider'] == 'gitea': + content = base64.b64decode(response.json()['content']).decode('utf-8') + else: + content = response.text + + return jsonify({ + 'success': True, + 'content': content + }) + else: + return jsonify({ + 'message': f'Failed to download docker-compose.yml: {response.json().get("message", "Unknown error")}' + }), 400 + + except Exception as e: + current_app.logger.error(f"Error downloading docker-compose.yml: {str(e)}") + return jsonify({'message': f'Error downloading docker-compose.yml: {str(e)}'}), 500 \ No newline at end of file diff --git a/static/css/launch_progress.css b/static/css/launch_progress.css new file mode 100644 index 0000000..c97661e --- /dev/null +++ b/static/css/launch_progress.css @@ -0,0 +1,95 @@ +.launch-steps-container { + max-height: calc(100vh - 600px); + min-height: 300px; + overflow-y: auto; + padding-right: 1rem; +} + +.launch-steps-container::-webkit-scrollbar { + width: 8px; +} + +.launch-steps-container::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +.launch-steps-container::-webkit-scrollbar-thumb { + background: #888; + border-radius: 4px; +} + +.launch-steps-container::-webkit-scrollbar-thumb:hover { + background: #555; +} + +.step-item { + display: flex; + align-items: flex-start; + margin-bottom: 1.5rem; + padding: 1rem; + border-radius: 8px; + background-color: #f8f9fa; + transition: all 0.3s ease; +} + +.step-item.active { + background-color: #e3f2fd; +} + +.step-item.completed { + background-color: #e8f5e9; +} + +.step-item.failed { + background-color: #ffebee; +} + +.step-icon { + width: 40px; + height: 40px; + border-radius: 50%; + background-color: #e9ecef; + display: flex; + align-items: center; + justify-content: center; + margin-right: 1rem; + flex-shrink: 0; +} + +.step-item.active .step-icon { + background-color: var(--primary-color); + color: white; +} + +.step-item.completed .step-icon { + background-color: #28a745; + color: white; +} + +.step-item.failed .step-icon { + background-color: #dc3545; + color: white; +} + +.step-content { + flex-grow: 1; +} + +.step-content h5 { + margin-bottom: 0.25rem; +} + +.step-status { + margin: 0; + font-size: 0.875rem; + color: #6c757d; +} + +.step-item.completed .step-status { + color: #28a745; +} + +.step-item.failed .step-status { + color: #dc3545; +} \ No newline at end of file diff --git a/static/js/launch_progress.js b/static/js/launch_progress.js new file mode 100644 index 0000000..1ff5ed7 --- /dev/null +++ b/static/js/launch_progress.js @@ -0,0 +1,915 @@ +document.addEventListener('DOMContentLoaded', function() { + // Get the launch data from sessionStorage + const launchData = JSON.parse(sessionStorage.getItem('instanceLaunchData')); + if (!launchData) { + showError('No launch data found. Please start over.'); + return; + } + + // Initialize the steps + initializeSteps(); + + // Start the launch process + startLaunch(launchData); +}); + +function initializeSteps() { + const stepsContainer = document.getElementById('stepsContainer'); + + // Add DNS check step + const dnsStep = document.createElement('div'); + dnsStep.className = 'step-item'; + dnsStep.innerHTML = ` +
+
+
Checking DNS Records
+

Verifying domain configurations...

+
+ `; + stepsContainer.appendChild(dnsStep); + + // Add NGINX connection check step + const nginxStep = document.createElement('div'); + nginxStep.className = 'step-item'; + nginxStep.innerHTML = ` +
+
+
Checking NGINX Connection
+

Verifying connection to NGINX Proxy Manager...

+
+ `; + stepsContainer.appendChild(nginxStep); + + // Add SSL Certificate generation step + const sslStep = document.createElement('div'); + sslStep.className = 'step-item'; + sslStep.innerHTML = ` +
+
+
Generating SSL Certificate
+

Setting up secure HTTPS connection...

+
+ `; + stepsContainer.appendChild(sslStep); + + // Add Proxy Host creation step + const proxyStep = document.createElement('div'); + proxyStep.className = 'step-item'; + proxyStep.innerHTML = ` +
+
+
Creating Proxy Host
+

Setting up NGINX proxy host configuration...

+
+ `; + stepsContainer.appendChild(proxyStep); + + // Add Portainer connection check step + const portainerStep = document.createElement('div'); + portainerStep.className = 'step-item'; + portainerStep.innerHTML = ` +
+
+
Checking Portainer Connection
+

Verifying connection to Portainer...

+
+ `; + stepsContainer.appendChild(portainerStep); + + // Add Docker Compose download step + const dockerComposeStep = document.createElement('div'); + dockerComposeStep.className = 'step-item'; + dockerComposeStep.innerHTML = ` +
+
+
Downloading Docker Compose
+

Fetching docker-compose.yml from repository...

+
+ `; + stepsContainer.appendChild(dockerComposeStep); +} + +async function startLaunch(data) { + try { + // Step 1: Check DNS records + await updateStep(1, 'Checking DNS Records', 'Verifying domain configurations...'); + const dnsResult = await checkDNSRecords(data.webAddresses); + + // Check if any domains failed to resolve + const failedDomains = Object.entries(dnsResult.results) + .filter(([_, result]) => !result.resolved) + .map(([domain]) => domain); + + if (failedDomains.length > 0) { + throw new Error(`DNS records not found for: ${failedDomains.join(', ')}`); + } + + // Update the step to show success + const dnsStep = document.querySelectorAll('.step-item')[0]; + dnsStep.classList.remove('active'); + dnsStep.classList.add('completed'); + + // Create a details section for DNS results + const detailsSection = document.createElement('div'); + detailsSection.className = 'mt-3'; + detailsSection.innerHTML = ` +
+
+
DNS Check Results
+
+ + + + + + + + + + + ${Object.entries(dnsResult.results).map(([domain, result]) => ` + + + + + + + `).join('')} + +
DomainStatusIP AddressTTL
${domain} + + ${result.resolved ? 'Resolved' : 'Not Found'} + + ${result.ip || 'N/A'}${result.ttl || 'N/A'}
+
+
+
+ `; + + // Add the details section after the status text + const statusText = dnsStep.querySelector('.step-status'); + statusText.textContent = 'DNS records verified successfully'; + statusText.after(detailsSection); + + // Step 2: Check NGINX connection + await updateStep(2, 'Checking NGINX Connection', 'Verifying connection to NGINX Proxy Manager...'); + const nginxResult = await checkNginxConnection(); + + if (!nginxResult.success) { + throw new Error(nginxResult.error || 'Failed to connect to NGINX Proxy Manager'); + } + + // Update the step to show success + const nginxStep = document.querySelectorAll('.step-item')[1]; + nginxStep.classList.remove('active'); + nginxStep.classList.add('completed'); + nginxStep.querySelector('.step-status').textContent = 'Successfully connected to NGINX Proxy Manager'; + + // Step 3: Generate SSL Certificate + await updateStep(3, 'Generating SSL Certificate', 'Setting up secure HTTPS connection...'); + const sslResult = await generateSSLCertificate(data.webAddresses); + + if (!sslResult.success) { + throw new Error(sslResult.error || 'Failed to generate SSL certificate'); + } + + // Step 4: Create Proxy Host + await updateStep(4, 'Creating Proxy Host', 'Setting up NGINX proxy host configuration...'); + const proxyResult = await createProxyHost(data.webAddresses, data.port, sslResult.data.certificate.id); + + if (!proxyResult.success) { + throw new Error(proxyResult.error || 'Failed to create proxy host'); + } + + // Step 5: Check Portainer connection + await updateStep(5, 'Checking Portainer Connection', 'Verifying connection to Portainer...'); + const portainerResult = await checkPortainerConnection(); + + if (!portainerResult.success) { + throw new Error(portainerResult.message || 'Failed to connect to Portainer'); + } + + // Update the step to show success + const portainerStep = document.querySelectorAll('.step-item')[4]; + portainerStep.classList.remove('active'); + portainerStep.classList.add('completed'); + portainerStep.querySelector('.step-status').textContent = portainerResult.message; + + // Step 6: Download Docker Compose + await updateStep(6, 'Downloading Docker Compose', 'Fetching docker-compose.yml from repository...'); + const dockerComposeResult = await downloadDockerCompose(data.repository, data.branch); + + if (!dockerComposeResult.success) { + throw new Error(dockerComposeResult.error || 'Failed to download docker-compose.yml'); + } + + // Update the step to show success + const dockerComposeStep = document.querySelectorAll('.step-item')[5]; + dockerComposeStep.classList.remove('active'); + dockerComposeStep.classList.add('completed'); + dockerComposeStep.querySelector('.step-status').textContent = 'Successfully downloaded docker-compose.yml'; + + // Add download button + const downloadButton = document.createElement('button'); + downloadButton.className = 'btn btn-sm btn-primary mt-2'; + downloadButton.innerHTML = ' Download docker-compose.yml'; + downloadButton.onclick = () => { + const blob = new Blob([dockerComposeResult.content], { type: 'text/yaml' }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'docker-compose.yml'; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + }; + dockerComposeStep.querySelector('.step-content').appendChild(downloadButton); + + } catch (error) { + showError(error.message); + } +} + +async function checkDNSRecords(domains) { + try { + const response = await fetch('/api/check-dns', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content + }, + body: JSON.stringify({ domains }) + }); + + if (!response.ok) { + throw new Error('Failed to check DNS records'); + } + + const result = await response.json(); + console.log('DNS check result:', result); + return result; + } catch (error) { + console.error('Error checking DNS records:', error); + throw error; + } +} + +async function checkNginxConnection() { + try { + // Get NGINX settings from the template + const nginxSettings = { + url: window.nginxSettings?.url || '', + username: window.nginxSettings?.username || '', + password: window.nginxSettings?.password || '' + }; + + // Debug log the settings (without password) + console.log('NGINX Settings:', { + url: nginxSettings.url, + username: nginxSettings.username, + hasPassword: !!nginxSettings.password + }); + + // Check if any required field is missing + if (!nginxSettings.url || !nginxSettings.username || !nginxSettings.password) { + return { + success: false, + error: 'NGINX settings are not configured. Please configure NGINX settings in the admin panel.' + }; + } + + // First, get the token + const tokenResponse = await fetch(`${nginxSettings.url}/api/tokens`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + identity: nginxSettings.username, + secret: nginxSettings.password + }) + }); + + if (!tokenResponse.ok) { + const errorText = await tokenResponse.text(); + console.error('Token Error Response:', errorText); + try { + const errorJson = JSON.parse(errorText); + throw new Error(`Failed to authenticate with NGINX: ${errorJson.message || errorText}`); + } catch (e) { + throw new Error(`Failed to authenticate with NGINX: ${errorText}`); + } + } + + const tokenData = await tokenResponse.json(); + const token = tokenData.token; + + if (!token) { + throw new Error('No token received from NGINX Proxy Manager'); + } + + // Now test the connection using the token + const response = await fetch(`${nginxSettings.url}/api/nginx/proxy-hosts`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('NGINX connection error:', errorText); + try { + const errorJson = JSON.parse(errorText); + throw new Error(errorJson.message || 'Failed to connect to NGINX Proxy Manager'); + } catch (e) { + throw new Error(`Failed to connect to NGINX Proxy Manager: ${errorText}`); + } + } + + return { success: true }; + } catch (error) { + console.error('Error checking NGINX connection:', error); + return { + success: false, + error: error.message || 'Error checking NGINX connection' + }; + } +} + +async function checkPortainerConnection() { + try { + // Get Portainer settings from the template + const portainerSettings = { + url: window.portainerSettings?.url || '', + api_key: window.portainerSettings?.api_key || '' + }; + + // Debug log the settings (without API key) + console.log('Portainer Settings:', { + url: portainerSettings.url, + hasApiKey: !!portainerSettings.api_key + }); + + // Check if any required field is missing + if (!portainerSettings.url || !portainerSettings.api_key) { + console.error('Missing Portainer settings:', portainerSettings); + return { + success: false, + message: 'Portainer settings are not configured. Please configure Portainer settings in the admin panel.' + }; + } + + const response = await fetch('/api/admin/test-portainer-connection', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content + }, + body: JSON.stringify({ + url: portainerSettings.url, + api_key: portainerSettings.api_key + }) + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Failed to connect to Portainer'); + } + + return { + success: true, + message: 'Successfully connected to Portainer' + }; + } catch (error) { + console.error('Portainer connection error:', error); + return { + success: false, + message: error.message || 'Failed to connect to Portainer' + }; + } +} + +function updateStatus(step, message, type = 'info', details = '') { + const statusElement = document.getElementById(`${step}Status`); + const detailsElement = document.getElementById(`${step}Details`); + + if (statusElement) { + // Remove any existing status classes + statusElement.classList.remove('text-info', 'text-success', 'text-danger'); + + // Add appropriate class based on type + switch (type) { + case 'success': + statusElement.classList.add('text-success'); + break; + case 'error': + statusElement.classList.add('text-danger'); + break; + default: + statusElement.classList.add('text-info'); + } + + statusElement.textContent = message; + } + + if (detailsElement) { + detailsElement.innerHTML = details; + } +} + +async function createProxyHost(domains, port, sslCertificateId) { + try { + // Get NGINX settings from the template + const nginxSettings = { + url: window.nginxSettings?.url || '', + username: window.nginxSettings?.username || '', + password: window.nginxSettings?.password || '' + }; + + console.log('NGINX Settings:', { ...nginxSettings, password: '***' }); + + // Update status to show we're getting the token + updateStatus('proxy', 'Getting authentication token...', 'info'); + + // First, get the JWT token from NGINX + const tokenResponse = await fetch(`${nginxSettings.url}/api/tokens`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + identity: nginxSettings.username, + secret: nginxSettings.password + }) + }); + + console.log('Token Response Status:', tokenResponse.status); + console.log('Token Response Headers:', Object.fromEntries(tokenResponse.headers.entries())); + + if (!tokenResponse.ok) { + const errorText = await tokenResponse.text(); + console.error('Token Error Response:', errorText); + try { + const errorJson = JSON.parse(errorText); + throw new Error(`Failed to authenticate with NGINX: ${errorJson.message || errorText}`); + } catch (e) { + throw new Error(`Failed to authenticate with NGINX: ${errorText}`); + } + } + + const tokenData = await tokenResponse.json(); + console.log('Token Data:', { ...tokenData, token: tokenData.token ? '***' : null }); + const token = tokenData.token; + + if (!token) { + throw new Error('No token received from NGINX Proxy Manager'); + } + + // Store the token in sessionStorage for later use + sessionStorage.setItem('nginxToken', token); + + // Check if a proxy host already exists with the same properties + const proxyHostsResponse = await fetch(`${nginxSettings.url}/api/nginx/proxy-hosts`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + if (!proxyHostsResponse.ok) { + throw new Error('Failed to fetch existing proxy hosts'); + } + const proxyHosts = await proxyHostsResponse.json(); + const existingProxy = proxyHosts.find(ph => { + const sameDomains = Array.isArray(ph.domain_names) && + ph.domain_names.length === domains.length && + domains.every(d => ph.domain_names.includes(d)); + return ( + sameDomains && + ph.forward_scheme === 'http' && + ph.forward_host === '192.168.68.124' && + parseInt(ph.forward_port) === parseInt(port) + ); + }); + + let result; + if (existingProxy) { + console.log('Found existing proxy host:', existingProxy); + result = existingProxy; + } else { + // Update status to show we're creating the proxy host + updateStatus('proxy', 'Creating proxy host configuration...', 'info'); + + const proxyHostData = { + domain_names: domains, + forward_scheme: 'http', + forward_host: '192.168.68.124', + forward_port: parseInt(port), + ssl_forced: true, + caching_enabled: true, + block_exploits: true, + allow_websocket_upgrade: true, + http2_support: true, + hsts_enabled: true, + hsts_subdomains: true, + certificate_id: sslCertificateId, + meta: { + letsencrypt_agree: true, + dns_challenge: false + } + }; + + console.log('Creating proxy host with data:', proxyHostData); + + // Create the proxy host with NGINX + const response = await fetch(`${nginxSettings.url}/api/nginx/proxy-hosts`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(proxyHostData) + }); + + console.log('Proxy Host Response Status:', response.status); + console.log('Proxy Host Response Headers:', Object.fromEntries(response.headers.entries())); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Proxy Host Error Response:', errorText); + try { + const errorJson = JSON.parse(errorText); + const errorMessage = errorJson.error?.message || errorText; + // Check if the error is about a domain already being in use + if (errorMessage.includes('is already in use')) { + const domain = errorMessage.split(' ')[0]; + throw new Error(`Domain ${domain} is already configured in NGINX Proxy Manager. Please remove it from NGINX Proxy Manager and try again.`); + } + throw new Error(`Failed to create proxy host: ${errorMessage}`); + } catch (e) { + if (e.message.includes('is already configured in NGINX Proxy Manager')) { + throw e; // Re-throw the domain in use error + } + throw new Error(`Failed to create proxy host: ${errorText}`); + } + } + + result = await response.json(); + console.log('Proxy Host Creation Result:', result); + } + + // Create a detailed success message with NGINX Proxy results + const successDetails = ` +
+
+
+
NGINX Proxy Results
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
Proxy Host ID${result.id || 'N/A'}
Domains${domains.join(', ')}
Forward Schemehttp
Forward Host192.168.68.124
Forward Port${parseInt(port)}
SSL Status + Forced +
SSL Certificate + Using Certificate ID: ${sslCertificateId} +
Security Features + Block Exploits + HSTS + HTTP/2 +
Performance + Caching + WebSocket +
+
+
+
+
+ `; + + // Update the proxy step to show success and add the results + const proxyStep = document.querySelectorAll('.step-item')[3]; + proxyStep.classList.remove('active'); + proxyStep.classList.add('completed'); + const statusText = proxyStep.querySelector('.step-status'); + statusText.textContent = existingProxy ? 'Using existing proxy host' : 'Successfully created proxy host'; + statusText.after(document.createRange().createContextualFragment(successDetails)); + + return { + success: true, + data: result + }; + } catch (error) { + console.error('Error creating proxy host:', error); + // Update status with error message + updateStatus('proxy', `Failed: ${error.message}`, 'error'); + return { + success: false, + error: error.message + }; + } +} + +async function generateSSLCertificate(domains) { + try { + // Get NGINX settings from the template + const nginxSettings = { + url: window.nginxSettings?.url || '', + username: window.nginxSettings?.username || '', + password: window.nginxSettings?.password || '', + email: window.nginxSettings?.email || '' + }; + + // Get a fresh token from NGINX + const tokenResponse = await fetch(`${nginxSettings.url}/api/tokens`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + identity: nginxSettings.username, + secret: nginxSettings.password + }) + }); + + if (!tokenResponse.ok) { + const errorText = await tokenResponse.text(); + console.error('Token Error Response:', errorText); + throw new Error(`Failed to authenticate with NGINX: ${errorText}`); + } + + const tokenData = await tokenResponse.json(); + const token = tokenData.token; + + if (!token) { + throw new Error('No token received from NGINX Proxy Manager'); + } + + // First, check if a certificate already exists for these domains + const checkResponse = await fetch(`${nginxSettings.url}/api/nginx/certificates`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + } + }); + + if (!checkResponse.ok) { + throw new Error('Failed to check existing certificates'); + } + + const existingCertificates = await checkResponse.json(); + const existingCertificate = existingCertificates.find(cert => { + const certDomains = cert.domain_names || []; + return domains.every(domain => certDomains.includes(domain)) && + certDomains.length === domains.length; + }); + + let result; + let usedExisting = false; + if (existingCertificate) { + console.log('Found existing certificate:', existingCertificate); + result = existingCertificate; + usedExisting = true; + } else { + // Create the SSL certificate directly with NGINX + const requestBody = { + domain_names: domains, + meta: { + letsencrypt_email: nginxSettings.email, + letsencrypt_agree: true, + dns_challenge: false + }, + provider: 'letsencrypt' + }; + console.log('Request Body:', requestBody); + + const response = await fetch(`${nginxSettings.url}/api/nginx/certificates`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(requestBody) + }); + + console.log('Response Status:', response.status); + console.log('Response Headers:', Object.fromEntries(response.headers.entries())); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Certificate creation error:', errorText); + throw new Error(`Failed to generate SSL certificate: ${errorText}`); + } + + result = await response.json(); + console.log('Certificate creation result:', result); + } + + // Update the SSL step to show success + const sslStep = document.querySelectorAll('.step-item')[2]; + sslStep.classList.remove('active'); + sslStep.classList.add('completed'); + const sslStatusText = sslStep.querySelector('.step-status'); + sslStatusText.textContent = usedExisting ? + 'Using existing SSL certificate' : + 'SSL certificate generated successfully'; + + // Always add SSL certificate details + const sslDetails = document.createElement('div'); + sslDetails.className = 'mt-3'; + sslDetails.innerHTML = ` +
+
+
SSL Certificate Details
+
+ + + + + + + + + + + + + + + + + + + + + +
PropertyValue
Certificate ID${result.id || 'N/A'}
Domains${(result.domain_names || domains).join(', ')}
Provider${result.provider || 'Let\'s Encrypt'}
+
+
+
+ `; + sslStatusText.after(sslDetails); + + return { + success: true, + data: { + certificate: result + } + }; + } catch (error) { + console.error('Error generating SSL certificate:', error); + return { + success: false, + error: error.message + }; + } +} + +function scrollToStep(stepElement) { + const container = document.querySelector('.launch-steps-container'); + const containerRect = container.getBoundingClientRect(); + const elementRect = stepElement.getBoundingClientRect(); + + // Calculate the scroll position to center the element in the container + const scrollTop = elementRect.top - containerRect.top - (containerRect.height / 2) + (elementRect.height / 2); + + // Smooth scroll to the element + container.scrollTo({ + top: container.scrollTop + scrollTop, + behavior: 'smooth' + }); +} + +function updateStep(stepNumber, title, description) { + return new Promise((resolve) => { + // Update the current step in the header + document.getElementById('currentStep').textContent = title; + document.getElementById('stepDescription').textContent = description; + + // Update progress bar + const progress = (stepNumber - 1) * 20; + const progressBar = document.getElementById('launchProgress'); + progressBar.style.width = `${progress}%`; + progressBar.textContent = `${progress}%`; + + // Update step items + const steps = document.querySelectorAll('.step-item'); + steps.forEach((item, index) => { + const step = index + 1; + item.classList.remove('active', 'completed', 'failed'); + + if (step < stepNumber) { + item.classList.add('completed'); + item.querySelector('.step-status').textContent = 'Completed'; + } else if (step === stepNumber) { + item.classList.add('active'); + item.querySelector('.step-status').textContent = description; + // Scroll to the active step + scrollToStep(item); + } + }); + + // Simulate some work being done + setTimeout(resolve, 1000); + }); +} + +function showError(message) { + const errorContainer = document.getElementById('errorContainer'); + const errorMessage = document.getElementById('errorMessage'); + + errorMessage.textContent = message; + errorContainer.style.display = 'block'; + + // Update the current step to show error + const currentStep = document.querySelector('.step-item.active'); + if (currentStep) { + currentStep.classList.add('failed'); + currentStep.querySelector('.step-status').textContent = 'Failed: ' + message; + } +} + +function retryLaunch() { + // Reload the page to start over + window.location.reload(); +} + +// Add new function to download docker-compose.yml +async function downloadDockerCompose(repo, branch) { + try { + const response = await fetch('/api/admin/download-docker-compose', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content + }, + body: JSON.stringify({ + repository: repo, + branch: branch + }) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Failed to download docker-compose.yml'); + } + + const result = await response.json(); + return { + success: true, + content: result.content + }; + } catch (error) { + console.error('Error downloading docker-compose.yml:', error); + return { + success: false, + error: error.message + }; + } +} \ No newline at end of file diff --git a/static/js/settings/connections.js b/static/js/settings/connections.js index b639a62..f5c1449 100644 --- a/static/js/settings/connections.js +++ b/static/js/settings/connections.js @@ -156,42 +156,37 @@ async function testGitConnection(provider) { // Test Portainer Connection async function testPortainerConnection() { - const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal')); - const messageElement = document.getElementById('saveConnectionMessage'); - messageElement.textContent = 'Testing connection...'; - messageElement.className = ''; - saveModal.show(); - + const url = document.getElementById('portainerUrl').value; + const apiKey = document.getElementById('portainerApiKey').value; + + if (!url || !apiKey) { + showError('Please fill in all fields'); + return; + } + try { - const url = document.getElementById('portainerUrl').value; - const apiKey = document.getElementById('portainerApiKey').value; - - if (!url || !apiKey) { - throw new Error('Please fill in all required fields'); - } - const response = await fetch('/api/admin/test-portainer-connection', { method: 'POST', headers: { 'Content-Type': 'application/json', - 'X-CSRF-Token': getCsrfToken() + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').content }, body: JSON.stringify({ url: url, api_key: apiKey }) }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Connection test failed'); + + const data = await response.json(); + + if (response.ok) { + showSuccess('Connection successful'); + } else { + showError(data.error || 'Failed to connect to Portainer'); } - - messageElement.textContent = 'Connection test successful!'; - messageElement.className = 'text-success'; } catch (error) { - messageElement.textContent = error.message || 'Connection test failed'; - messageElement.className = 'text-danger'; + console.error('Error:', error); + showError('Failed to connect to Portainer'); } } diff --git a/templates/main/launch_progress.html b/templates/main/launch_progress.html index cad6dfc..4f47708 100644 --- a/templates/main/launch_progress.html +++ b/templates/main/launch_progress.html @@ -3,6 +3,10 @@ {% block title %}Launching Instance - DocuPulse{% endblock %} +{% block extra_css %} + +{% endblock %} + {% block content %} {{ header( title="Launching Instance", @@ -48,996 +52,22 @@ - - - {% endblock %} {% block extra_js %} + {% endblock %} \ No newline at end of file