1872 lines
72 KiB
Python
1872 lines
72 KiB
Python
from flask import jsonify, request, current_app, Blueprint
|
|
from models import (
|
|
KeyValueSettings,
|
|
Instance
|
|
)
|
|
from extensions import db, csrf
|
|
from routes.admin_api import token_required
|
|
import json
|
|
import smtplib
|
|
from email.mime.text import MIMEText
|
|
from email.mime.multipart import MIMEMultipart
|
|
from email.utils import formatdate
|
|
from datetime import datetime
|
|
import requests
|
|
import base64
|
|
from flask_wtf.csrf import CSRFProtect
|
|
from functools import wraps
|
|
import os
|
|
|
|
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:
|
|
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
|
|
|
|
# Get the current commit hash and latest tag for the branch
|
|
commit_hash = None
|
|
latest_tag = None
|
|
if git_settings['provider'] == 'gitea':
|
|
headers = {
|
|
'Accept': 'application/json',
|
|
'Authorization': f'token {git_settings["token"]}'
|
|
}
|
|
|
|
# Get the latest commit for the branch
|
|
commit_response = requests.get(
|
|
f'{git_settings["url"]}/api/v1/repos/{data["repository"]}/commits/{data["branch"]}',
|
|
headers=headers
|
|
)
|
|
|
|
if commit_response.status_code == 200:
|
|
commit_data = commit_response.json()
|
|
commit_hash = commit_data.get('sha')
|
|
else:
|
|
# Try token as query parameter if header auth fails
|
|
commit_response = requests.get(
|
|
f'{git_settings["url"]}/api/v1/repos/{data["repository"]}/commits/{data["branch"]}?token={git_settings["token"]}',
|
|
headers={'Accept': 'application/json'}
|
|
)
|
|
if commit_response.status_code == 200:
|
|
commit_data = commit_response.json()
|
|
commit_hash = commit_data.get('sha')
|
|
|
|
# Get the latest tag
|
|
tags_response = requests.get(
|
|
f'{git_settings["url"]}/api/v1/repos/{data["repository"]}/tags',
|
|
headers=headers
|
|
)
|
|
|
|
if tags_response.status_code == 200:
|
|
tags_data = tags_response.json()
|
|
if tags_data:
|
|
# Sort tags by commit date (newest first) and get the latest
|
|
sorted_tags = sorted(tags_data, key=lambda x: x.get('commit', {}).get('created', ''), reverse=True)
|
|
if sorted_tags:
|
|
latest_tag = sorted_tags[0].get('name')
|
|
else:
|
|
# Try token as query parameter if header auth fails
|
|
tags_response = requests.get(
|
|
f'{git_settings["url"]}/api/v1/repos/{data["repository"]}/tags?token={git_settings["token"]}',
|
|
headers={'Accept': 'application/json'}
|
|
)
|
|
if tags_response.status_code == 200:
|
|
tags_data = tags_response.json()
|
|
if tags_data:
|
|
sorted_tags = sorted(tags_data, key=lambda x: x.get('commit', {}).get('created', ''), reverse=True)
|
|
if sorted_tags:
|
|
latest_tag = sorted_tags[0].get('name')
|
|
|
|
# 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,
|
|
'commit_hash': commit_hash,
|
|
'latest_tag': latest_tag
|
|
})
|
|
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
|
|
|
|
@launch_api.route('/deploy-stack', methods=['POST'])
|
|
@csrf.exempt
|
|
def deploy_stack():
|
|
try:
|
|
data = request.get_json()
|
|
if not data or 'name' not in data or 'StackFileContent' not in data:
|
|
return jsonify({'error': 'Missing required fields'}), 400
|
|
|
|
# Get Portainer settings
|
|
portainer_settings = KeyValueSettings.get_value('portainer_settings')
|
|
if not portainer_settings:
|
|
return jsonify({'error': 'Portainer settings not configured'}), 400
|
|
|
|
# Define timeout early to ensure it's available throughout the function
|
|
stack_timeout = current_app.config.get('STACK_DEPLOYMENT_TIMEOUT', 300) # Default to 5 minutes
|
|
|
|
# Verify Portainer authentication
|
|
auth_response = requests.get(
|
|
f"{portainer_settings['url'].rstrip('/')}/api/status",
|
|
headers={
|
|
'X-API-Key': portainer_settings['api_key'],
|
|
'Accept': 'application/json'
|
|
},
|
|
timeout=30 # 30 seconds timeout for status check
|
|
)
|
|
|
|
if not auth_response.ok:
|
|
current_app.logger.error(f"Portainer authentication failed: {auth_response.text}")
|
|
return jsonify({'error': 'Failed to authenticate with Portainer'}), 401
|
|
|
|
# Get Portainer endpoint ID (assuming it's the first endpoint)
|
|
endpoint_response = requests.get(
|
|
f"{portainer_settings['url'].rstrip('/')}/api/endpoints",
|
|
headers={
|
|
'X-API-Key': portainer_settings['api_key'],
|
|
'Accept': 'application/json'
|
|
},
|
|
timeout=30 # 30 seconds timeout for endpoint check
|
|
)
|
|
if not endpoint_response.ok:
|
|
error_text = endpoint_response.text
|
|
try:
|
|
error_json = endpoint_response.json()
|
|
error_text = error_json.get('message', error_text)
|
|
except:
|
|
pass
|
|
return jsonify({'error': f'Failed to get Portainer endpoints: {error_text}'}), 500
|
|
|
|
endpoints = endpoint_response.json()
|
|
if not endpoints:
|
|
return jsonify({'error': 'No Portainer endpoints found'}), 400
|
|
|
|
endpoint_id = endpoints[0]['Id']
|
|
|
|
# Log the request data
|
|
current_app.logger.info(f"Creating stack with data: {json.dumps(data)}")
|
|
current_app.logger.info(f"Using endpoint ID: {endpoint_id}")
|
|
current_app.logger.info(f"Using timeout: {stack_timeout} seconds")
|
|
|
|
# First, check if a stack with this name already exists
|
|
stacks_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks"
|
|
stacks_response = requests.get(
|
|
stacks_url,
|
|
headers={
|
|
'X-API-Key': portainer_settings['api_key'],
|
|
'Accept': 'application/json'
|
|
},
|
|
params={'filters': json.dumps({'Name': data['name']})},
|
|
timeout=30
|
|
)
|
|
|
|
if stacks_response.ok:
|
|
existing_stacks = stacks_response.json()
|
|
for stack in existing_stacks:
|
|
if stack['Name'] == data['name']:
|
|
current_app.logger.info(f"Found existing stack: {stack['Name']} (ID: {stack['Id']})")
|
|
return jsonify({
|
|
'success': True,
|
|
'data': {
|
|
'name': stack['Name'],
|
|
'id': stack['Id'],
|
|
'status': 'existing'
|
|
}
|
|
})
|
|
|
|
# If no existing stack found, proceed with creation
|
|
url = f"{portainer_settings['url'].rstrip('/')}/api/stacks/create/standalone/string"
|
|
current_app.logger.info(f"Making request to: {url}")
|
|
|
|
# Prepare the request body according to Portainer's API spec
|
|
request_body = data
|
|
|
|
# Add endpointId as a query parameter
|
|
params = {'endpointId': endpoint_id}
|
|
|
|
# Use a configurable timeout for stack creation initiation
|
|
create_response = requests.post(
|
|
url,
|
|
headers={
|
|
'X-API-Key': portainer_settings['api_key'],
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json'
|
|
},
|
|
params=params,
|
|
json=request_body,
|
|
timeout=stack_timeout # Use configurable timeout
|
|
)
|
|
|
|
# Log the response details
|
|
current_app.logger.info(f"Response status: {create_response.status_code}")
|
|
current_app.logger.info(f"Response headers: {dict(create_response.headers)}")
|
|
|
|
response_text = create_response.text
|
|
current_app.logger.info(f"Response body: {response_text}")
|
|
|
|
if not create_response.ok:
|
|
error_message = response_text
|
|
try:
|
|
error_json = create_response.json()
|
|
error_message = error_json.get('message', error_message)
|
|
except:
|
|
pass
|
|
return jsonify({'error': f'Failed to create stack: {error_message}'}), 500
|
|
|
|
stack_info = create_response.json()
|
|
current_app.logger.info(f"Stack creation initiated: {stack_info['Name']} (ID: {stack_info['Id']})")
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'data': {
|
|
'name': stack_info['Name'],
|
|
'id': stack_info['Id'],
|
|
'status': 'creating'
|
|
}
|
|
})
|
|
|
|
except requests.exceptions.Timeout:
|
|
current_app.logger.error(f"Request timed out after {stack_timeout} seconds while initiating stack deployment")
|
|
current_app.logger.error(f"Stack name: {data.get('name', 'unknown') if 'data' in locals() else 'unknown'}")
|
|
current_app.logger.error(f"Portainer URL: {portainer_settings.get('url', 'unknown') if 'portainer_settings' in locals() else 'unknown'}")
|
|
return jsonify({
|
|
'error': f'Request timed out after {stack_timeout} seconds while initiating stack deployment. The operation may still be in progress.',
|
|
'timeout_seconds': stack_timeout,
|
|
'stack_name': data.get('name', 'unknown') if 'data' in locals() else 'unknown'
|
|
}), 504
|
|
except Exception as e:
|
|
current_app.logger.error(f"Error deploying stack: {str(e)}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@launch_api.route('/check-stack-status', methods=['POST'])
|
|
@csrf.exempt
|
|
def check_stack_status():
|
|
try:
|
|
data = request.get_json()
|
|
if not data or 'stack_name' not in data:
|
|
return jsonify({'error': 'Missing stack_name field'}), 400
|
|
|
|
# Get Portainer settings
|
|
portainer_settings = KeyValueSettings.get_value('portainer_settings')
|
|
if not portainer_settings:
|
|
return jsonify({'error': 'Portainer settings not configured'}), 400
|
|
|
|
# Get Portainer endpoint ID (assuming it's the first endpoint)
|
|
endpoint_response = requests.get(
|
|
f"{portainer_settings['url'].rstrip('/')}/api/endpoints",
|
|
headers={
|
|
'X-API-Key': portainer_settings['api_key'],
|
|
'Accept': 'application/json'
|
|
},
|
|
timeout=30
|
|
)
|
|
|
|
if not endpoint_response.ok:
|
|
return jsonify({'error': 'Failed to get Portainer endpoints'}), 500
|
|
|
|
endpoints = endpoint_response.json()
|
|
if not endpoints:
|
|
return jsonify({'error': 'No Portainer endpoints found'}), 400
|
|
|
|
endpoint_id = endpoints[0]['Id']
|
|
|
|
# Get stack information
|
|
stacks_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks"
|
|
stacks_response = requests.get(
|
|
stacks_url,
|
|
headers={
|
|
'X-API-Key': portainer_settings['api_key'],
|
|
'Accept': 'application/json'
|
|
},
|
|
params={'filters': json.dumps({'Name': data['stack_name']})},
|
|
timeout=30
|
|
)
|
|
|
|
if not stacks_response.ok:
|
|
return jsonify({'error': 'Failed to get stack information'}), 500
|
|
|
|
stacks = stacks_response.json()
|
|
target_stack = None
|
|
|
|
for stack in stacks:
|
|
if stack['Name'] == data['stack_name']:
|
|
target_stack = stack
|
|
break
|
|
|
|
if not target_stack:
|
|
return jsonify({'error': f'Stack {data["stack_name"]} not found'}), 404
|
|
|
|
# Get stack services to check their status
|
|
services_url = f"{portainer_settings['url'].rstrip('/')}/api/endpoints/{endpoint_id}/docker/services"
|
|
current_app.logger.info(f"Checking services for stack {data['stack_name']} at endpoint {endpoint_id}")
|
|
|
|
try:
|
|
services_response = requests.get(
|
|
services_url,
|
|
headers={
|
|
'X-API-Key': portainer_settings['api_key'],
|
|
'Accept': 'application/json'
|
|
},
|
|
params={'filters': json.dumps({'label': f'com.docker.stack.namespace={data["stack_name"]}'})},
|
|
timeout=30
|
|
)
|
|
|
|
current_app.logger.info(f"Services API response status: {services_response.status_code}")
|
|
|
|
if services_response.ok:
|
|
services = services_response.json()
|
|
current_app.logger.info(f"Found {len(services)} services for stack {data['stack_name']}")
|
|
|
|
# Check if all services are running
|
|
all_running = True
|
|
service_statuses = []
|
|
|
|
for service in services:
|
|
replicas_running = service.get('Spec', {}).get('Mode', {}).get('Replicated', {}).get('Replicas', 0)
|
|
replicas_actual = service.get('ServiceStatus', {}).get('RunningTasks', 0)
|
|
|
|
service_status = {
|
|
'name': service.get('Spec', {}).get('Name', 'Unknown'),
|
|
'replicas_expected': replicas_running,
|
|
'replicas_running': replicas_actual,
|
|
'status': 'running' if replicas_actual >= replicas_running else 'not_running'
|
|
}
|
|
|
|
service_statuses.append(service_status)
|
|
|
|
if replicas_actual < replicas_running:
|
|
all_running = False
|
|
|
|
# Determine overall stack status
|
|
if all_running and len(services) > 0:
|
|
status = 'active'
|
|
elif len(services) > 0:
|
|
status = 'partial'
|
|
else:
|
|
status = 'inactive'
|
|
else:
|
|
# Services API failed, but stack exists - assume it's still starting up
|
|
current_app.logger.warning(f"Failed to get services for stack {data['stack_name']}: {services_response.status_code} - {services_response.text}")
|
|
|
|
# Provide more specific error context
|
|
if services_response.status_code == 404:
|
|
current_app.logger.info(f"Services endpoint not found for stack {data['stack_name']} - stack may still be initializing")
|
|
elif services_response.status_code == 403:
|
|
current_app.logger.warning(f"Access denied to services for stack {data['stack_name']} - check Portainer permissions")
|
|
elif services_response.status_code >= 500:
|
|
current_app.logger.warning(f"Portainer server error when getting services for stack {data['stack_name']}")
|
|
|
|
services = []
|
|
service_statuses = []
|
|
status = 'starting' # Stack exists but services not available yet
|
|
|
|
except Exception as e:
|
|
# Exception occurred while getting services, but stack exists
|
|
current_app.logger.warning(f"Exception getting services for stack {data['stack_name']}: {str(e)}")
|
|
services = []
|
|
service_statuses = []
|
|
status = 'starting' # Stack exists but services not available yet
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'data': {
|
|
'stack_name': data['stack_name'],
|
|
'stack_id': target_stack['Id'],
|
|
'status': status,
|
|
'services': service_statuses,
|
|
'total_services': len(services),
|
|
'running_services': len([s for s in service_statuses if s['status'] == 'running']),
|
|
'stack_created_at': target_stack.get('CreatedAt', 'unknown'),
|
|
'stack_updated_at': target_stack.get('UpdatedAt', 'unknown')
|
|
}
|
|
})
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Error checking stack status: {str(e)}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@launch_api.route('/save-instance', methods=['POST'])
|
|
@csrf.exempt
|
|
def save_instance():
|
|
try:
|
|
if not request.is_json:
|
|
return jsonify({'error': 'Request must be JSON'}), 400
|
|
|
|
data = request.get_json()
|
|
required_fields = ['name', 'port', 'domains', 'stack_id', 'stack_name', 'status', 'repository', 'branch']
|
|
|
|
if not all(field in data for field in required_fields):
|
|
missing_fields = [field for field in required_fields if field not in data]
|
|
return jsonify({'error': f'Missing required fields: {", ".join(missing_fields)}'}), 400
|
|
|
|
# Check if instance already exists
|
|
existing_instance = Instance.query.filter_by(name=data['name']).first()
|
|
|
|
if existing_instance:
|
|
# Update existing instance
|
|
existing_instance.port = data['port']
|
|
existing_instance.domains = data['domains']
|
|
existing_instance.stack_id = data['stack_id']
|
|
existing_instance.stack_name = data['stack_name']
|
|
existing_instance.status = data['status']
|
|
existing_instance.repository = data['repository']
|
|
existing_instance.branch = data['branch']
|
|
existing_instance.deployed_version = data.get('deployed_version', 'unknown')
|
|
existing_instance.deployed_branch = data.get('deployed_branch', data['branch'])
|
|
existing_instance.version_checked_at = datetime.utcnow()
|
|
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'message': 'Instance data updated successfully',
|
|
'data': {
|
|
'name': existing_instance.name,
|
|
'port': existing_instance.port,
|
|
'domains': existing_instance.domains,
|
|
'stack_id': existing_instance.stack_id,
|
|
'stack_name': existing_instance.stack_name,
|
|
'status': existing_instance.status,
|
|
'repository': existing_instance.repository,
|
|
'branch': existing_instance.branch,
|
|
'deployed_version': existing_instance.deployed_version,
|
|
'deployed_branch': existing_instance.deployed_branch
|
|
}
|
|
})
|
|
else:
|
|
# Create new instance
|
|
instance = Instance(
|
|
name=data['name'],
|
|
company='Loading...', # Will be updated later
|
|
rooms_count=0,
|
|
conversations_count=0,
|
|
data_size=0.0,
|
|
payment_plan='Basic',
|
|
main_url=f"https://{data['domains'][0]}" if data['domains'] else f"http://localhost:{data['port']}",
|
|
status=data['status'],
|
|
port=data['port'],
|
|
stack_id=data['stack_id'],
|
|
stack_name=data['stack_name'],
|
|
repository=data['repository'],
|
|
branch=data['branch'],
|
|
deployed_version=data.get('deployed_version', 'unknown'),
|
|
deployed_branch=data.get('deployed_branch', data['branch'])
|
|
)
|
|
|
|
db.session.add(instance)
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'message': 'Instance data saved successfully',
|
|
'data': {
|
|
'name': instance.name,
|
|
'port': instance.port,
|
|
'domains': instance.domains,
|
|
'stack_id': instance.stack_id,
|
|
'stack_name': instance.stack_name,
|
|
'status': instance.status,
|
|
'repository': instance.repository,
|
|
'branch': instance.branch,
|
|
'deployed_version': instance.deployed_version,
|
|
'deployed_branch': instance.deployed_branch
|
|
}
|
|
})
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Error saving instance data: {str(e)}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@launch_api.route('/apply-company-information', methods=['POST'])
|
|
@csrf.exempt
|
|
def apply_company_information():
|
|
"""Apply company information to a launched instance"""
|
|
try:
|
|
if not request.is_json:
|
|
return jsonify({'error': 'Request must be JSON'}), 400
|
|
|
|
data = request.get_json()
|
|
required_fields = ['instance_url', 'company_data']
|
|
|
|
if not all(field in data for field in required_fields):
|
|
missing_fields = [field for field in required_fields if field not in data]
|
|
return jsonify({'error': f'Missing required fields: {", ".join(missing_fields)}'}), 400
|
|
|
|
instance_url = data['instance_url']
|
|
company_data = data['company_data']
|
|
|
|
# Get the instance from database to get the connection token
|
|
instance = Instance.query.filter_by(main_url=instance_url).first()
|
|
|
|
if not instance:
|
|
return jsonify({'error': 'Instance not found in database'}), 404
|
|
|
|
if not instance.connection_token:
|
|
return jsonify({'error': 'Instance not authenticated'}), 400
|
|
|
|
# First get JWT token from the instance
|
|
jwt_response = requests.post(
|
|
f"{instance_url.rstrip('/')}/api/admin/management-token",
|
|
headers={
|
|
'X-API-Key': instance.connection_token,
|
|
'Accept': 'application/json'
|
|
},
|
|
timeout=10
|
|
)
|
|
|
|
if jwt_response.status_code != 200:
|
|
return jsonify({'error': f'Failed to get JWT token: {jwt_response.text}'}), 400
|
|
|
|
jwt_data = jwt_response.json()
|
|
jwt_token = jwt_data.get('token')
|
|
|
|
if not jwt_token:
|
|
return jsonify({'error': 'No JWT token received'}), 400
|
|
|
|
# Prepare company data for the API
|
|
api_company_data = {
|
|
'company_name': company_data.get('name'),
|
|
'company_industry': company_data.get('industry'),
|
|
'company_email': company_data.get('email'),
|
|
'company_website': company_data.get('website'),
|
|
'company_address': company_data.get('streetAddress'),
|
|
'company_city': company_data.get('city'),
|
|
'company_state': company_data.get('state'),
|
|
'company_zip': company_data.get('zipCode'),
|
|
'company_country': company_data.get('country'),
|
|
'company_description': company_data.get('description'),
|
|
'company_phone': company_data.get('phone')
|
|
}
|
|
|
|
# Apply company information to the instance
|
|
company_response = requests.put(
|
|
f"{instance_url.rstrip('/')}/api/admin/settings",
|
|
headers={
|
|
'Authorization': f'Bearer {jwt_token}',
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json'
|
|
},
|
|
json=api_company_data,
|
|
timeout=10
|
|
)
|
|
|
|
if company_response.status_code != 200:
|
|
return jsonify({'error': f'Failed to apply company information: {company_response.text}'}), 400
|
|
|
|
# Update the instance company name in our database
|
|
instance.company = company_data.get('name', 'Unknown')
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'message': 'Company information applied successfully',
|
|
'data': {
|
|
'company_name': company_data.get('name'),
|
|
'instance_url': instance_url
|
|
}
|
|
})
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Error applying company information: {str(e)}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@launch_api.route('/apply-colors', methods=['POST'])
|
|
@csrf.exempt
|
|
def apply_colors():
|
|
"""Apply colors to a launched instance"""
|
|
try:
|
|
if not request.is_json:
|
|
return jsonify({'error': 'Request must be JSON'}), 400
|
|
|
|
data = request.get_json()
|
|
required_fields = ['instance_url', 'colors_data']
|
|
|
|
if not all(field in data for field in required_fields):
|
|
missing_fields = [field for field in required_fields if field not in data]
|
|
return jsonify({'error': f'Missing required fields: {", ".join(missing_fields)}'}), 400
|
|
|
|
instance_url = data['instance_url']
|
|
colors_data = data['colors_data']
|
|
|
|
# Get the instance from database to get the connection token
|
|
instance = Instance.query.filter_by(main_url=instance_url).first()
|
|
|
|
if not instance:
|
|
return jsonify({'error': 'Instance not found in database'}), 404
|
|
|
|
if not instance.connection_token:
|
|
return jsonify({'error': 'Instance not authenticated'}), 400
|
|
|
|
# First get JWT token from the instance
|
|
jwt_response = requests.post(
|
|
f"{instance_url.rstrip('/')}/api/admin/management-token",
|
|
headers={
|
|
'X-API-Key': instance.connection_token,
|
|
'Accept': 'application/json'
|
|
},
|
|
timeout=10
|
|
)
|
|
|
|
if jwt_response.status_code != 200:
|
|
return jsonify({'error': f'Failed to get JWT token: {jwt_response.text}'}), 400
|
|
|
|
jwt_data = jwt_response.json()
|
|
jwt_token = jwt_data.get('token')
|
|
|
|
if not jwt_token:
|
|
return jsonify({'error': 'No JWT token received'}), 400
|
|
|
|
# Prepare colors data for the API
|
|
api_colors_data = {
|
|
'primary_color': colors_data.get('primary'),
|
|
'secondary_color': colors_data.get('secondary')
|
|
}
|
|
|
|
# Apply colors to the instance
|
|
colors_response = requests.put(
|
|
f"{instance_url.rstrip('/')}/api/admin/settings",
|
|
headers={
|
|
'Authorization': f'Bearer {jwt_token}',
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json'
|
|
},
|
|
json=api_colors_data,
|
|
timeout=10
|
|
)
|
|
|
|
if colors_response.status_code != 200:
|
|
return jsonify({'error': f'Failed to apply colors: {colors_response.text}'}), 400
|
|
|
|
return jsonify({
|
|
'message': 'Colors applied successfully',
|
|
'data': {
|
|
'primary_color': colors_data.get('primary'),
|
|
'secondary_color': colors_data.get('secondary'),
|
|
'instance_url': instance_url
|
|
}
|
|
})
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Error applying colors: {str(e)}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@launch_api.route('/update-admin-credentials', methods=['POST'])
|
|
@csrf.exempt
|
|
def update_admin_credentials():
|
|
"""Update admin credentials on a launched instance"""
|
|
try:
|
|
if not request.is_json:
|
|
return jsonify({'error': 'Request must be JSON'}), 400
|
|
|
|
data = request.get_json()
|
|
required_fields = ['instance_url', 'email']
|
|
|
|
if not all(field in data for field in required_fields):
|
|
missing_fields = [field for field in required_fields if field not in data]
|
|
return jsonify({'error': f'Missing required fields: {", ".join(missing_fields)}'}), 400
|
|
|
|
instance_url = data['instance_url']
|
|
email = data['email']
|
|
|
|
# Get the instance from database to get the connection token
|
|
instance = Instance.query.filter_by(main_url=instance_url).first()
|
|
|
|
if not instance:
|
|
return jsonify({'error': 'Instance not found in database'}), 404
|
|
|
|
if not instance.connection_token:
|
|
return jsonify({'error': 'Instance not authenticated'}), 400
|
|
|
|
# First get JWT token from the instance
|
|
jwt_response = requests.post(
|
|
f"{instance_url.rstrip('/')}/api/admin/management-token",
|
|
headers={
|
|
'X-API-Key': instance.connection_token,
|
|
'Accept': 'application/json'
|
|
},
|
|
timeout=10
|
|
)
|
|
|
|
if jwt_response.status_code != 200:
|
|
return jsonify({'error': f'Failed to get JWT token: {jwt_response.text}'}), 400
|
|
|
|
jwt_data = jwt_response.json()
|
|
jwt_token = jwt_data.get('token')
|
|
|
|
if not jwt_token:
|
|
return jsonify({'error': 'No JWT token received'}), 400
|
|
|
|
# Get the admin user ID first
|
|
users_response = requests.get(
|
|
f"{instance_url.rstrip('/')}/api/admin/contacts",
|
|
headers={
|
|
'Authorization': f'Bearer {jwt_token}',
|
|
'Accept': 'application/json'
|
|
},
|
|
timeout=10
|
|
)
|
|
|
|
if users_response.status_code != 200:
|
|
return jsonify({'error': f'Failed to get users: {users_response.text}'}), 400
|
|
|
|
users_data = users_response.json()
|
|
admin_user = None
|
|
|
|
# Find the administrator user by role (since email was already updated)
|
|
for user in users_data:
|
|
if user.get('is_admin') == True:
|
|
admin_user = user
|
|
break
|
|
|
|
if not admin_user:
|
|
return jsonify({'error': 'Administrator user not found'}), 404
|
|
|
|
admin_user_id = admin_user.get('id')
|
|
|
|
# Try to login with default credentials first
|
|
try:
|
|
login_response = requests.post(
|
|
f"{instance_url.rstrip('/')}/api/admin/login",
|
|
headers={
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json'
|
|
},
|
|
json={
|
|
'email': 'administrator@docupulse.com',
|
|
'password': 'changeme'
|
|
},
|
|
timeout=10
|
|
)
|
|
|
|
# If login with default credentials succeeds, update the credentials
|
|
if login_response.status_code == 200:
|
|
login_data = login_response.json()
|
|
if login_data.get('status') == 'success' and login_data.get('token'):
|
|
admin_token = login_data.get('token')
|
|
|
|
# Generate a secure password
|
|
import secrets
|
|
import string
|
|
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
|
|
new_password = ''.join(secrets.choice(alphabet) for i in range(16))
|
|
|
|
# Update the admin user with new email and password
|
|
update_response = requests.put(
|
|
f"{instance_url.rstrip('/')}/api/admin/contacts/{admin_user_id}",
|
|
headers={
|
|
'Authorization': f'Bearer {admin_token}',
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json'
|
|
},
|
|
json={
|
|
'email': email,
|
|
'password': new_password,
|
|
'username': 'administrator',
|
|
'last_name': 'Administrator',
|
|
'role': 'admin'
|
|
},
|
|
timeout=10
|
|
)
|
|
|
|
if update_response.status_code != 200:
|
|
return jsonify({'error': f'Failed to update admin credentials: {update_response.text}'}), 400
|
|
|
|
return jsonify({
|
|
'message': 'Admin credentials updated successfully',
|
|
'data': {
|
|
'email': email,
|
|
'password': new_password,
|
|
'username': 'administrator',
|
|
'instance_url': instance_url
|
|
}
|
|
})
|
|
|
|
# If login with default credentials fails, check if email is already updated
|
|
else:
|
|
# Check if the email is already the target email
|
|
if admin_user.get('email') == email:
|
|
return jsonify({
|
|
'message': 'Admin credentials already updated',
|
|
'data': {
|
|
'email': email,
|
|
'password': 'Already set',
|
|
'username': 'administrator',
|
|
'instance_url': instance_url,
|
|
'already_updated': True
|
|
}
|
|
})
|
|
else:
|
|
return jsonify({'error': 'Failed to login with default credentials and email not yet updated'}), 400
|
|
|
|
except Exception as login_error:
|
|
# If there's an error with login, check if email is already updated
|
|
if admin_user.get('email') == email:
|
|
return jsonify({
|
|
'message': 'Admin credentials already updated',
|
|
'data': {
|
|
'email': email,
|
|
'password': 'Already set',
|
|
'username': 'administrator',
|
|
'instance_url': instance_url,
|
|
'already_updated': True
|
|
}
|
|
})
|
|
else:
|
|
return jsonify({'error': f'Login error and email not yet updated: {str(login_error)}'}), 400
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Error updating admin credentials: {str(e)}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@launch_api.route('/send-completion-email', methods=['POST'])
|
|
@csrf.exempt
|
|
def send_completion_email():
|
|
"""Send completion email to client with instance details and password reset link"""
|
|
try:
|
|
if not request.is_json:
|
|
return jsonify({'error': 'Request must be JSON'}), 400
|
|
|
|
data = request.get_json()
|
|
required_fields = ['instance_url', 'company_data', 'credentials_data']
|
|
|
|
if not all(field in data for field in required_fields):
|
|
missing_fields = [field for field in required_fields if field not in data]
|
|
return jsonify({'error': f'Missing required fields: {", ".join(missing_fields)}'}), 400
|
|
|
|
instance_url = data['instance_url']
|
|
company_data = data['company_data']
|
|
credentials_data = data['credentials_data']
|
|
|
|
# Get SMTP settings from master instance
|
|
smtp_settings = KeyValueSettings.get_value('smtp_settings')
|
|
if not smtp_settings:
|
|
return jsonify({'error': 'SMTP settings not configured'}), 400
|
|
|
|
# Get the instance from database to get the connection token
|
|
instance = Instance.query.filter_by(main_url=instance_url).first()
|
|
|
|
if not instance:
|
|
return jsonify({'error': 'Instance not found in database'}), 404
|
|
|
|
if not instance.connection_token:
|
|
return jsonify({'error': 'Instance not authenticated'}), 400
|
|
|
|
# Get JWT token from the launched instance using management API key
|
|
jwt_response = requests.post(
|
|
f"{instance_url.rstrip('/')}/api/admin/management-token",
|
|
headers={
|
|
'X-API-Key': instance.connection_token,
|
|
'Accept': 'application/json'
|
|
},
|
|
timeout=10
|
|
)
|
|
|
|
if jwt_response.status_code != 200:
|
|
return jsonify({'error': f'Failed to get JWT token: {jwt_response.text}'}), 400
|
|
|
|
jwt_data = jwt_response.json()
|
|
jwt_token = jwt_data.get('token')
|
|
|
|
if not jwt_token:
|
|
return jsonify({'error': 'No JWT token received'}), 400
|
|
|
|
# Get the admin user ID from the launched instance
|
|
users_response = requests.get(
|
|
f"{instance_url.rstrip('/')}/api/admin/contacts",
|
|
headers={
|
|
'Authorization': f'Bearer {jwt_token}',
|
|
'Accept': 'application/json'
|
|
},
|
|
timeout=10
|
|
)
|
|
|
|
if users_response.status_code != 200:
|
|
return jsonify({'error': f'Failed to get users: {users_response.text}'}), 400
|
|
|
|
users_data = users_response.json()
|
|
admin_user = None
|
|
|
|
# Find the administrator user by role (since email was already updated)
|
|
for user in users_data:
|
|
if user.get('is_admin') == True:
|
|
admin_user = user
|
|
break
|
|
|
|
if not admin_user:
|
|
return jsonify({'error': 'Administrator user not found'}), 404
|
|
|
|
admin_user_id = admin_user.get('id')
|
|
|
|
# Generate password reset token for the launched instance using management API
|
|
reset_response = requests.post(
|
|
f"{instance_url.rstrip('/')}/api/admin/generate-password-reset/{admin_user_id}",
|
|
headers={
|
|
'Authorization': f'Bearer {jwt_token}',
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
json={'instance_url': instance_url},
|
|
timeout=10
|
|
)
|
|
|
|
if reset_response.status_code != 200:
|
|
return jsonify({'error': f'Failed to generate password reset: {reset_response.text}'}), 400
|
|
|
|
reset_data = reset_response.json()
|
|
reset_url = reset_data.get('reset_url')
|
|
expires_at = reset_data.get('expires_at')
|
|
|
|
if not reset_url:
|
|
return jsonify({'error': 'No reset URL received from instance'}), 400
|
|
|
|
# Create email content
|
|
subject = "Your DocuPulse Instance is Ready!"
|
|
|
|
# Build HTML email content
|
|
html_content = f"""
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<style>
|
|
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
|
|
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
|
|
.header {{ background-color: #16767b; color: white; padding: 20px; text-align: center; border-radius: 5px 5px 0 0; }}
|
|
.content {{ background-color: #f9f9f9; padding: 20px; border-radius: 0 0 5px 5px; }}
|
|
.credentials {{ background-color: #e8f5e9; padding: 15px; border-radius: 5px; margin: 15px 0; }}
|
|
.button {{ display: inline-block; background-color: #16767b; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; margin: 10px 0; }}
|
|
.footer {{ margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd; font-size: 12px; color: #666; }}
|
|
.info-box {{ background-color: #e3f2fd; padding: 15px; border-radius: 5px; margin: 15px 0; }}
|
|
.security-notice {{ background-color: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 5px; margin: 15px 0; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>🎉 Your DocuPulse Instance is Ready!</h1>
|
|
</div>
|
|
<div class="content">
|
|
<p>Dear {company_data.get('name', 'Valued Customer')},</p>
|
|
|
|
<p>Great news! Your DocuPulse instance has been successfully deployed and configured.
|
|
You can now access your secure document management platform.</p>
|
|
|
|
<div class="info-box">
|
|
<h3>📋 Instance Details</h3>
|
|
<p><strong>Instance URL:</strong> <a href="{instance_url}">{instance_url}</a></p>
|
|
<p><strong>Company Name:</strong> {company_data.get('name', 'Not set')}</p>
|
|
<p><strong>Industry:</strong> {company_data.get('industry', 'Not set')}</p>
|
|
<p><strong>Deployment Date:</strong> {datetime.utcnow().strftime('%B %d, %Y at %I:%M %p UTC')}</p>
|
|
</div>
|
|
|
|
<div class="credentials">
|
|
<h3>🔐 Account Access</h3>
|
|
<p><strong>Email Address:</strong> {admin_user.get('email', 'Not set')}</p>
|
|
<p><strong>Username:</strong> {admin_user.get('username', 'administrator')}</p>
|
|
|
|
<div class="security-notice">
|
|
<h4>🔒 Security Setup Required</h4>
|
|
<p>For your security, you need to set up your password. Click the button below to create your secure password.</p>
|
|
<p><strong>Password Reset Link Expires:</strong> {expires_at}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="text-align: center; margin: 30px 0;">
|
|
<a href="{reset_url}" class="button">🔐 Set Up Your Password</a>
|
|
<br><br>
|
|
<a href="{instance_url}" class="button">🚀 Access Your Instance</a>
|
|
</div>
|
|
|
|
<h3>✅ What's Been Configured</h3>
|
|
<ul>
|
|
<li>✅ Secure SSL certificate for HTTPS access</li>
|
|
<li>✅ Company information and branding</li>
|
|
<li>✅ Custom color scheme</li>
|
|
<li>✅ Admin account created</li>
|
|
<li>✅ Document management system ready</li>
|
|
</ul>
|
|
|
|
<h3>🎯 Next Steps</h3>
|
|
<ol>
|
|
<li>Click the "Set Up Your Password" button above</li>
|
|
<li>Create your secure password</li>
|
|
<li>Return to your instance and log in</li>
|
|
<li>Explore your new DocuPulse platform</li>
|
|
<li>Start uploading and organizing your documents</li>
|
|
<li>Invite team members to collaborate</li>
|
|
</ol>
|
|
|
|
<div class="footer">
|
|
<p>If you have any questions or need assistance, please don't hesitate to contact our support team.</p>
|
|
<p>Thank you for choosing DocuPulse!</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
# Build plain text version
|
|
text_content = f"""
|
|
Your DocuPulse Instance is Ready!
|
|
|
|
Dear {company_data.get('name', 'Valued Customer')},
|
|
|
|
Great news! Your DocuPulse instance has been successfully deployed and configured.
|
|
|
|
INSTANCE DETAILS:
|
|
- Instance URL: {instance_url}
|
|
- Company Name: {company_data.get('name', 'Not set')}
|
|
- Industry: {company_data.get('industry', 'Not set')}
|
|
- Deployment Date: {datetime.utcnow().strftime('%B %d, %Y at %I:%M %p UTC')}
|
|
|
|
ACCOUNT ACCESS:
|
|
- Email Address: {admin_user.get('email', 'Not set')}
|
|
- Username: {admin_user.get('username', 'administrator')}
|
|
|
|
SECURITY SETUP REQUIRED:
|
|
For your security, you need to set up your password.
|
|
|
|
Password Reset Link: {reset_url}
|
|
Password Reset Link Expires: {expires_at}
|
|
|
|
WHAT'S BEEN CONFIGURED:
|
|
✓ Secure SSL certificate for HTTPS access
|
|
✓ Company information and branding
|
|
✓ Custom color scheme
|
|
✓ Admin account created
|
|
✓ Document management system ready
|
|
|
|
NEXT STEPS:
|
|
1. Click the password reset link above
|
|
2. Create your secure password
|
|
3. Return to your instance and log in
|
|
4. Explore your new DocuPulse platform
|
|
5. Start uploading and organizing your documents
|
|
6. Invite team members to collaborate
|
|
|
|
If you have any questions or need assistance, please don't hesitate to contact our support team.
|
|
|
|
Thank you for choosing DocuPulse!
|
|
"""
|
|
|
|
# Send email using master instance's email system
|
|
try:
|
|
# Get SMTP settings
|
|
smtp_settings = KeyValueSettings.get_value('smtp_settings')
|
|
if not smtp_settings:
|
|
return jsonify({'error': 'SMTP settings not configured'}), 400
|
|
|
|
# Create message
|
|
msg = MIMEMultipart()
|
|
msg['From'] = f"{smtp_settings.get('smtp_from_name', 'DocuPulse')} <{smtp_settings.get('smtp_from_email')}>"
|
|
msg['To'] = company_data.get('email')
|
|
msg['Subject'] = subject
|
|
msg['Date'] = formatdate(localtime=True)
|
|
|
|
# Add HTML content
|
|
msg.attach(MIMEText(html_content, 'html'))
|
|
|
|
# Send email
|
|
if smtp_settings.get('smtp_security') == 'ssl':
|
|
server = smtplib.SMTP_SSL(smtp_settings.get('smtp_host'), smtp_settings.get('smtp_port'))
|
|
else:
|
|
server = smtplib.SMTP(smtp_settings.get('smtp_host'), smtp_settings.get('smtp_port'))
|
|
|
|
if smtp_settings.get('smtp_security') == 'tls':
|
|
server.starttls()
|
|
|
|
if smtp_settings.get('smtp_username') and smtp_settings.get('smtp_password'):
|
|
server.login(smtp_settings.get('smtp_username'), smtp_settings.get('smtp_password'))
|
|
|
|
server.send_message(msg)
|
|
server.quit()
|
|
|
|
# Log the email sending
|
|
current_app.logger.info(f"Completion email sent to {company_data.get('email')} for instance {instance_url}")
|
|
|
|
return jsonify({
|
|
'message': 'Completion email sent successfully',
|
|
'data': {
|
|
'recipient': company_data.get('email'),
|
|
'subject': subject,
|
|
'instance_url': instance_url,
|
|
'password_reset_sent': True
|
|
}
|
|
})
|
|
|
|
except Exception as email_error:
|
|
current_app.logger.error(f"Failed to send completion email: {str(email_error)}")
|
|
return jsonify({'error': f'Failed to send email: {str(email_error)}'}), 500
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Error sending completion email: {str(e)}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@launch_api.route('/copy-smtp-settings', methods=['POST'])
|
|
@csrf.exempt
|
|
def copy_smtp_settings():
|
|
"""Copy SMTP settings from master instance to launched instance"""
|
|
try:
|
|
if not request.is_json:
|
|
return jsonify({'error': 'Request must be JSON'}), 400
|
|
|
|
data = request.get_json()
|
|
if 'instance_url' not in data:
|
|
return jsonify({'error': 'Missing instance_url parameter'}), 400
|
|
|
|
instance_url = data['instance_url']
|
|
|
|
# Get SMTP settings from master instance
|
|
smtp_settings = KeyValueSettings.get_value('smtp_settings')
|
|
if not smtp_settings:
|
|
return jsonify({'error': 'SMTP settings not configured on master instance'}), 400
|
|
|
|
# Get the instance from database to get the connection token
|
|
instance = Instance.query.filter_by(main_url=instance_url).first()
|
|
|
|
if not instance:
|
|
return jsonify({'error': 'Instance not found in database'}), 404
|
|
|
|
if not instance.connection_token:
|
|
return jsonify({'error': 'Instance not authenticated'}), 400
|
|
|
|
# Get JWT token from the launched instance using management API key
|
|
jwt_response = requests.post(
|
|
f"{instance_url.rstrip('/')}/api/admin/management-token",
|
|
headers={
|
|
'X-API-Key': instance.connection_token,
|
|
'Accept': 'application/json'
|
|
},
|
|
timeout=10
|
|
)
|
|
|
|
if jwt_response.status_code != 200:
|
|
return jsonify({'error': f'Failed to get JWT token: {jwt_response.text}'}), 400
|
|
|
|
jwt_data = jwt_response.json()
|
|
jwt_token = jwt_data.get('token')
|
|
|
|
if not jwt_token:
|
|
return jsonify({'error': 'No JWT token received'}), 400
|
|
|
|
# Prepare SMTP settings data for the API
|
|
api_smtp_data = {
|
|
'smtp_host': smtp_settings.get('smtp_host'),
|
|
'smtp_port': smtp_settings.get('smtp_port'),
|
|
'smtp_username': smtp_settings.get('smtp_username'),
|
|
'smtp_password': smtp_settings.get('smtp_password'),
|
|
'smtp_security': smtp_settings.get('smtp_security'),
|
|
'smtp_from_email': smtp_settings.get('smtp_from_email'),
|
|
'smtp_from_name': smtp_settings.get('smtp_from_name')
|
|
}
|
|
|
|
# Copy SMTP settings to the launched instance
|
|
smtp_response = requests.put(
|
|
f"{instance_url.rstrip('/')}/api/admin/settings",
|
|
headers={
|
|
'Authorization': f'Bearer {jwt_token}',
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
json=api_smtp_data,
|
|
timeout=10
|
|
)
|
|
|
|
if smtp_response.status_code != 200:
|
|
return jsonify({'error': f'Failed to copy SMTP settings: {smtp_response.text}'}), 400
|
|
|
|
# Log the SMTP settings copy
|
|
current_app.logger.info(f"SMTP settings copied to instance {instance_url}")
|
|
|
|
return jsonify({
|
|
'message': 'SMTP settings copied successfully',
|
|
'data': api_smtp_data
|
|
})
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Error copying SMTP settings: {str(e)}")
|
|
return jsonify({'error': str(e)}), 500 |