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)