246 lines
8.1 KiB
Python
246 lines
8.1 KiB
Python
from app.core.models import App, Port, Server
|
|
from app.core.extensions import db
|
|
from flask import flash
|
|
import re
|
|
|
|
|
|
def validate_app_data(name, server_id, existing_app_id=None):
|
|
"""
|
|
Validate application data
|
|
Returns tuple (valid, error_message)
|
|
"""
|
|
if not name or not server_id:
|
|
return False, "Please fill in all required fields"
|
|
|
|
# Check if app exists with same name on same server
|
|
query = App.query.filter(App.name == name, App.server_id == server_id)
|
|
|
|
# If editing, exclude the current app
|
|
if existing_app_id:
|
|
query = query.filter(App.id != existing_app_id)
|
|
|
|
if query.first():
|
|
return False, f"Application '{name}' already exists on this server"
|
|
|
|
# Find similar app names on this server for suggestion
|
|
similar_apps = App.query.filter(
|
|
App.name.ilike(f"%{name}%"),
|
|
App.server_id == server_id
|
|
).limit(3).all()
|
|
|
|
if similar_apps and (not existing_app_id or not any(app.id == existing_app_id for app in similar_apps)):
|
|
similar_names = ", ".join([f"'{app.name}'" for app in similar_apps])
|
|
return False, f"Similar application names found on this server: {similar_names}"
|
|
|
|
return True, None
|
|
|
|
|
|
def is_port_in_use(port, server_id, exclude_app_id=None):
|
|
"""
|
|
Check if a port is already in use on a server
|
|
|
|
Args:
|
|
port: The port number to check
|
|
server_id: The ID of the server
|
|
exclude_app_id: Optional app ID to exclude from the check (for editing an app)
|
|
|
|
Returns:
|
|
bool: True if port is in use, False otherwise
|
|
"""
|
|
from app.core.models import App, Port
|
|
|
|
# Get all apps on this server
|
|
apps_on_server = App.query.filter_by(server_id=server_id).all()
|
|
|
|
for app in apps_on_server:
|
|
# Skip the app we're editing
|
|
if exclude_app_id and app.id == int(exclude_app_id):
|
|
continue
|
|
|
|
# Check if this app uses the port - use port_number rather than number
|
|
for app_port in app.ports:
|
|
if int(app_port.port_number) == int(port): # Use port_number here
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def validate_port_data(ports, descriptions=None, server_id=None, exclude_app_id=None, protocol=None):
|
|
"""
|
|
Validate port data - works with both the API and form submissions
|
|
|
|
Args:
|
|
ports: List of port numbers or a single port number string
|
|
descriptions: List of port descriptions or a single description
|
|
server_id: The server ID
|
|
exclude_app_id: Optional app ID to exclude from port conflict check
|
|
protocol: Optional protocol (for API validation)
|
|
|
|
Returns:
|
|
For form validation: Error message string or None if valid
|
|
For API validation: (valid, clean_port, error_message) tuple
|
|
"""
|
|
# Handle the API call format
|
|
if protocol is not None:
|
|
# This is the API validation path
|
|
try:
|
|
port = int(ports)
|
|
if port < 1 or port > 65535:
|
|
return False, None, f"Port {port} is out of valid range (1-65535)"
|
|
|
|
# Check if port is already in use
|
|
if is_port_in_use(port, server_id, exclude_app_id):
|
|
return False, None, f"Port {port} is already in use on this server"
|
|
|
|
return True, port, None
|
|
except ValueError:
|
|
return False, None, "Invalid port number"
|
|
|
|
# Handle the form submission format (list of ports)
|
|
seen_ports = set()
|
|
|
|
# Make sure ports is a list
|
|
if not isinstance(ports, list):
|
|
ports = [ports]
|
|
|
|
# Make sure descriptions is a list (or empty list)
|
|
if descriptions is None:
|
|
descriptions = []
|
|
elif not isinstance(descriptions, list):
|
|
descriptions = [descriptions]
|
|
|
|
for i, port_str in enumerate(ports):
|
|
if not port_str: # Skip empty port entries
|
|
continue
|
|
|
|
try:
|
|
port = int(port_str)
|
|
if port < 1 or port > 65535:
|
|
return f"Port {port} is out of valid range (1-65535)"
|
|
|
|
# Check for duplicate ports in the submitted data
|
|
if port in seen_ports:
|
|
return f"Duplicate port {port} in submission"
|
|
seen_ports.add(port)
|
|
|
|
# Check if port is already in use on this server
|
|
if is_port_in_use(port, server_id, exclude_app_id):
|
|
return f"Port {port} is already in use on this server"
|
|
|
|
except ValueError:
|
|
return f"Invalid port number: {port_str}"
|
|
|
|
return None
|
|
|
|
|
|
def process_app_ports(app_id, port_data, server_id=None):
|
|
"""
|
|
Process port data for an application
|
|
port_data should be a list of tuples (port_number, protocol, description)
|
|
Returns (success, error_message)
|
|
"""
|
|
# Get the app's server_id if not provided
|
|
if not server_id and app_id:
|
|
app = App.query.get(app_id)
|
|
if app:
|
|
server_id = app.server_id
|
|
|
|
# If no port data is provided, that's valid (app with no ports)
|
|
if not port_data:
|
|
return True, None
|
|
|
|
# Track the port+protocol combinations we've seen to avoid duplicates
|
|
seen_ports = set()
|
|
|
|
try:
|
|
for port_number, protocol, description in port_data:
|
|
# Skip empty port entries
|
|
if not port_number or not port_number.strip():
|
|
continue
|
|
|
|
port_key = f"{port_number}/{protocol}"
|
|
|
|
# Check for duplicates within this form submission
|
|
if port_key in seen_ports:
|
|
return False, f"Duplicate port: {port_key} is specified multiple times"
|
|
|
|
seen_ports.add(port_key)
|
|
|
|
# Validate the port data
|
|
valid, clean_port, error = validate_port_data(
|
|
[port_number], [description], server_id, app_id
|
|
)
|
|
|
|
if not valid:
|
|
return False, error
|
|
|
|
# Check if port already exists for this app
|
|
existing_port = Port.query.filter_by(
|
|
app_id=app_id, port_number=clean_port, protocol=protocol
|
|
).first()
|
|
|
|
if existing_port:
|
|
# Update existing port
|
|
existing_port.description = description
|
|
else:
|
|
# Create new port
|
|
new_port = Port(
|
|
app_id=app_id,
|
|
port_number=clean_port,
|
|
protocol=protocol,
|
|
description=description,
|
|
)
|
|
db.session.add(new_port)
|
|
|
|
return True, None
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
return False, str(e)
|
|
|
|
|
|
def save_app(name, server_id, documentation, port_data, app_id=None, url=None):
|
|
"""
|
|
Save or update an application
|
|
Returns (success, app, error_message)
|
|
"""
|
|
try:
|
|
# Validate application data
|
|
valid, error = validate_app_data(name, server_id, app_id)
|
|
if not valid:
|
|
return False, None, error
|
|
|
|
if app_id:
|
|
# Update existing app
|
|
app = App.query.get(app_id)
|
|
if not app:
|
|
return False, None, "Application not found"
|
|
app.name = name
|
|
app.server_id = server_id
|
|
app.documentation = documentation
|
|
app.url = url
|
|
else:
|
|
# Create new app
|
|
app = App(
|
|
name=name,
|
|
server_id=server_id,
|
|
documentation=documentation,
|
|
url=url
|
|
)
|
|
db.session.add(app)
|
|
db.session.flush() # Get the app ID without committing
|
|
|
|
# Remove all existing ports if updating
|
|
if app_id:
|
|
Port.query.filter_by(app_id=app_id).delete()
|
|
|
|
# Process and save ports
|
|
port_success, port_error = process_app_ports(app.id, port_data, server_id)
|
|
if not port_success:
|
|
db.session.rollback()
|
|
return False, None, port_error
|
|
|
|
db.session.commit()
|
|
return True, app, None
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
return False, None, str(e)
|