Files
docupulse/routes/launch_api.py
2025-06-23 14:24:13 +02:00

1835 lines
69 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:
# 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
# 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
# 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}")
# 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 shorter timeout for stack creation initiation (2 minutes)
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=120 # 2 minutes timeout for stack creation initiation
)
# 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("Request timed out while initiating stack deployment")
return jsonify({'error': 'Request timed out while initiating stack deployment. The operation may still be in progress.'}), 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"
services_response = requests.get(
services_url,
headers={
'X-API-Key': portainer_settings['api_key'],
'Accept': 'application/json'
},
params={'filters': json.dumps({'label': f'com.docker.stack.namespace={data["stack_name"]}'})},
timeout=30
)
if not services_response.ok:
return jsonify({'error': 'Failed to get stack services'}), 500
services = services_response.json()
# Check if all services are running
all_running = True
service_statuses = []
for service in services:
replicas_running = service.get('Spec', {}).get('Mode', {}).get('Replicated', {}).get('Replicas', 0)
replicas_actual = service.get('ServiceStatus', {}).get('RunningTasks', 0)
service_status = {
'name': service.get('Spec', {}).get('Name', 'Unknown'),
'replicas_expected': replicas_running,
'replicas_running': replicas_actual,
'status': 'running' if replicas_actual >= replicas_running else 'not_running'
}
service_statuses.append(service_status)
if replicas_actual < replicas_running:
all_running = False
# Determine overall stack status
if all_running and len(services) > 0:
status = 'active'
elif len(services) > 0:
status = 'partial'
else:
status = 'inactive'
return jsonify({
'success': True,
'data': {
'stack_name': data['stack_name'],
'stack_id': target_stack['Id'],
'status': status,
'services': service_statuses,
'total_services': len(services),
'running_services': len([s for s in service_statuses if s['status'] == 'running'])
}
})
except Exception as e:
current_app.logger.error(f"Error checking stack status: {str(e)}")
return jsonify({'error': str(e)}), 500
@launch_api.route('/save-instance', methods=['POST'])
@csrf.exempt
def save_instance():
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