This commit is contained in:
Mateusz Gruszczyński
2025-11-04 09:56:37 +01:00
parent 32ef62e4ac
commit addb21bc3e
34 changed files with 3864 additions and 367 deletions

102
routes/auth_routes.py Normal file
View File

@@ -0,0 +1,102 @@
"""Authentication routes - Login, Logout, Register"""
from flask import Blueprint, render_template, request, redirect, url_for, session, jsonify, flash
from functools import wraps
from database import db
from database.models import User
from datetime import datetime
import logging
auth_bp = Blueprint('auth', __name__)
logger = logging.getLogger(__name__)
def login_required(f):
"""Decorator to require login"""
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return redirect(url_for('auth.login', next=request.url))
return f(*args, **kwargs)
return decorated_function
def admin_required(f):
"""Decorator to require admin role"""
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return redirect(url_for('auth.login'))
user = User.query.get(session['user_id'])
if not user or not user.is_admin:
flash('Admin access required', 'danger')
return redirect(url_for('main.index'))
return f(*args, **kwargs)
return decorated_function
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
"""Login page"""
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if not username or not password:
flash('Username and password required', 'warning')
return redirect(url_for('auth.login'))
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
session['user_id'] = user.id
session['username'] = user.username
session['is_admin'] = user.is_admin
session.permanent = True
user.last_login = datetime.utcnow()
db.session.commit()
logger.info(f"[AUTH] User '{username}' logged in", flush=True)
next_page = request.args.get('next')
if next_page and next_page.startswith('/'):
return redirect(next_page)
return redirect(url_for('main.index'))
logger.warning(f"[AUTH] Failed login attempt for '{username}'", flush=True)
flash('Invalid username or password', 'danger')
return render_template('login.html')
@auth_bp.route('/logout')
def logout():
"""Logout"""
username = session.get('username', 'Unknown')
session.clear()
logger.info(f"[AUTH] User '{username}' logged out", flush=True)
flash('You have been logged out', 'info')
return redirect(url_for('auth.login'))
@auth_bp.route('/api/current-user')
def api_current_user():
"""Get current user info (API)"""
if 'user_id' not in session:
return jsonify({'error': 'Not authenticated'}), 401
user = User.query.get(session['user_id'])
if not user:
session.clear()
return jsonify({'error': 'User not found'}), 404
return jsonify({
'id': user.id,
'username': user.username,
'is_admin': user.is_admin,
'created_at': user.created_at.isoformat() if user.created_at else None,
'last_login': user.last_login.isoformat() if user.last_login else None
})

217
routes/cert_routes.py Normal file
View File

@@ -0,0 +1,217 @@
"""Certificate Management - Upload, List, Delete"""
from flask import Blueprint, request, jsonify, session
from functools import wraps
from database import db
from database.models import Certificate, VirtualHost
from utils.cert_manager import parse_certificate, save_cert_file, delete_cert_file
from config.settings import UPLOAD_FOLDER, MAX_CONTENT_LENGTH
from datetime import datetime
import os
import logging
cert_bp = Blueprint('certs', __name__, url_prefix='/api/certificates')
logger = logging.getLogger(__name__)
def login_required_api(f):
"""API version of login required"""
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return jsonify({'error': 'Not authenticated', 'success': False}), 401
return f(*args, **kwargs)
return decorated_function
@cert_bp.route('', methods=['GET'])
@login_required_api
def list_certificates():
"""Get all certificates"""
try:
certs = Certificate.query.order_by(Certificate.created_at.desc()).all()
return jsonify({
'success': True,
'certificates': [{
'id': c.id,
'name': c.name,
'common_name': c.common_name,
'expires_at': c.expires_at.isoformat() if c.expires_at else None,
'created_at': c.created_at.isoformat(),
'vhost_count': len(c.vhosts),
'is_expired': c.expires_at < datetime.utcnow() if c.expires_at else False
} for c in certs]
})
except Exception as e:
logger.error(f"[CERTS] Error listing: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@cert_bp.route('/<int:cert_id>', methods=['GET'])
@login_required_api
def get_certificate(cert_id):
"""Get certificate details"""
try:
cert = Certificate.query.get(cert_id)
if not cert:
return jsonify({'error': 'Certificate not found', 'success': False}), 404
return jsonify({
'success': True,
'certificate': {
'id': cert.id,
'name': cert.name,
'common_name': cert.common_name,
'subject_alt_names': cert.get_san_list(),
'issued_at': cert.issued_at.isoformat() if cert.issued_at else None,
'expires_at': cert.expires_at.isoformat() if cert.expires_at else None,
'created_at': cert.created_at.isoformat(),
'vhosts': [{
'id': v.id,
'name': v.name,
'hostname': v.hostname
} for v in cert.vhosts]
}
})
except Exception as e:
logger.error(f"[CERTS] Error getting cert {cert_id}: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@cert_bp.route('', methods=['POST'])
@login_required_api
def upload_certificate():
"""Upload SSL certificate (PEM format)"""
try:
# Check if file provided
if 'cert_file' not in request.files:
return jsonify({'error': 'No certificate file provided', 'success': False}), 400
file = request.files['cert_file']
cert_name = request.form.get('name', '').strip()
if not file or file.filename == '':
return jsonify({'error': 'No selected file', 'success': False}), 400
if not cert_name:
return jsonify({'error': 'Certificate name required', 'success': False}), 400
# Check if name already exists
if Certificate.query.filter_by(name=cert_name).first():
return jsonify({'error': 'Certificate name already exists', 'success': False}), 400
# Read file content
cert_content = file.read().decode('utf-8')
# Validate and parse certificate
cert_data = parse_certificate(cert_content)
if 'error' in cert_data:
return jsonify({'error': cert_data['error'], 'success': False}), 400
# Create certificate record
cert = Certificate(
name=cert_name,
cert_content=cert_content,
cert_only=cert_data.get('cert_only'),
key_only=cert_data.get('key_only'),
common_name=cert_data.get('common_name'),
issued_at=cert_data.get('issued_at'),
expires_at=cert_data.get('expires_at')
)
if cert_data.get('subject_alt_names'):
cert.set_san_list(cert_data['subject_alt_names'])
db.session.add(cert)
db.session.flush()
# Save cert file to disk
cert_path = os.path.join(UPLOAD_FOLDER, f'{cert_name}.pem')
if not save_cert_file(cert_path, cert_content):
db.session.rollback()
return jsonify({'error': 'Failed to save certificate file', 'success': False}), 500
db.session.commit()
logger.info(f"[CERTS] Uploaded certificate '{cert_name}' by {session.get('username')}", flush=True)
return jsonify({
'success': True,
'id': cert.id,
'name': cert.name,
'message': 'Certificate uploaded successfully'
}), 201
except Exception as e:
db.session.rollback()
logger.error(f"[CERTS] Error uploading cert: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@cert_bp.route('/<int:cert_id>', methods=['DELETE'])
@login_required_api
def delete_certificate(cert_id):
"""Delete certificate"""
try:
cert = Certificate.query.get(cert_id)
if not cert:
return jsonify({'error': 'Certificate not found', 'success': False}), 404
# Check if certificate is in use
if cert.vhosts:
vhost_names = [v.name for v in cert.vhosts]
return jsonify({
'error': f'Certificate is in use by vhosts: {", ".join(vhost_names)}',
'success': False
}), 400
cert_name = cert.name
# Delete file from disk
cert_path = os.path.join(UPLOAD_FOLDER, f'{cert_name}.pem')
delete_cert_file(cert_path)
# Delete from database
db.session.delete(cert)
db.session.commit()
logger.info(f"[CERTS] Deleted certificate '{cert_name}' by {session.get('username')}", flush=True)
return jsonify({
'success': True,
'message': f'Certificate {cert_name} deleted successfully'
})
except Exception as e:
db.session.rollback()
logger.error(f"[CERTS] Error deleting cert: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@cert_bp.route('/<int:cert_id>/export', methods=['GET'])
@login_required_api
def export_certificate(cert_id):
"""Download certificate in PEM format"""
try:
cert = Certificate.query.get(cert_id)
if not cert:
return jsonify({'error': 'Certificate not found', 'success': False}), 404
# Read from disk
cert_path = os.path.join(UPLOAD_FOLDER, f'{cert.name}.pem')
if not os.path.exists(cert_path):
return jsonify({'error': 'Certificate file not found', 'success': False}), 404
with open(cert_path, 'r') as f:
cert_content = f.read()
return jsonify({
'success': True,
'name': cert.name,
'content': cert_content
})
except Exception as e:
logger.error(f"[CERTS] Error exporting cert: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500

211
routes/user_routes.py Normal file
View File

@@ -0,0 +1,211 @@
"""User management routes - Admin only"""
from flask import Blueprint, request, jsonify, session
from functools import wraps
from database import db
from database.models import User
from routes.auth_routes import admin_required
import logging
user_bp = Blueprint('users', __name__, url_prefix='/api/users')
logger = logging.getLogger(__name__)
def api_admin_required(f):
"""Decorator for API admin requirement"""
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return jsonify({'error': 'Not authenticated'}), 401
user = User.query.get(session['user_id'])
if not user or not user.is_admin:
return jsonify({'error': 'Admin access required'}), 403
return f(*args, **kwargs)
return decorated_function
@user_bp.route('', methods=['GET'])
@api_admin_required
def list_users():
"""List all users"""
try:
users = User.query.all()
return jsonify({
'success': True,
'users': [{
'id': u.id,
'username': u.username,
'is_admin': u.is_admin,
'created_at': u.created_at.isoformat(),
'last_login': u.last_login.isoformat() if u.last_login else None
} for u in users]
})
except Exception as e:
logger.error(f"[USERS] Error listing users: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@user_bp.route('', methods=['POST'])
@api_admin_required
def create_user():
"""Create new user"""
try:
data = request.json
username = data.get('username', '').strip()
password = data.get('password', '').strip()
is_admin = data.get('is_admin', False)
if not username or not password:
return jsonify({'error': 'Username and password required', 'success': False}), 400
if len(username) < 3:
return jsonify({'error': 'Username must be at least 3 characters', 'success': False}), 400
if len(password) < 6:
return jsonify({'error': 'Password must be at least 6 characters', 'success': False}), 400
# Check if exists
if User.query.filter_by(username=username).first():
return jsonify({'error': 'Username already exists', 'success': False}), 400
user = User(username=username, is_admin=is_admin)
user.set_password(password)
db.session.add(user)
db.session.commit()
logger.info(f"[USERS] Created user '{username}' by {session.get('username')}", flush=True)
return jsonify({
'success': True,
'id': user.id,
'username': user.username,
'is_admin': user.is_admin,
'message': 'User created successfully'
}), 201
except Exception as e:
db.session.rollback()
logger.error(f"[USERS] Error creating user: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@user_bp.route('/<int:user_id>', methods=['PUT'])
@api_admin_required
def update_user(user_id):
"""Update user"""
try:
user = User.query.get(user_id)
if not user:
return jsonify({'error': 'User not found', 'success': False}), 404
data = request.json
# Don't allow self-demotion
if user.id == session['user_id'] and 'is_admin' in data and not data['is_admin']:
return jsonify({'error': 'Cannot remove admin role from yourself', 'success': False}), 400
if 'is_admin' in data:
user.is_admin = data['is_admin']
if 'password' in data and data['password']:
password = data['password'].strip()
if len(password) < 6:
return jsonify({'error': 'Password must be at least 6 characters', 'success': False}), 400
user.set_password(password)
db.session.commit()
logger.info(f"[USERS] Updated user '{user.username}' by {session.get('username')}", flush=True)
return jsonify({
'success': True,
'message': 'User updated successfully',
'id': user.id,
'username': user.username,
'is_admin': user.is_admin
})
except Exception as e:
db.session.rollback()
logger.error(f"[USERS] Error updating user: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@user_bp.route('/<int:user_id>', methods=['DELETE'])
@api_admin_required
def delete_user(user_id):
"""Delete user"""
try:
# Don't allow self-deletion
if user_id == session['user_id']:
return jsonify({'error': 'Cannot delete your own account', 'success': False}), 400
user = User.query.get(user_id)
if not user:
return jsonify({'error': 'User not found', 'success': False}), 404
username = user.username
db.session.delete(user)
db.session.commit()
logger.info(f"[USERS] Deleted user '{username}' by {session.get('username')}", flush=True)
return jsonify({
'success': True,
'message': f'User {username} deleted successfully'
})
except Exception as e:
db.session.rollback()
logger.error(f"[USERS] Error deleting user: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@user_bp.route('/<int:user_id>/change-password', methods=['POST'])
def change_password(user_id):
"""Change own password"""
try:
# Check if logged in
if 'user_id' not in session:
return jsonify({'error': 'Not authenticated', 'success': False}), 401
# Can only change own password, or admin can change others
user = User.query.get(user_id)
if not user:
return jsonify({'error': 'User not found', 'success': False}), 404
is_admin = session.get('is_admin', False)
is_own_account = session['user_id'] == user_id
if not is_own_account and not is_admin:
return jsonify({'error': 'Forbidden', 'success': False}), 403
data = request.json
# If changing own password, require old password
if is_own_account:
old_password = data.get('old_password', '').strip()
if not user.check_password(old_password):
return jsonify({'error': 'Current password is incorrect', 'success': False}), 400
new_password = data.get('new_password', '').strip()
if len(new_password) < 6:
return jsonify({'error': 'Password must be at least 6 characters', 'success': False}), 400
user.set_password(new_password)
db.session.commit()
logger.info(f"[USERS] Password changed for '{user.username}'", flush=True)
return jsonify({
'success': True,
'message': 'Password changed successfully'
})
except Exception as e:
db.session.rollback()
logger.error(f"[USERS] Error changing password: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500

523
routes/vhost_routes.py Normal file
View File

@@ -0,0 +1,523 @@
"""Virtual Host Management - RESTful API"""
from flask import Blueprint, request, jsonify, session
from functools import wraps
from database import db
from database.models import VirtualHost, BackendServer, ConfigHistory, Certificate
from utils.config_generator import generate_haproxy_config, reload_haproxy
from datetime import datetime
import logging
vhost_bp = Blueprint('vhosts', __name__, url_prefix='/api/vhosts')
logger = logging.getLogger(__name__)
def login_required_api(f):
"""API version of login required"""
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return jsonify({'error': 'Not authenticated', 'success': False}), 401
return f(*args, **kwargs)
return decorated_function
@vhost_bp.route('', methods=['GET'])
@login_required_api
def list_vhosts():
"""Get all virtual hosts"""
try:
vhosts = VirtualHost.query.order_by(VirtualHost.created_at.desc()).all()
return jsonify({
'success': True,
'vhosts': [{
'id': v.id,
'name': v.name,
'hostname': v.hostname,
'frontend_ip': v.frontend_ip,
'frontend_port': v.frontend_port,
'protocol': v.protocol,
'use_ssl': v.use_ssl,
'lb_method': v.lb_method,
'enabled': v.enabled,
'backend_count': len(v.backend_servers),
'created_at': v.created_at.isoformat(),
'updated_at': v.updated_at.isoformat() if v.updated_at else None
} for v in vhosts]
})
except Exception as e:
logger.error(f"[VHOSTS] Error listing: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@vhost_bp.route('/<int:vhost_id>', methods=['GET'])
@login_required_api
def get_vhost(vhost_id):
"""Get single vhost with backend servers"""
try:
vhost = VirtualHost.query.get(vhost_id)
if not vhost:
return jsonify({'error': 'VHost not found', 'success': False}), 404
return jsonify({
'success': True,
'vhost': {
'id': vhost.id,
'name': vhost.name,
'hostname': vhost.hostname,
'description': vhost.description,
'frontend_ip': vhost.frontend_ip,
'frontend_port': vhost.frontend_port,
'protocol': vhost.protocol,
'use_ssl': vhost.use_ssl,
'certificate_id': vhost.certificate_id,
'ssl_redirect': vhost.ssl_redirect,
'ssl_redirect_port': vhost.ssl_redirect_port,
'lb_method': vhost.lb_method,
'dos_protection': vhost.dos_protection,
'dos_ban_duration': vhost.dos_ban_duration,
'dos_limit_requests': vhost.dos_limit_requests,
'sql_injection_check': vhost.sql_injection_check,
'xss_check': vhost.xss_check,
'webshell_check': vhost.webshell_check,
'add_custom_header': vhost.add_custom_header,
'custom_header_name': vhost.custom_header_name,
'custom_header_value': vhost.custom_header_value,
'del_server_header': vhost.del_server_header,
'forward_for': vhost.forward_for,
'enabled': vhost.enabled,
'backend_servers': [{
'id': bs.id,
'name': bs.name,
'ip_address': bs.ip_address,
'port': bs.port,
'maxconn': bs.maxconn,
'weight': bs.weight,
'health_check': bs.health_check,
'health_check_path': bs.health_check_path,
'enabled': bs.enabled
} for bs in vhost.backend_servers]
}
})
except Exception as e:
logger.error(f"[VHOSTS] Error getting vhost {vhost_id}: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@vhost_bp.route('', methods=['POST'])
@login_required_api
def create_vhost():
"""Create new virtual host"""
try:
data = request.json
# Validate required fields
required = ['name', 'hostname', 'frontend_port']
for field in required:
if not data.get(field):
return jsonify({'error': f'{field} is required', 'success': False}), 400
# Check if name already exists
if VirtualHost.query.filter_by(name=data['name']).first():
return jsonify({'error': 'VHost name already exists', 'success': False}), 400
# Create vhost
vhost = VirtualHost(
name=data['name'].strip(),
hostname=data['hostname'].strip(),
description=data.get('description', '').strip(),
frontend_ip=data.get('frontend_ip', '0.0.0.0'),
frontend_port=int(data['frontend_port']),
protocol=data.get('protocol', 'http'),
use_ssl=data.get('use_ssl', False),
certificate_id=data.get('certificate_id'),
ssl_redirect=data.get('ssl_redirect', False),
ssl_redirect_port=int(data.get('ssl_redirect_port', 80)),
lb_method=data.get('lb_method', 'roundrobin'),
dos_protection=data.get('dos_protection', False),
dos_ban_duration=data.get('dos_ban_duration', '30m'),
dos_limit_requests=int(data.get('dos_limit_requests', 100)),
sql_injection_check=data.get('sql_injection_check', False),
xss_check=data.get('xss_check', False),
webshell_check=data.get('webshell_check', False),
add_custom_header=data.get('add_custom_header', False),
custom_header_name=data.get('custom_header_name', ''),
custom_header_value=data.get('custom_header_value', ''),
del_server_header=data.get('del_server_header', False),
forward_for=data.get('forward_for', True),
enabled=data.get('enabled', True)
)
db.session.add(vhost)
db.session.flush()
# Add backend servers if provided
if data.get('backend_servers'):
for bs_data in data['backend_servers']:
backend = BackendServer(
vhost_id=vhost.id,
name=bs_data.get('name', f'server_{bs_data.get("ip_address")}'),
ip_address=bs_data['ip_address'],
port=int(bs_data['port']),
maxconn=bs_data.get('maxconn'),
weight=int(bs_data.get('weight', 1)),
health_check=bs_data.get('health_check', False),
health_check_path=bs_data.get('health_check_path', '/'),
enabled=bs_data.get('enabled', True)
)
db.session.add(backend)
db.session.commit()
# Save config history
config_history = ConfigHistory(
config_content=generate_haproxy_config(),
change_type='vhost_create',
vhost_id=vhost.id,
user_id=session['user_id'],
description=f"Created VHost: {vhost.name}"
)
db.session.add(config_history)
db.session.commit()
# Reload HAProxy
reload_haproxy()
logger.info(f"[VHOSTS] Created VHost '{vhost.name}' by {session.get('username')}", flush=True)
return jsonify({
'success': True,
'id': vhost.id,
'name': vhost.name,
'message': 'VHost created successfully'
}), 201
except Exception as e:
db.session.rollback()
logger.error(f"[VHOSTS] Error creating vhost: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@vhost_bp.route('/<int:vhost_id>', methods=['PUT'])
@login_required_api
def update_vhost(vhost_id):
"""Update virtual host"""
try:
vhost = VirtualHost.query.get(vhost_id)
if not vhost:
return jsonify({'error': 'VHost not found', 'success': False}), 404
data = request.json
# Update basic fields
if 'name' in data:
# Check if name is taken
existing = VirtualHost.query.filter_by(name=data['name']).filter(VirtualHost.id != vhost_id).first()
if existing:
return jsonify({'error': 'VHost name already exists', 'success': False}), 400
vhost.name = data['name'].strip()
if 'hostname' in data:
vhost.hostname = data['hostname'].strip()
if 'description' in data:
vhost.description = data['description'].strip()
if 'frontend_ip' in data:
vhost.frontend_ip = data['frontend_ip']
if 'frontend_port' in data:
vhost.frontend_port = int(data['frontend_port'])
if 'protocol' in data:
vhost.protocol = data['protocol']
if 'use_ssl' in data:
vhost.use_ssl = data['use_ssl']
if 'certificate_id' in data:
vhost.certificate_id = data['certificate_id']
if 'ssl_redirect' in data:
vhost.ssl_redirect = data['ssl_redirect']
if 'ssl_redirect_port' in data:
vhost.ssl_redirect_port = int(data['ssl_redirect_port'])
if 'lb_method' in data:
vhost.lb_method = data['lb_method']
# Security settings
if 'dos_protection' in data:
vhost.dos_protection = data['dos_protection']
if 'dos_ban_duration' in data:
vhost.dos_ban_duration = data['dos_ban_duration']
if 'dos_limit_requests' in data:
vhost.dos_limit_requests = int(data['dos_limit_requests'])
if 'sql_injection_check' in data:
vhost.sql_injection_check = data['sql_injection_check']
if 'xss_check' in data:
vhost.xss_check = data['xss_check']
if 'webshell_check' in data:
vhost.webshell_check = data['webshell_check']
# Header settings
if 'add_custom_header' in data:
vhost.add_custom_header = data['add_custom_header']
if 'custom_header_name' in data:
vhost.custom_header_name = data['custom_header_name']
if 'custom_header_value' in data:
vhost.custom_header_value = data['custom_header_value']
if 'del_server_header' in data:
vhost.del_server_header = data['del_server_header']
if 'forward_for' in data:
vhost.forward_for = data['forward_for']
if 'enabled' in data:
vhost.enabled = data['enabled']
vhost.updated_at = datetime.utcnow()
db.session.commit()
# Save config history
config_history = ConfigHistory(
config_content=generate_haproxy_config(),
change_type='vhost_edit',
vhost_id=vhost.id,
user_id=session['user_id'],
description=f"Updated VHost: {vhost.name}"
)
db.session.add(config_history)
db.session.commit()
# Reload HAProxy
reload_haproxy()
logger.info(f"[VHOSTS] Updated VHost '{vhost.name}' by {session.get('username')}", flush=True)
return jsonify({
'success': True,
'message': 'VHost updated successfully'
})
except Exception as e:
db.session.rollback()
logger.error(f"[VHOSTS] Error updating vhost {vhost_id}: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@vhost_bp.route('/<int:vhost_id>', methods=['DELETE'])
@login_required_api
def delete_vhost(vhost_id):
"""Delete virtual host"""
try:
vhost = VirtualHost.query.get(vhost_id)
if not vhost:
return jsonify({'error': 'VHost not found', 'success': False}), 404
vhost_name = vhost.name
# Save config history before deletion
config_history = ConfigHistory(
config_content=generate_haproxy_config(),
change_type='vhost_delete',
vhost_id=vhost.id,
user_id=session['user_id'],
description=f"Deleted VHost: {vhost_name}"
)
db.session.add(config_history)
# Delete vhost (cascades to backend servers)
db.session.delete(vhost)
db.session.commit()
# Reload HAProxy
reload_haproxy()
logger.info(f"[VHOSTS] Deleted VHost '{vhost_name}' by {session.get('username')}", flush=True)
return jsonify({
'success': True,
'message': f'VHost {vhost_name} deleted successfully'
})
except Exception as e:
db.session.rollback()
logger.error(f"[VHOSTS] Error deleting vhost {vhost_id}: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@vhost_bp.route('/<int:vhost_id>/toggle', methods=['POST'])
@login_required_api
def toggle_vhost(vhost_id):
"""Toggle vhost enabled/disabled"""
try:
vhost = VirtualHost.query.get(vhost_id)
if not vhost:
return jsonify({'error': 'VHost not found', 'success': False}), 404
vhost.enabled = not vhost.enabled
vhost.updated_at = datetime.utcnow()
db.session.commit()
# Reload HAProxy
reload_haproxy()
logger.info(f"[VHOSTS] Toggled VHost '{vhost.name}' to {vhost.enabled} by {session.get('username')}", flush=True)
return jsonify({
'success': True,
'enabled': vhost.enabled,
'message': f"VHost {'enabled' if vhost.enabled else 'disabled'}"
})
except Exception as e:
db.session.rollback()
logger.error(f"[VHOSTS] Error toggling vhost {vhost_id}: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
# ===== BACKEND SERVERS =====
@vhost_bp.route('/<int:vhost_id>/servers', methods=['GET'])
@login_required_api
def get_vhost_servers(vhost_id):
"""Get all backend servers for vhost"""
try:
vhost = VirtualHost.query.get(vhost_id)
if not vhost:
return jsonify({'error': 'VHost not found', 'success': False}), 404
return jsonify({
'success': True,
'servers': [{
'id': bs.id,
'name': bs.name,
'ip_address': bs.ip_address,
'port': bs.port,
'maxconn': bs.maxconn,
'weight': bs.weight,
'health_check': bs.health_check,
'health_check_path': bs.health_check_path,
'enabled': bs.enabled
} for bs in vhost.backend_servers]
})
except Exception as e:
logger.error(f"[SERVERS] Error getting servers: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@vhost_bp.route('/<int:vhost_id>/servers', methods=['POST'])
@login_required_api
def add_backend_server(vhost_id):
"""Add backend server to vhost"""
try:
vhost = VirtualHost.query.get(vhost_id)
if not vhost:
return jsonify({'error': 'VHost not found', 'success': False}), 404
data = request.json
if not data.get('ip_address') or not data.get('port'):
return jsonify({'error': 'IP address and port required', 'success': False}), 400
server = BackendServer(
vhost_id=vhost_id,
name=data.get('name', f"server_{data['ip_address']}"),
ip_address=data['ip_address'],
port=int(data['port']),
maxconn=data.get('maxconn'),
weight=int(data.get('weight', 1)),
health_check=data.get('health_check', False),
health_check_path=data.get('health_check_path', '/'),
enabled=data.get('enabled', True)
)
db.session.add(server)
db.session.commit()
# Reload HAProxy
reload_haproxy()
logger.info(f"[SERVERS] Added server to VHost '{vhost.name}' by {session.get('username')}", flush=True)
return jsonify({
'success': True,
'id': server.id,
'message': 'Backend server added'
}), 201
except Exception as e:
db.session.rollback()
logger.error(f"[SERVERS] Error adding server: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@vhost_bp.route('/servers/<int:server_id>', methods=['PUT'])
@login_required_api
def update_backend_server(server_id):
"""Update backend server"""
try:
server = BackendServer.query.get(server_id)
if not server:
return jsonify({'error': 'Server not found', 'success': False}), 404
data = request.json
if 'name' in data:
server.name = data['name']
if 'ip_address' in data:
server.ip_address = data['ip_address']
if 'port' in data:
server.port = int(data['port'])
if 'maxconn' in data:
server.maxconn = data['maxconn']
if 'weight' in data:
server.weight = int(data['weight'])
if 'health_check' in data:
server.health_check = data['health_check']
if 'health_check_path' in data:
server.health_check_path = data['health_check_path']
if 'enabled' in data:
server.enabled = data['enabled']
server.updated_at = datetime.utcnow()
db.session.commit()
# Reload HAProxy
reload_haproxy()
logger.info(f"[SERVERS] Updated server '{server.name}' by {session.get('username')}", flush=True)
return jsonify({
'success': True,
'message': 'Backend server updated'
})
except Exception as e:
db.session.rollback()
logger.error(f"[SERVERS] Error updating server: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500
@vhost_bp.route('/servers/<int:server_id>', methods=['DELETE'])
@login_required_api
def delete_backend_server(server_id):
"""Delete backend server"""
try:
server = BackendServer.query.get(server_id)
if not server:
return jsonify({'error': 'Server not found', 'success': False}), 404
server_name = server.name
vhost_id = server.vhost_id
db.session.delete(server)
db.session.commit()
# Reload HAProxy
reload_haproxy()
logger.info(f"[SERVERS] Deleted server '{server_name}' by {session.get('username')}", flush=True)
return jsonify({
'success': True,
'message': f'Backend server {server_name} deleted'
})
except Exception as e:
db.session.rollback()
logger.error(f"[SERVERS] Error deleting server: {e}", flush=True)
return jsonify({'error': str(e), 'success': False}), 500