Split files
This commit is contained in:
2
app.py
2
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():
|
||||
|
||||
@@ -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
|
||||
return jsonify({'message': 'Setup mail queued for resending'})
|
||||
712
routes/launch_api.py
Normal file
712
routes/launch_api.py
Normal file
@@ -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
|
||||
95
static/css/launch_progress.css
Normal file
95
static/css/launch_progress.css
Normal file
@@ -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;
|
||||
}
|
||||
915
static/js/launch_progress.js
Normal file
915
static/js/launch_progress.js
Normal file
@@ -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 = `
|
||||
<div class="step-icon"><i class="fas fa-globe"></i></div>
|
||||
<div class="step-content">
|
||||
<h5>Checking DNS Records</h5>
|
||||
<p class="step-status">Verifying domain configurations...</p>
|
||||
</div>
|
||||
`;
|
||||
stepsContainer.appendChild(dnsStep);
|
||||
|
||||
// Add NGINX connection check step
|
||||
const nginxStep = document.createElement('div');
|
||||
nginxStep.className = 'step-item';
|
||||
nginxStep.innerHTML = `
|
||||
<div class="step-icon"><i class="fas fa-network-wired"></i></div>
|
||||
<div class="step-content">
|
||||
<h5>Checking NGINX Connection</h5>
|
||||
<p class="step-status">Verifying connection to NGINX Proxy Manager...</p>
|
||||
</div>
|
||||
`;
|
||||
stepsContainer.appendChild(nginxStep);
|
||||
|
||||
// Add SSL Certificate generation step
|
||||
const sslStep = document.createElement('div');
|
||||
sslStep.className = 'step-item';
|
||||
sslStep.innerHTML = `
|
||||
<div class="step-icon"><i class="fas fa-lock"></i></div>
|
||||
<div class="step-content">
|
||||
<h5>Generating SSL Certificate</h5>
|
||||
<p class="step-status">Setting up secure HTTPS connection...</p>
|
||||
</div>
|
||||
`;
|
||||
stepsContainer.appendChild(sslStep);
|
||||
|
||||
// Add Proxy Host creation step
|
||||
const proxyStep = document.createElement('div');
|
||||
proxyStep.className = 'step-item';
|
||||
proxyStep.innerHTML = `
|
||||
<div class="step-icon"><i class="fas fa-server"></i></div>
|
||||
<div class="step-content">
|
||||
<h5>Creating Proxy Host</h5>
|
||||
<p class="step-status">Setting up NGINX proxy host configuration...</p>
|
||||
</div>
|
||||
`;
|
||||
stepsContainer.appendChild(proxyStep);
|
||||
|
||||
// Add Portainer connection check step
|
||||
const portainerStep = document.createElement('div');
|
||||
portainerStep.className = 'step-item';
|
||||
portainerStep.innerHTML = `
|
||||
<div class="step-icon"><i class="fab fa-docker"></i></div>
|
||||
<div class="step-content">
|
||||
<h5>Checking Portainer Connection</h5>
|
||||
<p class="step-status">Verifying connection to Portainer...</p>
|
||||
</div>
|
||||
`;
|
||||
stepsContainer.appendChild(portainerStep);
|
||||
|
||||
// Add Docker Compose download step
|
||||
const dockerComposeStep = document.createElement('div');
|
||||
dockerComposeStep.className = 'step-item';
|
||||
dockerComposeStep.innerHTML = `
|
||||
<div class="step-icon"><i class="fas fa-file-code"></i></div>
|
||||
<div class="step-content">
|
||||
<h5>Downloading Docker Compose</h5>
|
||||
<p class="step-status">Fetching docker-compose.yml from repository...</p>
|
||||
</div>
|
||||
`;
|
||||
stepsContainer.appendChild(dockerComposeStep);
|
||||
}
|
||||
|
||||
async function startLaunch(data) {
|
||||
try {
|
||||
// Step 1: Check DNS records
|
||||
await updateStep(1, 'Checking DNS Records', 'Verifying domain configurations...');
|
||||
const dnsResult = await checkDNSRecords(data.webAddresses);
|
||||
|
||||
// Check if any domains failed to resolve
|
||||
const failedDomains = Object.entries(dnsResult.results)
|
||||
.filter(([_, result]) => !result.resolved)
|
||||
.map(([domain]) => domain);
|
||||
|
||||
if (failedDomains.length > 0) {
|
||||
throw new Error(`DNS records not found for: ${failedDomains.join(', ')}`);
|
||||
}
|
||||
|
||||
// Update the step to show success
|
||||
const dnsStep = document.querySelectorAll('.step-item')[0];
|
||||
dnsStep.classList.remove('active');
|
||||
dnsStep.classList.add('completed');
|
||||
|
||||
// Create a details section for DNS results
|
||||
const detailsSection = document.createElement('div');
|
||||
detailsSection.className = 'mt-3';
|
||||
detailsSection.innerHTML = `
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title mb-3">DNS Check Results</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th>Status</th>
|
||||
<th>IP Address</th>
|
||||
<th>TTL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${Object.entries(dnsResult.results).map(([domain, result]) => `
|
||||
<tr>
|
||||
<td>${domain}</td>
|
||||
<td>
|
||||
<span class="badge bg-${result.resolved ? 'success' : 'danger'}">
|
||||
${result.resolved ? 'Resolved' : 'Not Found'}
|
||||
</span>
|
||||
</td>
|
||||
<td>${result.ip || 'N/A'}</td>
|
||||
<td>${result.ttl || 'N/A'}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add the details section after the status text
|
||||
const statusText = dnsStep.querySelector('.step-status');
|
||||
statusText.textContent = 'DNS records verified successfully';
|
||||
statusText.after(detailsSection);
|
||||
|
||||
// Step 2: Check NGINX connection
|
||||
await updateStep(2, 'Checking NGINX Connection', 'Verifying connection to NGINX Proxy Manager...');
|
||||
const nginxResult = await checkNginxConnection();
|
||||
|
||||
if (!nginxResult.success) {
|
||||
throw new Error(nginxResult.error || 'Failed to connect to NGINX Proxy Manager');
|
||||
}
|
||||
|
||||
// Update the step to show success
|
||||
const nginxStep = document.querySelectorAll('.step-item')[1];
|
||||
nginxStep.classList.remove('active');
|
||||
nginxStep.classList.add('completed');
|
||||
nginxStep.querySelector('.step-status').textContent = 'Successfully connected to NGINX Proxy Manager';
|
||||
|
||||
// Step 3: Generate SSL Certificate
|
||||
await updateStep(3, 'Generating SSL Certificate', 'Setting up secure HTTPS connection...');
|
||||
const sslResult = await generateSSLCertificate(data.webAddresses);
|
||||
|
||||
if (!sslResult.success) {
|
||||
throw new Error(sslResult.error || 'Failed to generate SSL certificate');
|
||||
}
|
||||
|
||||
// Step 4: Create Proxy Host
|
||||
await updateStep(4, 'Creating Proxy Host', 'Setting up NGINX proxy host configuration...');
|
||||
const proxyResult = await createProxyHost(data.webAddresses, data.port, sslResult.data.certificate.id);
|
||||
|
||||
if (!proxyResult.success) {
|
||||
throw new Error(proxyResult.error || 'Failed to create proxy host');
|
||||
}
|
||||
|
||||
// Step 5: Check Portainer connection
|
||||
await updateStep(5, 'Checking Portainer Connection', 'Verifying connection to Portainer...');
|
||||
const portainerResult = await checkPortainerConnection();
|
||||
|
||||
if (!portainerResult.success) {
|
||||
throw new Error(portainerResult.message || 'Failed to connect to Portainer');
|
||||
}
|
||||
|
||||
// Update the step to show success
|
||||
const portainerStep = document.querySelectorAll('.step-item')[4];
|
||||
portainerStep.classList.remove('active');
|
||||
portainerStep.classList.add('completed');
|
||||
portainerStep.querySelector('.step-status').textContent = portainerResult.message;
|
||||
|
||||
// Step 6: Download Docker Compose
|
||||
await updateStep(6, 'Downloading Docker Compose', 'Fetching docker-compose.yml from repository...');
|
||||
const dockerComposeResult = await downloadDockerCompose(data.repository, data.branch);
|
||||
|
||||
if (!dockerComposeResult.success) {
|
||||
throw new Error(dockerComposeResult.error || 'Failed to download docker-compose.yml');
|
||||
}
|
||||
|
||||
// Update the step to show success
|
||||
const dockerComposeStep = document.querySelectorAll('.step-item')[5];
|
||||
dockerComposeStep.classList.remove('active');
|
||||
dockerComposeStep.classList.add('completed');
|
||||
dockerComposeStep.querySelector('.step-status').textContent = 'Successfully downloaded docker-compose.yml';
|
||||
|
||||
// Add download button
|
||||
const downloadButton = document.createElement('button');
|
||||
downloadButton.className = 'btn btn-sm btn-primary mt-2';
|
||||
downloadButton.innerHTML = '<i class="fas fa-download me-1"></i> Download docker-compose.yml';
|
||||
downloadButton.onclick = () => {
|
||||
const blob = new Blob([dockerComposeResult.content], { type: 'text/yaml' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'docker-compose.yml';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
};
|
||||
dockerComposeStep.querySelector('.step-content').appendChild(downloadButton);
|
||||
|
||||
} 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 = `
|
||||
<div class="mt-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title mb-3">NGINX Proxy Results</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Property</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Proxy Host ID</td>
|
||||
<td>${result.id || 'N/A'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Domains</td>
|
||||
<td>${domains.join(', ')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Forward Scheme</td>
|
||||
<td>http</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Forward Host</td>
|
||||
<td>192.168.68.124</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Forward Port</td>
|
||||
<td>${parseInt(port)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SSL Status</td>
|
||||
<td>
|
||||
<span class="badge bg-success">Forced</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SSL Certificate</td>
|
||||
<td>
|
||||
<span class="badge bg-success">Using Certificate ID: ${sslCertificateId}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Security Features</td>
|
||||
<td>
|
||||
<span class="badge bg-success me-1">Block Exploits</span>
|
||||
<span class="badge bg-success me-1">HSTS</span>
|
||||
<span class="badge bg-success">HTTP/2</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Performance</td>
|
||||
<td>
|
||||
<span class="badge bg-success me-1">Caching</span>
|
||||
<span class="badge bg-success">WebSocket</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Update the proxy step to show success and add the results
|
||||
const proxyStep = document.querySelectorAll('.step-item')[3];
|
||||
proxyStep.classList.remove('active');
|
||||
proxyStep.classList.add('completed');
|
||||
const statusText = proxyStep.querySelector('.step-status');
|
||||
statusText.textContent = existingProxy ? 'Using existing proxy host' : 'Successfully created proxy host';
|
||||
statusText.after(document.createRange().createContextualFragment(successDetails));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error creating proxy host:', error);
|
||||
// Update status with error message
|
||||
updateStatus('proxy', `Failed: ${error.message}`, 'error');
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function generateSSLCertificate(domains) {
|
||||
try {
|
||||
// Get NGINX settings from the template
|
||||
const nginxSettings = {
|
||||
url: window.nginxSettings?.url || '',
|
||||
username: window.nginxSettings?.username || '',
|
||||
password: window.nginxSettings?.password || '',
|
||||
email: window.nginxSettings?.email || ''
|
||||
};
|
||||
|
||||
// Get a fresh token from NGINX
|
||||
const tokenResponse = await fetch(`${nginxSettings.url}/api/tokens`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
identity: nginxSettings.username,
|
||||
secret: nginxSettings.password
|
||||
})
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const errorText = await tokenResponse.text();
|
||||
console.error('Token Error Response:', errorText);
|
||||
throw new Error(`Failed to authenticate with NGINX: ${errorText}`);
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json();
|
||||
const token = tokenData.token;
|
||||
|
||||
if (!token) {
|
||||
throw new Error('No token received from NGINX Proxy Manager');
|
||||
}
|
||||
|
||||
// First, check if a certificate already exists for these domains
|
||||
const checkResponse = await fetch(`${nginxSettings.url}/api/nginx/certificates`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!checkResponse.ok) {
|
||||
throw new Error('Failed to check existing certificates');
|
||||
}
|
||||
|
||||
const existingCertificates = await checkResponse.json();
|
||||
const existingCertificate = existingCertificates.find(cert => {
|
||||
const certDomains = cert.domain_names || [];
|
||||
return domains.every(domain => certDomains.includes(domain)) &&
|
||||
certDomains.length === domains.length;
|
||||
});
|
||||
|
||||
let result;
|
||||
let usedExisting = false;
|
||||
if (existingCertificate) {
|
||||
console.log('Found existing certificate:', existingCertificate);
|
||||
result = existingCertificate;
|
||||
usedExisting = true;
|
||||
} else {
|
||||
// Create the SSL certificate directly with NGINX
|
||||
const requestBody = {
|
||||
domain_names: domains,
|
||||
meta: {
|
||||
letsencrypt_email: nginxSettings.email,
|
||||
letsencrypt_agree: true,
|
||||
dns_challenge: false
|
||||
},
|
||||
provider: 'letsencrypt'
|
||||
};
|
||||
console.log('Request Body:', requestBody);
|
||||
|
||||
const response = await fetch(`${nginxSettings.url}/api/nginx/certificates`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
console.log('Response Status:', response.status);
|
||||
console.log('Response Headers:', Object.fromEntries(response.headers.entries()));
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Certificate creation error:', errorText);
|
||||
throw new Error(`Failed to generate SSL certificate: ${errorText}`);
|
||||
}
|
||||
|
||||
result = await response.json();
|
||||
console.log('Certificate creation result:', result);
|
||||
}
|
||||
|
||||
// Update the SSL step to show success
|
||||
const sslStep = document.querySelectorAll('.step-item')[2];
|
||||
sslStep.classList.remove('active');
|
||||
sslStep.classList.add('completed');
|
||||
const sslStatusText = sslStep.querySelector('.step-status');
|
||||
sslStatusText.textContent = usedExisting ?
|
||||
'Using existing SSL certificate' :
|
||||
'SSL certificate generated successfully';
|
||||
|
||||
// Always add SSL certificate details
|
||||
const sslDetails = document.createElement('div');
|
||||
sslDetails.className = 'mt-3';
|
||||
sslDetails.innerHTML = `
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title mb-3">SSL Certificate Details</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Property</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Certificate ID</td>
|
||||
<td>${result.id || 'N/A'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Domains</td>
|
||||
<td>${(result.domain_names || domains).join(', ')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Provider</td>
|
||||
<td>${result.provider || 'Let\'s Encrypt'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
sslStatusText.after(sslDetails);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
certificate: result
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error generating SSL certificate:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToStep(stepElement) {
|
||||
const container = document.querySelector('.launch-steps-container');
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const elementRect = stepElement.getBoundingClientRect();
|
||||
|
||||
// Calculate the scroll position to center the element in the container
|
||||
const scrollTop = elementRect.top - containerRect.top - (containerRect.height / 2) + (elementRect.height / 2);
|
||||
|
||||
// Smooth scroll to the element
|
||||
container.scrollTo({
|
||||
top: container.scrollTop + scrollTop,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
function updateStep(stepNumber, title, description) {
|
||||
return new Promise((resolve) => {
|
||||
// Update the current step in the header
|
||||
document.getElementById('currentStep').textContent = title;
|
||||
document.getElementById('stepDescription').textContent = description;
|
||||
|
||||
// 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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user