add connections

This commit is contained in:
2025-06-11 10:29:58 +02:00
parent 468235662b
commit 04689797f7
8 changed files with 1622 additions and 4 deletions

274
NGINX_swagger.json Normal file
View File

@@ -0,0 +1,274 @@
{
"openapi": "3.1.0",
"info": {
"title": "Nginx Proxy Manager API",
"version": "2.x.x"
},
"servers": [
{
"url": "http://127.0.0.1:81/api"
}
],
"components": {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
},
"paths": {
"/": {
"get": {
"$ref": "./paths/get.json"
}
},
"/audit-log": {
"get": {
"$ref": "./paths/audit-log/get.json"
}
},
"/nginx/access-lists": {
"get": {
"$ref": "./paths/nginx/access-lists/get.json"
},
"post": {
"$ref": "./paths/nginx/access-lists/post.json"
}
},
"/nginx/access-lists/{listID}": {
"get": {
"$ref": "./paths/nginx/access-lists/listID/get.json"
},
"put": {
"$ref": "./paths/nginx/access-lists/listID/put.json"
},
"delete": {
"$ref": "./paths/nginx/access-lists/listID/delete.json"
}
},
"/nginx/certificates": {
"get": {
"$ref": "./paths/nginx/certificates/get.json"
},
"post": {
"$ref": "./paths/nginx/certificates/post.json"
}
},
"/nginx/certificates/validate": {
"post": {
"$ref": "./paths/nginx/certificates/validate/post.json"
}
},
"/nginx/certificates/test-http": {
"get": {
"$ref": "./paths/nginx/certificates/test-http/get.json"
}
},
"/nginx/certificates/{certID}": {
"get": {
"$ref": "./paths/nginx/certificates/certID/get.json"
},
"delete": {
"$ref": "./paths/nginx/certificates/certID/delete.json"
}
},
"/nginx/certificates/{certID}/download": {
"get": {
"$ref": "./paths/nginx/certificates/certID/download/get.json"
}
},
"/nginx/certificates/{certID}/renew": {
"post": {
"$ref": "./paths/nginx/certificates/certID/renew/post.json"
}
},
"/nginx/certificates/{certID}/upload": {
"post": {
"$ref": "./paths/nginx/certificates/certID/upload/post.json"
}
},
"/nginx/proxy-hosts": {
"get": {
"$ref": "./paths/nginx/proxy-hosts/get.json"
},
"post": {
"$ref": "./paths/nginx/proxy-hosts/post.json"
}
},
"/nginx/proxy-hosts/{hostID}": {
"get": {
"$ref": "./paths/nginx/proxy-hosts/hostID/get.json"
},
"put": {
"$ref": "./paths/nginx/proxy-hosts/hostID/put.json"
},
"delete": {
"$ref": "./paths/nginx/proxy-hosts/hostID/delete.json"
}
},
"/nginx/proxy-hosts/{hostID}/enable": {
"post": {
"$ref": "./paths/nginx/proxy-hosts/hostID/enable/post.json"
}
},
"/nginx/proxy-hosts/{hostID}/disable": {
"post": {
"$ref": "./paths/nginx/proxy-hosts/hostID/disable/post.json"
}
},
"/nginx/redirection-hosts": {
"get": {
"$ref": "./paths/nginx/redirection-hosts/get.json"
},
"post": {
"$ref": "./paths/nginx/redirection-hosts/post.json"
}
},
"/nginx/redirection-hosts/{hostID}": {
"get": {
"$ref": "./paths/nginx/redirection-hosts/hostID/get.json"
},
"put": {
"$ref": "./paths/nginx/redirection-hosts/hostID/put.json"
},
"delete": {
"$ref": "./paths/nginx/redirection-hosts/hostID/delete.json"
}
},
"/nginx/redirection-hosts/{hostID}/enable": {
"post": {
"$ref": "./paths/nginx/redirection-hosts/hostID/enable/post.json"
}
},
"/nginx/redirection-hosts/{hostID}/disable": {
"post": {
"$ref": "./paths/nginx/redirection-hosts/hostID/disable/post.json"
}
},
"/nginx/dead-hosts": {
"get": {
"$ref": "./paths/nginx/dead-hosts/get.json"
},
"post": {
"$ref": "./paths/nginx/dead-hosts/post.json"
}
},
"/nginx/dead-hosts/{hostID}": {
"get": {
"$ref": "./paths/nginx/dead-hosts/hostID/get.json"
},
"put": {
"$ref": "./paths/nginx/dead-hosts/hostID/put.json"
},
"delete": {
"$ref": "./paths/nginx/dead-hosts/hostID/delete.json"
}
},
"/nginx/dead-hosts/{hostID}/enable": {
"post": {
"$ref": "./paths/nginx/dead-hosts/hostID/enable/post.json"
}
},
"/nginx/dead-hosts/{hostID}/disable": {
"post": {
"$ref": "./paths/nginx/dead-hosts/hostID/disable/post.json"
}
},
"/nginx/streams": {
"get": {
"$ref": "./paths/nginx/streams/get.json"
},
"post": {
"$ref": "./paths/nginx/streams/post.json"
}
},
"/nginx/streams/{streamID}": {
"get": {
"$ref": "./paths/nginx/streams/streamID/get.json"
},
"put": {
"$ref": "./paths/nginx/streams/streamID/put.json"
},
"delete": {
"$ref": "./paths/nginx/streams/streamID/delete.json"
}
},
"/nginx/streams/{streamID}/enable": {
"post": {
"$ref": "./paths/nginx/streams/streamID/enable/post.json"
}
},
"/nginx/streams/{streamID}/disable": {
"post": {
"$ref": "./paths/nginx/streams/streamID/disable/post.json"
}
},
"/reports/hosts": {
"get": {
"$ref": "./paths/reports/hosts/get.json"
}
},
"/schema": {
"get": {
"$ref": "./paths/schema/get.json"
}
},
"/settings": {
"get": {
"$ref": "./paths/settings/get.json"
}
},
"/settings/{settingID}": {
"get": {
"$ref": "./paths/settings/settingID/get.json"
},
"put": {
"$ref": "./paths/settings/settingID/put.json"
}
},
"/tokens": {
"get": {
"$ref": "./paths/tokens/get.json"
},
"post": {
"$ref": "./paths/tokens/post.json"
}
},
"/users": {
"get": {
"$ref": "./paths/users/get.json"
},
"post": {
"$ref": "./paths/users/post.json"
}
},
"/users/{userID}": {
"get": {
"$ref": "./paths/users/userID/get.json"
},
"put": {
"$ref": "./paths/users/userID/put.json"
},
"delete": {
"$ref": "./paths/users/userID/delete.json"
}
},
"/users/{userID}/auth": {
"put": {
"$ref": "./paths/users/userID/auth/put.json"
}
},
"/users/{userID}/permissions": {
"put": {
"$ref": "./paths/users/userID/permissions/put.json"
}
},
"/users/{userID}/login": {
"post": {
"$ref": "./paths/users/userID/login/post.json"
}
}
}
}

3
app.py
View File

@@ -20,6 +20,9 @@ from utils.asset_utils import get_asset_version
# Load environment variables
load_dotenv()
print("Environment variables after loading .env:")
print(f"MASTER: {os.getenv('MASTER')}")
print(f"ISMASTER: {os.getenv('ISMASTER')}")
def create_app():
app = Flask(__name__)

View File

@@ -11,6 +11,8 @@ import jwt
from werkzeug.security import generate_password_hash
import secrets
from flask_login import login_user
import requests
import json
admin_api = Blueprint('admin_api', __name__)
@@ -526,4 +528,334 @@ def resend_setup_mail(current_user, user_id):
db.session.add(mail)
db.session.commit()
return jsonify({'message': 'Setup mail queued for resending'})
return jsonify({'message': 'Setup mail queued for resending'})
# Connection Settings
@admin_api.route('/test-portainer-connection', methods=['POST'])
@csrf.exempt
def test_portainer_connection():
data = request.get_json()
url = data.get('url')
api_key = data.get('api_key')
if not url or not api_key:
return jsonify({'error': 'Missing required fields'}), 400
try:
# Test Portainer connection
response = requests.get(
f"{url.rstrip('/')}/api/status",
headers={
'X-API-Key': api_key,
'Accept': 'application/json'
},
timeout=5
)
if response.status_code == 200:
return jsonify({'message': 'Connection successful'})
else:
return jsonify({'error': 'Failed to connect to Portainer'}), 400
except Exception as e:
return jsonify({'error': str(e)}), 500
@admin_api.route('/test-nginx-connection', methods=['POST'])
@csrf.exempt
def test_nginx_connection():
data = request.get_json()
url = data.get('url')
username = data.get('username')
password = data.get('password')
if not url or not username or not password:
return jsonify({'error': 'Missing required fields'}), 400
try:
# Test NGINX Proxy Manager connection
response = requests.get(
f"{url.rstrip('/')}/api/nginx/proxy-hosts",
auth=(username, password),
headers={'Accept': 'application/json'},
timeout=5
)
if response.status_code == 200:
return jsonify({'message': 'Connection successful'})
else:
return jsonify({'error': 'Failed to connect to NGINX Proxy Manager'}), 400
except Exception as e:
return jsonify({'error': str(e)}), 500
@admin_api.route('/save-portainer-connection', methods=['POST'])
@csrf.exempt
@token_required
def save_portainer_connection(current_user):
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
data = request.get_json()
url = data.get('url')
api_key = data.get('api_key')
if not url or not api_key:
return jsonify({'error': 'Missing required fields'}), 400
try:
# Save Portainer settings
KeyValueSettings.set_value('portainer_settings', {
'url': url,
'api_key': api_key
})
return jsonify({'message': 'Settings saved successfully'})
except Exception as e:
return jsonify({'error': str(e)}), 500
@admin_api.route('/save-nginx-connection', methods=['POST'])
@csrf.exempt
@token_required
def save_nginx_connection(current_user):
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
data = request.get_json()
url = data.get('url')
username = data.get('username')
password = data.get('password')
if not url or not username or not password:
return jsonify({'error': 'Missing required fields'}), 400
try:
# Save NGINX Proxy Manager settings
KeyValueSettings.set_value('nginx_settings', {
'url': url,
'username': username,
'password': password
})
return jsonify({'message': 'Settings saved successfully'})
except Exception as e:
return jsonify({'error': str(e)}), 500
@admin_api.route('/generate-gitea-token', methods=['POST'])
@csrf.exempt
def generate_gitea_token():
"""Generate a new Gitea API token"""
data = request.get_json()
if not data or 'url' not in data or 'username' not in data or 'password' not in data:
return jsonify({'message': 'Missing required fields'}), 400
try:
headers = {
'Content-Type': 'application/json'
}
if data.get('otp'):
headers['X-Gitea-OTP'] = data['otp']
# Generate token with required scopes
token_data = {
'name': f'docupulse_{datetime.utcnow().strftime("%Y%m%d_%H%M%S")}',
'scopes': [
'read:activitypub',
'read:issue',
'write:misc',
'read:notification',
'read:organization',
'read:package',
'read:repository',
'read:user'
]
}
# Make request to Gitea API
response = requests.post(
f'{data["url"]}/api/v1/users/{data["username"]}/tokens',
headers=headers,
json=token_data,
auth=(data['username'], data['password'])
)
if response.status_code == 201:
token_data = response.json()
return jsonify({
'token': token_data['sha1'],
'name': token_data['name'],
'token_last_eight': token_data['token_last_eight']
}), 200
else:
return jsonify({'message': f'Failed to generate token: {response.json().get("message", "Unknown error")}'}), 400
except Exception as e:
return jsonify({'message': f'Failed to generate token: {str(e)}'}), 400
@admin_api.route('/list-gitea-repos', methods=['POST'])
@csrf.exempt
def list_gitea_repos():
"""List repositories from Gitea"""
data = request.get_json()
if not data or 'url' not in data or 'token' not in data:
return jsonify({'message': 'Missing required fields'}), 400
try:
# Try different authentication methods
headers = {
'Accept': 'application/json'
}
# First try token in Authorization header
headers['Authorization'] = f'token {data["token"]}'
# Get user's repositories
response = requests.get(
f'{data["url"]}/api/v1/user/repos',
headers=headers
)
# If that fails, try token as query parameter
if response.status_code != 200:
response = requests.get(
f'{data["url"]}/api/v1/user/repos?token={data["token"]}',
headers={'Accept': 'application/json'}
)
if response.status_code == 200:
return jsonify({
'repositories': response.json()
}), 200
else:
return jsonify({'message': f'Failed to list repositories: {response.json().get("message", "Unknown error")}'}), 400
except Exception as e:
return jsonify({'message': f'Failed to list repositories: {str(e)}'}), 400
@admin_api.route('/list-gitlab-repos', methods=['POST'])
@csrf.exempt
def list_gitlab_repos():
"""List repositories from GitLab"""
data = request.get_json()
if not data or 'url' not in data or 'token' not in data:
return jsonify({'message': 'Missing required fields'}), 400
try:
headers = {
'PRIVATE-TOKEN': data['token'],
'Accept': 'application/json'
}
# Get user's projects (repositories)
response = requests.get(
f'{data["url"]}/api/v4/projects',
headers=headers,
params={'membership': 'true'} # Only get projects where user is a member
)
if response.status_code == 200:
return jsonify({
'repositories': response.json()
}), 200
else:
return jsonify({'message': f'Failed to list repositories: {response.json().get("message", "Unknown error")}'}), 400
except Exception as e:
return jsonify({'message': f'Failed to list repositories: {str(e)}'}), 400
@admin_api.route('/test-git-connection', methods=['POST'])
@csrf.exempt
def test_git_connection():
"""Test the connection to a Git repository"""
data = request.get_json()
if not data or 'provider' not in data or 'url' not in data or 'username' not in data or 'token' not in data or 'repo' not in data:
return jsonify({'message': 'Missing required fields'}), 400
try:
if data['provider'] == 'gitea':
# Test Gitea connection with different authentication methods
headers = {
'Accept': 'application/json'
}
# First try token in Authorization header
headers['Authorization'] = f'token {data["token"]}'
# Try to get repository information
response = requests.get(
f'{data["url"]}/api/v1/repos/{data["repo"]}',
headers=headers
)
# If that fails, try token as query parameter
if response.status_code != 200:
response = requests.get(
f'{data["url"]}/api/v1/repos/{data["repo"]}?token={data["token"]}',
headers={'Accept': 'application/json'}
)
if response.status_code == 200:
return jsonify({'message': 'Connection successful'}), 200
else:
return jsonify({'message': f'Connection failed: {response.json().get("message", "Unknown error")}'}), 400
elif data['provider'] == 'gitlab':
# Test GitLab connection
headers = {
'PRIVATE-TOKEN': data['token'],
'Accept': 'application/json'
}
# Try to get repository information
response = requests.get(
f'{data["url"]}/api/v4/projects/{data["repo"].replace("/", "%2F")}',
headers=headers
)
if response.status_code == 200:
return jsonify({'message': 'Connection successful'}), 200
else:
return jsonify({'message': f'Connection failed: {response.json().get("message", "Unknown error")}'}), 400
else:
return jsonify({'message': 'Invalid Git provider'}), 400
except Exception as e:
return jsonify({'message': f'Connection failed: {str(e)}'}), 400
@admin_api.route('/save-git-connection', methods=['POST'])
@csrf.exempt
@token_required
def save_git_connection(current_user):
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
data = request.get_json()
provider = data.get('provider')
url = data.get('url')
username = data.get('username')
token = data.get('token')
repo = data.get('repo')
if not provider or not url or not username or not token or not repo:
return jsonify({'error': 'Missing required fields'}), 400
if provider not in ['gitea', 'gitlab']:
return jsonify({'error': 'Invalid provider'}), 400
try:
# Save Git settings
KeyValueSettings.set_value('git_settings', {
'provider': provider,
'url': url,
'username': username,
'token': token,
'repo': repo
})
return jsonify({'message': 'Settings saved successfully'})
except Exception as e:
return jsonify({'error': str(e)}), 500

View File

@@ -1,6 +1,6 @@
from flask import render_template, Blueprint, redirect, url_for, request, flash, Response, jsonify, session, current_app
from flask_login import current_user, login_required
from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings, Event, Conversation, Message, MessageAttachment, Notif, EmailTemplate, Mail, KeyValueSettings, DocuPulseSettings, PasswordSetupToken, Instance
from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings, Event, Conversation, Message, MessageAttachment, Notif, EmailTemplate, Mail, KeyValueSettings, DocuPulseSettings, PasswordSetupToken, Instance, ManagementAPIKey
from routes.auth import require_password_change
import os
from werkzeug.utils import secure_filename
@@ -905,7 +905,7 @@ def init_routes(main_bp):
active_tab = request.args.get('tab', 'colors')
# Validate tab parameter
valid_tabs = ['colors', 'general', 'email_templates', 'mails', 'security', 'events', 'debugging', 'smtp']
valid_tabs = ['colors', 'general', 'email_templates', 'mails', 'security', 'events', 'debugging', 'smtp', 'connections']
if active_tab not in valid_tabs:
active_tab = 'colors'
@@ -917,6 +917,20 @@ def init_routes(main_bp):
if active_tab == 'smtp':
smtp_settings = KeyValueSettings.get_value('smtp_settings')
# Get connection settings for the connections tab
portainer_settings = None
nginx_settings = None
git_settings = None
if active_tab == 'connections':
portainer_settings = KeyValueSettings.get_value('portainer_settings')
nginx_settings = KeyValueSettings.get_value('nginx_settings')
git_settings = KeyValueSettings.get_value('git_settings')
# Get management API key for the connections tab
management_api_key = ManagementAPIKey.query.filter_by(is_active=True).first()
if management_api_key:
site_settings.management_api_key = management_api_key.api_key
# Get events for the events tab
events = None
total_pages = 0
@@ -978,6 +992,9 @@ def init_routes(main_bp):
email_templates=email_templates,
form=company_form,
smtp_settings=smtp_settings,
portainer_settings=portainer_settings,
nginx_settings=nginx_settings,
git_settings=git_settings,
csrf_token=generate_csrf())
@main_bp.route('/settings/update-smtp', methods=['POST'])
@@ -1493,4 +1510,160 @@ def init_routes(main_bp):
headers={
'Content-Disposition': f'attachment; filename=event_log_{datetime.utcnow().strftime("%Y%m%d_%H%M%S")}.csv'
}
)
)
@main_bp.route('/settings/save-portainer-connection', methods=['POST'])
@login_required
def save_portainer_connection():
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
@main_bp.route('/settings/save-nginx-connection', methods=['POST'])
@login_required
def save_nginx_connection():
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
@main_bp.route('/settings/save-git-connection', methods=['POST'])
@login_required
def save_git_connection():
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
@main_bp.route('/settings/test-git-connection', methods=['POST'])
@login_required
def test_git_connection():
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')
if not provider or not url or not username or not token:
return jsonify({'error': 'Missing required fields'}), 400
if provider not in ['gitea', 'gitlab']:
return jsonify({'error': 'Invalid provider'}), 400
try:
if provider == 'gitea':
# Test Gitea connection with different authentication methods
headers = {
'Accept': 'application/json'
}
# First try token in Authorization header
headers['Authorization'] = f'token {token}'
# Try to get user information
response = requests.get(
f'{url.rstrip("/")}/api/v1/user',
headers=headers,
timeout=5
)
# If that fails, try token as query parameter
if response.status_code != 200:
response = requests.get(
f'{url.rstrip("/")}/api/v1/user?token={token}',
headers={'Accept': 'application/json'},
timeout=5
)
if response.status_code == 200:
return jsonify({'message': 'Connection successful'})
else:
return jsonify({'error': f'Connection failed: {response.json().get("message", "Unknown error")}'}), 400
elif provider == 'gitlab':
# Test GitLab connection
headers = {
'PRIVATE-TOKEN': token,
'Accept': 'application/json'
}
# Try to get user information
response = requests.get(
f'{url.rstrip("/")}/api/v4/user',
headers=headers,
timeout=5
)
if response.status_code == 200:
return jsonify({'message': 'Connection successful'})
else:
return jsonify({'error': f'Connection failed: {response.json().get("message", "Unknown error")}'}), 400
except Exception as e:
return jsonify({'error': f'Connection failed: {str(e)}'}), 400

View File

@@ -0,0 +1,89 @@
{% macro connection_modals() %}
<!-- Test Connection Modal -->
<div class="modal fade" id="testConnectionModal" tabindex="-1" aria-labelledby="testConnectionModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="testConnectionModalLabel">Testing Connection</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="text-center">
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p id="testConnectionMessage">Testing connection...</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Save Connection Modal -->
<div class="modal fade" id="saveConnectionModal" tabindex="-1" aria-labelledby="saveConnectionModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="saveConnectionModalLabel">Save Connection</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="text-center">
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p id="saveConnectionMessage">Saving connection settings...</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Error Modal -->
<div class="modal fade" id="errorModal" tabindex="-1" aria-labelledby="errorModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="errorModalLabel">Error</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="text-center">
<i class="fas fa-exclamation-circle text-danger fa-3x mb-3"></i>
<p id="errorMessage"></p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Success Modal -->
<div class="modal fade" id="successModal" tabindex="-1" aria-labelledby="successModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="successModalLabel">Success</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="text-center">
<i class="fas fa-check-circle text-success fa-3x mb-3"></i>
<p id="successMessage"></p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endmacro %}

View File

@@ -8,6 +8,7 @@
{% from "settings/tabs/email_templates.html" import email_templates_tab %}
{% from "settings/tabs/mails.html" import mails_tab %}
{% from "settings/tabs/smtp_settings.html" import smtp_settings_tab %}
{% from "settings/tabs/connections.html" import connections_tab %}
{% from "settings/components/reset_colors_modal.html" import reset_colors_modal %}
{% block title %}Settings - DocuPulse{% endblock %}
@@ -75,6 +76,13 @@
<i class="fas fa-server me-2"></i>SMTP
</button>
</li>
{% if is_master %}
<li class="nav-item" role="presentation">
<button class="nav-link {% if active_tab == 'connections' %}active{% endif %}" id="connections-tab" data-bs-toggle="tab" data-bs-target="#connections" type="button" role="tab" aria-controls="connections" aria-selected="{{ 'true' if active_tab == 'connections' else 'false' }}">
<i class="fas fa-plug me-2"></i>Connections
</button>
</li>
{% endif %}
</ul>
</div>
<div class="card-body">
@@ -122,6 +130,13 @@
<div class="tab-pane fade {% if active_tab == 'smtp' %}show active{% endif %}" id="smtp" role="tabpanel" aria-labelledby="smtp-tab">
{{ smtp_settings_tab(smtp_settings, csrf_token) }}
</div>
{% if is_master %}
<!-- Connections Tab -->
<div class="tab-pane fade {% if active_tab == 'connections' %}show active{% endif %}" id="connections" role="tabpanel" aria-labelledby="connections-tab">
{{ connections_tab(portainer_settings, nginx_settings, site_settings, git_settings) }}
</div>
{% endif %}
</div>
</div>
</div>

View File

@@ -0,0 +1,732 @@
{% from "settings/components/connection_modals.html" import connection_modals %}
{% macro connections_tab(portainer_settings, nginx_settings, site_settings, git_settings) %}
<div class="container-fluid">
<div class="row">
<!-- Portainer Connection Card -->
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-server me-2"></i>Portainer Connection
</h5>
<button class="btn btn-sm btn-outline-primary" onclick="testPortainerConnection()">
<i class="fas fa-plug me-1"></i>Test Connection
</button>
</div>
<div class="card-body">
<form id="portainerForm" onsubmit="savePortainerConnection(event)">
<div class="mb-3">
<label for="portainerUrl" class="form-label">Portainer URL</label>
<input type="url" class="form-control" id="portainerUrl" name="portainerUrl"
placeholder="https://portainer.example.com" required
value="{{ portainer_settings.url if portainer_settings and portainer_settings.url else '' }}">
<div class="form-text">The URL of your Portainer instance</div>
</div>
<div class="mb-3">
<label for="portainerApiKey" class="form-label">API Key</label>
<input type="password" class="form-control" id="portainerApiKey" name="portainerApiKey"
placeholder="Enter your Portainer API key" required
value="{{ portainer_settings.api_key if portainer_settings and portainer_settings.api_key else '' }}">
<div class="form-text">You can generate this in Portainer under Settings > API Keys</div>
</div>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>Save Portainer Settings
</button>
</div>
</form>
</div>
</div>
</div>
<!-- NGINX Connection Card -->
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-network-wired me-2"></i>NGINX Proxy Manager Connection
</h5>
<button class="btn btn-sm btn-outline-primary" onclick="testNginxConnection()">
<i class="fas fa-plug me-1"></i>Test Connection
</button>
</div>
<div class="card-body">
<form id="nginxForm" onsubmit="saveNginxConnection(event)">
<div class="mb-3">
<label for="nginxUrl" class="form-label">NGINX Proxy Manager URL</label>
<input type="url" class="form-control" id="nginxUrl" name="nginxUrl"
placeholder="https://nginx.example.com" required
value="{{ nginx_settings.url if nginx_settings and nginx_settings.url else '' }}">
<div class="form-text">The URL of your NGINX Proxy Manager instance</div>
</div>
<div class="mb-3">
<label for="nginxUsername" class="form-label">Username</label>
<input type="text" class="form-control" id="nginxUsername" name="nginxUsername"
placeholder="Enter your NGINX Proxy Manager username" required
value="{{ nginx_settings.username if nginx_settings and nginx_settings.username else '' }}">
</div>
<div class="mb-3">
<label for="nginxPassword" class="form-label">Password</label>
<input type="password" class="form-control" id="nginxPassword" name="nginxPassword"
placeholder="Enter your NGINX Proxy Manager password" required
value="{{ nginx_settings.password if nginx_settings and nginx_settings.password else '' }}">
</div>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>Save NGINX Settings
</button>
</div>
</form>
</div>
</div>
</div>
<!-- GitLab Connection Card -->
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fab fa-gitlab me-2"></i>GitLab Connection
</h5>
<button class="btn btn-sm btn-outline-primary" onclick="testGitConnection('gitlab')">
<i class="fas fa-plug me-1"></i>Test Connection
</button>
</div>
<div class="card-body">
<form id="gitlabForm" onsubmit="saveGitConnection(event, 'gitlab')">
<div class="mb-3">
<label for="gitlabUrl" class="form-label">GitLab Server URL</label>
<input type="url" class="form-control" id="gitlabUrl" name="gitlabUrl"
placeholder="https://gitlab.com" required
value="{{ git_settings.url if git_settings and git_settings.provider == 'gitlab' and git_settings.url else '' }}">
<div class="form-text">The URL of your GitLab server (use https://gitlab.com for GitLab.com)</div>
</div>
<div class="mb-3">
<label for="gitlabUsername" class="form-label">Username</label>
<input type="text" class="form-control" id="gitlabUsername" name="gitlabUsername"
placeholder="Enter your GitLab username" required
value="{{ git_settings.username if git_settings and git_settings.provider == 'gitlab' and git_settings.username else '' }}">
</div>
<div class="mb-3">
<label for="gitlabToken" class="form-label">Personal Access Token</label>
<input type="password" class="form-control" id="gitlabToken" name="gitlabToken"
placeholder="Enter your GitLab personal access token" required
value="{{ git_settings.token if git_settings and git_settings.provider == 'gitlab' and git_settings.token else '' }}">
<div class="form-text">You can generate this in your GitLab user settings > Access Tokens</div>
</div>
<div class="mb-3">
<label for="gitlabRepo" class="form-label">Repository</label>
<div class="input-group">
<select class="form-select" id="gitlabRepo" name="gitlabRepo" required>
<option value="">Select a repository</option>
{% if git_settings and git_settings.provider == 'gitlab' and git_settings.repo %}
<option value="{{ git_settings.repo }}" selected>{{ git_settings.repo }}</option>
{% endif %}
</select>
<button class="btn btn-outline-secondary" type="button" onclick="loadGitlabRepos()">
<i class="fas fa-sync-alt"></i>
</button>
</div>
<div class="form-text">Select the repository to connect to</div>
</div>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>Save GitLab Settings
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Gitea Connection Card -->
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-code-branch me-2"></i>Gitea Connection
</h5>
<button class="btn btn-sm btn-outline-primary" onclick="testGitConnection('gitea')">
<i class="fas fa-plug me-1"></i>Test Connection
</button>
</div>
<div class="card-body">
<form id="giteaForm" onsubmit="saveGitConnection(event, 'gitea')">
<div class="mb-3">
<label for="giteaUrl" class="form-label">Gitea Server URL</label>
<input type="url" class="form-control" id="giteaUrl" name="giteaUrl"
placeholder="https://gitea.example.com" required
value="{{ git_settings.url if git_settings and git_settings.provider == 'gitea' and git_settings.url else '' }}">
<div class="form-text">The URL of your Gitea server</div>
</div>
<div class="mb-3">
<label for="giteaUsername" class="form-label">Username</label>
<input type="text" class="form-control" id="giteaUsername" name="giteaUsername"
placeholder="Enter your Gitea username" required
value="{{ git_settings.username if git_settings and git_settings.provider == 'gitea' and git_settings.username else '' }}">
</div>
<div class="mb-3">
<label for="giteaPassword" class="form-label">Password</label>
<input type="password" class="form-control" id="giteaPassword" name="giteaPassword"
placeholder="Enter your Gitea password"
value="{{ git_settings.password if git_settings and git_settings.provider == 'gitea' and git_settings.password else '' }}">
<div class="form-text">Required for token generation if you don't have an existing token</div>
</div>
<div class="mb-3">
<label for="giteaOtp" class="form-label">Two-Factor Code (if enabled)</label>
<input type="text" class="form-control" id="giteaOtp" name="giteaOtp"
placeholder="Enter 2FA code if enabled">
<div class="form-text">Required if you have two-factor authentication enabled</div>
</div>
<div class="mb-3">
<label for="giteaToken" class="form-label">Access Token</label>
<div class="input-group">
<input type="password" class="form-control" id="giteaToken" name="giteaToken"
placeholder="Enter your Gitea access token" required
value="{{ git_settings.token if git_settings and git_settings.provider == 'gitea' and git_settings.token else '' }}">
<button class="btn btn-outline-secondary" type="button" onclick="generateGiteaToken()">
<i class="fas fa-key me-1"></i>Generate Token
</button>
</div>
<div class="form-text">You can generate a new token or use an existing one</div>
</div>
<div class="mb-3">
<label for="giteaRepo" class="form-label">Repository</label>
<div class="input-group">
<select class="form-select" id="giteaRepo" name="giteaRepo" required>
<option value="">Select a repository</option>
{% if git_settings and git_settings.provider == 'gitea' and git_settings.repo %}
<option value="{{ git_settings.repo }}" selected>{{ git_settings.repo }}</option>
{% endif %}
</select>
<button class="btn btn-outline-secondary" type="button" onclick="loadGiteaRepos()">
<i class="fas fa-sync-alt"></i>
</button>
</div>
<div class="form-text">Select the repository to connect to</div>
</div>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>Save Gitea Settings
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Save Connection Modal -->
<div class="modal fade" id="saveConnectionModal" tabindex="-1" aria-labelledby="saveConnectionModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="saveConnectionModalLabel">Save Connection</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p id="saveConnectionMessage"></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
{{ connection_modals() }}
<script>
// Form validation
(function () {
'use strict'
var forms = document.querySelectorAll('.needs-validation')
Array.prototype.slice.call(forms).forEach(function (form) {
form.addEventListener('submit', function (event) {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
// Show success modal
function showSuccess(message) {
const successModal = document.getElementById('successModal');
if (successModal) {
document.getElementById('successMessage').textContent = message;
new bootstrap.Modal(successModal).show();
} else {
alert(message); // Fallback if modal doesn't exist
}
}
// Show error modal
function showError(message) {
const errorModal = document.getElementById('errorModal');
if (errorModal) {
document.getElementById('errorMessage').textContent = message;
new bootstrap.Modal(errorModal).show();
} else {
alert(message); // Fallback if modal doesn't exist
}
}
// Get CSRF token from meta tag
function getCsrfToken() {
const metaTag = document.querySelector('meta[name="csrf-token"]');
return metaTag ? metaTag.getAttribute('content') : '';
}
// Get JWT token using management API key
async function getJwtToken() {
try {
const response = await fetch('/api/admin/management-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': '{{ site_settings.management_api_key }}'
}
});
if (!response.ok) {
throw new Error('Failed to get JWT token');
}
const data = await response.json();
return data.token;
} catch (error) {
console.error('Error getting JWT token:', error);
throw error;
}
}
// Load Gitea Repositories
async function loadGiteaRepos() {
const url = document.getElementById('giteaUrl').value;
const token = document.getElementById('giteaToken').value;
const repoSelect = document.getElementById('giteaRepo');
const currentRepo = repoSelect.value; // Store current selection
if (!url || !token) {
showError('Please fill in the server URL and access token');
return;
}
try {
const response = await fetch('/api/admin/list-gitea-repos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify({ url, token })
});
const data = await response.json();
if (response.ok) {
repoSelect.innerHTML = '<option value="">Select a repository</option>';
data.repositories.forEach(repo => {
const option = document.createElement('option');
option.value = repo.full_name;
option.textContent = repo.full_name;
if (repo.full_name === currentRepo) { // Restore selection
option.selected = true;
}
repoSelect.appendChild(option);
});
} else {
throw new Error(data.message || 'Failed to load repositories');
}
} catch (error) {
showError(error.message);
}
}
// Load GitLab Repositories
async function loadGitlabRepos() {
const url = document.getElementById('gitlabUrl').value;
const token = document.getElementById('gitlabToken').value;
const repoSelect = document.getElementById('gitlabRepo');
const currentRepo = repoSelect.value; // Store current selection
if (!url || !token) {
showError('Please fill in the server URL and access token');
return;
}
try {
const response = await fetch('/api/admin/list-gitlab-repos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify({ url, token })
});
const data = await response.json();
if (response.ok) {
repoSelect.innerHTML = '<option value="">Select a repository</option>';
data.repositories.forEach(repo => {
const option = document.createElement('option');
option.value = repo.path_with_namespace;
option.textContent = repo.path_with_namespace;
if (repo.path_with_namespace === currentRepo) { // Restore selection
option.selected = true;
}
repoSelect.appendChild(option);
});
} else {
throw new Error(data.message || 'Failed to load repositories');
}
} catch (error) {
showError(error.message);
}
}
// Load repositories on page load if settings exist
document.addEventListener('DOMContentLoaded', function() {
const gitSettings = JSON.parse('{{ git_settings|tojson|safe }}');
if (gitSettings) {
if (gitSettings.provider === 'gitea' && gitSettings.url && gitSettings.token) {
loadGiteaRepos();
} else if (gitSettings.provider === 'gitlab' && gitSettings.url && gitSettings.token) {
loadGitlabRepos();
}
}
});
// Test Git Connection
async function testGitConnection(provider) {
const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal'));
const messageElement = document.getElementById('saveConnectionMessage');
messageElement.textContent = 'Testing connection...';
messageElement.className = '';
saveModal.show();
try {
let data = {};
if (provider === 'gitea') {
const url = document.getElementById('giteaUrl').value;
const username = document.getElementById('giteaUsername').value;
const token = document.getElementById('giteaToken').value;
if (!url || !username || !token) {
throw new Error('Please fill in all required fields');
}
data = {
provider: 'gitea',
url: url,
username: username,
token: token
};
} else if (provider === 'gitlab') {
const url = document.getElementById('gitlabUrl').value;
const username = document.getElementById('gitlabUsername').value;
const token = document.getElementById('gitlabToken').value;
if (!url || !username || !token) {
throw new Error('Please fill in all required fields');
}
data = {
provider: 'gitlab',
url: url,
username: username,
token: token
};
}
const response = await fetch('/settings/test-git-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Connection test failed');
}
messageElement.textContent = 'Connection test successful!';
messageElement.className = 'text-success';
} catch (error) {
messageElement.textContent = error.message || 'Connection test failed';
messageElement.className = 'text-danger';
}
}
// Test Portainer Connection
async function testPortainerConnection() {
const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal'));
const messageElement = document.getElementById('saveConnectionMessage');
messageElement.textContent = 'Testing connection...';
messageElement.className = '';
saveModal.show();
try {
const url = document.getElementById('portainerUrl').value;
const apiKey = document.getElementById('portainerApiKey').value;
if (!url || !apiKey) {
throw new Error('Please fill in all required fields');
}
const response = await fetch('/api/admin/test-portainer-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify({
url: url,
api_key: apiKey
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Connection test failed');
}
messageElement.textContent = 'Connection test successful!';
messageElement.className = 'text-success';
} catch (error) {
messageElement.textContent = error.message || 'Connection test failed';
messageElement.className = 'text-danger';
}
}
// Test NGINX Connection
async function testNginxConnection() {
const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal'));
const messageElement = document.getElementById('saveConnectionMessage');
messageElement.textContent = 'Testing connection...';
messageElement.className = '';
saveModal.show();
try {
const url = document.getElementById('nginxUrl').value;
const username = document.getElementById('nginxUsername').value;
const password = document.getElementById('nginxPassword').value;
if (!url || !username || !password) {
throw new Error('Please fill in all required fields');
}
// First, get the token
const tokenResponse = await fetch(`${url}/api/tokens`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
identity: username,
secret: password
})
});
if (!tokenResponse.ok) {
throw new Error('Failed to authenticate with NGINX Proxy Manager');
}
const tokenData = await tokenResponse.json();
const token = tokenData.token;
// Now test the connection using the token
const response = await fetch(`${url}/api/nginx/proxy-hosts`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to connect to NGINX Proxy Manager');
}
messageElement.textContent = 'Connection test successful!';
messageElement.className = 'text-success';
} catch (error) {
messageElement.textContent = error.message || 'Connection test failed';
messageElement.className = 'text-danger';
}
}
// Save Portainer Connection
async function savePortainerConnection(event) {
event.preventDefault();
const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal'));
const messageElement = document.getElementById('saveConnectionMessage');
messageElement.textContent = '';
messageElement.className = '';
try {
const url = document.getElementById('portainerUrl').value;
const apiKey = document.getElementById('portainerApiKey').value;
if (!url || !apiKey) {
throw new Error('Please fill in all required fields');
}
const response = await fetch('/settings/save-portainer-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify({
url: url,
api_key: apiKey
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to save settings');
}
messageElement.textContent = 'Settings saved successfully!';
messageElement.className = 'text-success';
} catch (error) {
messageElement.textContent = error.message || 'Failed to save settings';
messageElement.className = 'text-danger';
}
saveModal.show();
}
// Save NGINX Connection
async function saveNginxConnection(event) {
event.preventDefault();
const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal'));
const messageElement = document.getElementById('saveConnectionMessage');
messageElement.textContent = '';
messageElement.className = '';
try {
const url = document.getElementById('nginxUrl').value;
const username = document.getElementById('nginxUsername').value;
const password = document.getElementById('nginxPassword').value;
if (!url || !username || !password) {
throw new Error('Please fill in all required fields');
}
const response = await fetch('/settings/save-nginx-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify({
url: url,
username: username,
password: password
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to save settings');
}
messageElement.textContent = 'Settings saved successfully!';
messageElement.className = 'text-success';
} catch (error) {
messageElement.textContent = error.message || 'Failed to save settings';
messageElement.className = 'text-danger';
}
saveModal.show();
}
// Save Git Connection
async function saveGitConnection(event, provider) {
event.preventDefault();
const saveModal = new bootstrap.Modal(document.getElementById('saveConnectionModal'));
const messageElement = document.getElementById('saveConnectionMessage');
messageElement.textContent = '';
messageElement.className = '';
try {
let data = {};
if (provider === 'gitea') {
const url = document.getElementById('giteaUrl').value;
const username = document.getElementById('giteaUsername').value;
const token = document.getElementById('giteaToken').value;
const repo = document.getElementById('giteaRepo').value;
const password = document.getElementById('giteaPassword').value;
const otp = document.getElementById('giteaOtp').value;
if (!url || !username || !token || !repo) {
throw new Error('Please fill in all required fields');
}
data = {
provider: 'gitea',
url: url,
username: username,
token: token,
repo: repo,
password: password,
otp: otp
};
} else if (provider === 'gitlab') {
const url = document.getElementById('gitlabUrl').value;
const username = document.getElementById('gitlabUsername').value;
const token = document.getElementById('gitlabToken').value;
const repo = document.getElementById('gitlabRepo').value;
if (!url || !username || !token || !repo) {
throw new Error('Please fill in all required fields');
}
data = {
provider: 'gitlab',
url: url,
username: username,
token: token,
repo: repo
};
}
const response = await fetch('/settings/save-git-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to save settings');
}
messageElement.textContent = 'Settings saved successfully!';
messageElement.className = 'text-success';
} catch (error) {
messageElement.textContent = error.message || 'Failed to save settings';
messageElement.className = 'text-danger';
}
saveModal.show();
}
</script>
{% endmacro %}