from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify from flask_login import login_required, current_user import markdown from app.core.models import Server, App, Subnet, Port from app.core.extensions import db, limiter from datetime import datetime from app.utils.app_utils import validate_app_data, is_port_in_use bp = Blueprint("dashboard", __name__, url_prefix="/dashboard") @bp.route("/") def dashboard_home(): """Main dashboard view showing server statistics""" # Check if user is logged in, redirect if not if not current_user.is_authenticated: flash("Please log in to access this page.", "info") return redirect(url_for('auth.login')) server_count = Server.query.count() app_count = App.query.count() subnet_count = Subnet.query.count() # Get latest added servers latest_servers = Server.query.order_by(Server.created_at.desc()).limit(5).all() # Get subnets with usage stats subnets = Subnet.query.all() for subnet in subnets: subnet.usage_percent = ( subnet.used_ips / 254 * 100 if subnet.cidr.endswith("/24") else 0 ) return render_template( "dashboard/index.html", title="Dashboard", server_count=server_count, app_count=app_count, subnet_count=subnet_count, latest_servers=latest_servers, subnets=subnets, now=datetime.now(), ) @bp.route("/servers") @login_required def server_list(): """List all servers""" servers = Server.query.order_by(Server.hostname).all() return render_template( "dashboard/server_list.html", title="Servers", servers=servers, now=datetime.now(), ) @bp.route("/server/") @login_required def server_view(server_id): """View server details""" server = Server.query.get_or_404(server_id) apps = App.query.filter_by(server_id=server_id).all() return render_template( "dashboard/server_view.html", title=f"Server - {server.hostname}", server=server, apps=apps, now=datetime.now(), ) @bp.route("/server/new", methods=["GET", "POST"]) @login_required def server_new(): """Create a new server""" subnets = Subnet.query.all() if request.method == "POST": hostname = request.form.get("hostname") ip_address = request.form.get("ip_address") subnet_id = request.form.get("subnet_id") documentation = request.form.get("documentation", "") # Basic validation if not hostname or not ip_address or not subnet_id: flash("Please fill in all required fields", "danger") return render_template( "dashboard/server_form.html", title="New Server", subnets=subnets, now=datetime.now(), ) # Check if hostname or IP already exists if Server.query.filter_by(hostname=hostname).first(): flash("Hostname already exists", "danger") return render_template( "dashboard/server_form.html", title="New Server", subnets=subnets, now=datetime.now(), ) if Server.query.filter_by(ip_address=ip_address).first(): flash("IP address already exists", "danger") return render_template( "dashboard/server_form.html", title="New Server", subnets=subnets, now=datetime.now(), ) # Create new server server = Server( hostname=hostname, ip_address=ip_address, subnet_id=subnet_id, documentation=documentation, ) db.session.add(server) db.session.commit() flash("Server created successfully", "success") return redirect(url_for("dashboard.server_view", server_id=server.id)) return render_template( "dashboard/server_form.html", title="New Server", subnets=subnets, now=datetime.now(), ) @bp.route("/server//edit", methods=["GET", "POST"]) @login_required def server_edit(server_id): """Edit an existing server""" server = Server.query.get_or_404(server_id) subnets = Subnet.query.all() if request.method == "POST": hostname = request.form.get("hostname") ip_address = request.form.get("ip_address") subnet_id = request.form.get("subnet_id") documentation = request.form.get("documentation", "") if not hostname or not ip_address or not subnet_id: flash("All fields are required", "danger") return render_template( "dashboard/server_form.html", title="Edit Server", server=server, subnets=subnets, ) # Check if hostname changed and already exists if ( hostname != server.hostname and Server.query.filter_by(hostname=hostname).first() ): flash("Hostname already exists", "danger") return render_template( "dashboard/server_form.html", title="Edit Server", server=server, subnets=subnets, ) # Check if IP changed and already exists if ( ip_address != server.ip_address and Server.query.filter_by(ip_address=ip_address).first() ): flash("IP address already exists", "danger") return render_template( "dashboard/server_form.html", title="Edit Server", server=server, subnets=subnets, ) # Update server server.hostname = hostname server.ip_address = ip_address server.subnet_id = subnet_id server.documentation = documentation db.session.commit() flash("Server updated successfully", "success") return redirect(url_for("dashboard.server_view", server_id=server.id)) # GET request - show form with current values return render_template( "dashboard/server_form.html", title=f"Edit Server - {server.hostname}", server=server, subnets=subnets, ) @bp.route("/server//delete", methods=["POST"]) @login_required def server_delete(server_id): """Delete a server""" server = Server.query.get_or_404(server_id) # Delete all apps associated with this server App.query.filter_by(server_id=server_id).delete() # Delete the server db.session.delete(server) db.session.commit() flash("Server deleted successfully", "success") return redirect(url_for("dashboard.dashboard_home")) @bp.route("/app/new", methods=["GET", "POST"]) @bp.route("/app/new/", methods=["GET", "POST"]) @login_required def app_new(server_id=None): """Create a new application""" servers = Server.query.all() if request.method == "POST": name = request.form.get("name", "").strip() server_id = request.form.get("server_id") documentation = request.form.get("documentation", "") url = request.form.get("url", "") # Validate application name if not name: flash("Application name is required", "danger") return render_template( "dashboard/app_form.html", edit_mode=False, servers=servers, selected_server_id=server_id ) # Check for duplicate application names on the same server existing_app = App.query.filter_by(name=name, server_id=server_id).first() if existing_app: flash(f"An application with the name '{name}' already exists on this server", "danger") return render_template( "dashboard/app_form.html", edit_mode=False, servers=servers, selected_server_id=server_id ) # Process port data from form port_data = [] port_numbers = request.form.getlist("port_numbers[]") protocols = request.form.getlist("protocols[]") descriptions = request.form.getlist("port_descriptions[]") for i in range(len(port_numbers)): if port_numbers[i] and port_numbers[i].strip(): protocol = protocols[i] if i < len(protocols) else "TCP" description = descriptions[i] if i < len(descriptions) else "" port_data.append((port_numbers[i], protocol, description)) # Check for port conflicts proactively conflicts = [] seen_ports = set() # To track ports already seen in this submission for i, (port_number, protocol, _) in enumerate(port_data): try: clean_port = int(port_number) # Check if this port has already been seen in this submission port_key = f"{clean_port}/{protocol}" if port_key in seen_ports: conflicts.append((clean_port, protocol, "Duplicate port in submission")) continue seen_ports.add(port_key) # Check if the port is in use by another application in_use, conflicting_app_name = is_port_in_use( clean_port, protocol, server_id ) if in_use: conflicts.append((clean_port, protocol, f"Port {clean_port}/{protocol} is already in use by application '{conflicting_app_name}'")) except (ValueError, TypeError): continue if conflicts: for conflict in conflicts: flash(f"Conflict: {conflict[0]}/{conflict[1]} - {conflict[2]}", "danger") return render_template( "dashboard/app_form.html", edit_mode=False, servers=servers, selected_server_id=server_id ) # Create the application try: new_app = App( name=name, server_id=server_id, documentation=documentation, url=url ) db.session.add(new_app) db.session.flush() # Get the app ID without committing # Add ports for port_number, protocol, description in port_data: new_port = Port( app_id=new_app.id, port_number=int(port_number), protocol=protocol, description=description ) db.session.add(new_port) db.session.commit() flash(f"Application '{name}' created successfully", "success") return redirect(url_for("dashboard.app_view", app_id=new_app.id)) except Exception as e: db.session.rollback() flash(f"Error creating application: {str(e)}", "danger") # GET request or validation failed - render the form return render_template( "dashboard/app_form.html", edit_mode=False, servers=servers, selected_server_id=server_id ) @bp.route("/app/", methods=["GET"]) @login_required def app_view(app_id): """View a specific application""" app = App.query.get_or_404(app_id) server = Server.query.get(app.server_id) return render_template( "dashboard/app_view.html", title=f"Application - {app.name}", app=app, server=server, now=datetime.now(), ) @bp.route("/app//edit", methods=["GET", "POST"]) @login_required def app_edit(app_id): """Edit an existing application with comprehensive error handling""" app = App.query.get_or_404(app_id) servers = Server.query.all() if request.method == "POST": # Get form data name = request.form.get("name", "").strip() server_id = request.form.get("server_id") documentation = request.form.get("documentation", "") url = request.form.get("url", "") # Get the URL # Validate application name if not name: flash("Application name is required", "danger") return render_template( "dashboard/app_form.html", edit_mode=True, app=app, servers=servers ) # Check for duplicate application names on the same server (excluding this app) existing_app = App.query.filter( App.name == name, App.server_id == server_id, App.id != app_id ).first() if existing_app: flash(f"An application with the name '{name}' already exists on this server", "danger") return render_template( "dashboard/app_form.html", edit_mode=True, app=app, servers=servers ) # Process port data from form port_data = [] port_numbers = request.form.getlist("port_numbers[]") protocols = request.form.getlist("protocols[]") descriptions = request.form.getlist("port_descriptions[]") for i in range(len(port_numbers)): if port_numbers[i] and port_numbers[i].strip(): protocol = protocols[i] if i < len(protocols) else "TCP" description = descriptions[i] if i < len(descriptions) else "" port_data.append((port_numbers[i], protocol, description)) # Check for port conflicts proactively conflicts = [] seen_ports = set() # To track ports already seen in this submission for i, (port_number, protocol, _) in enumerate(port_data): try: clean_port = int(port_number) # Check if this port has already been seen in this submission port_key = f"{clean_port}/{protocol}" if port_key in seen_ports: conflicts.append((clean_port, protocol, "Duplicate port in submission")) continue seen_ports.add(port_key) # Check if the port is in use by another application in_use, conflicting_app_name = is_port_in_use( clean_port, protocol, server_id, exclude_app_id=app_id ) if in_use: conflicts.append((clean_port, protocol, f"Port {clean_port}/{protocol} is already in use by application '{conflicting_app_name}'")) except (ValueError, TypeError): continue if conflicts: for conflict in conflicts: flash(f"Conflict: {conflict[0]}/{conflict[1]} - {conflict[2]}", "danger") return render_template( "dashboard/app_form.html", edit_mode=True, app=app, servers=servers ) # Update application details app.name = name app.server_id = server_id app.documentation = documentation app.url = url # Only delete existing ports if new port data is provided if port_data: # Remove existing ports and add new ones Port.query.filter_by(app_id=app_id).delete() for port_number, protocol, description in port_data: new_port = Port( app_id=app_id, port_number=int(port_number), protocol=protocol, description=description ) db.session.add(new_port) db.session.commit() flash("Application updated successfully", "success") return redirect(url_for("dashboard.app_view", app_id=app_id)) return render_template( "dashboard/app_form.html", edit_mode=True, app=app, servers=servers ) @bp.route("/app//delete", methods=["POST"]) @login_required def app_delete(app_id): """Delete an application and all its ports""" app = App.query.get_or_404(app_id) app_name = app.name server_id = app.server_id try: # First explicitly delete all associated ports Port.query.filter_by(app_id=app_id).delete() # Then delete the application db.session.delete(app) db.session.commit() flash(f"Application '{app_name}' has been deleted", "success") # Redirect back to server view return redirect(url_for("dashboard.server_view", server_id=server_id)) except Exception as e: db.session.rollback() flash(f"Error deleting application: {str(e)}", "danger") return redirect(url_for("dashboard.app_view", app_id=app_id)) @bp.route("/settings", methods=["GET", "POST"]) @login_required def settings(): """User settings page""" if request.method == "POST": # Handle user settings update current_password = request.form.get("current_password") new_password = request.form.get("new_password") confirm_password = request.form.get("confirm_password") # Validate inputs if not current_password: flash("Current password is required", "danger") return redirect(url_for("dashboard.settings")) if new_password != confirm_password: flash("New passwords do not match", "danger") return redirect(url_for("dashboard.settings")) # Verify current password if not current_user.check_password(current_password): flash("Current password is incorrect", "danger") return redirect(url_for("dashboard.settings")) # Update password current_user.set_password(new_password) db.session.commit() flash("Password updated successfully", "success") return redirect(url_for("dashboard.settings")) return render_template("dashboard/settings.html", title="User Settings") @bp.route("/overview") @login_required def overview(): """Hierarchical overview of subnets, servers, and applications""" # Get all subnets with their servers subnets = Subnet.query.all() # Get servers without subnets standalone_servers = Server.query.filter(Server.subnet_id.is_(None)).all() # Create a hierarchical structure hierarchy = { 'subnets': [], 'standalone_servers': [] } # Organize subnets and their servers for subnet in subnets: subnet_data = { 'id': subnet.id, 'cidr': subnet.cidr, 'description': subnet.description, 'location': subnet.location, 'servers': [] } for server in subnet.servers: server_data = { 'id': server.id, 'hostname': server.hostname, 'ip_address': server.ip_address, 'apps': [] } for app in server.apps: app_data = { 'id': app.id, 'name': app.name, 'ports': app.ports } server_data['apps'].append(app_data) subnet_data['servers'].append(server_data) hierarchy['subnets'].append(subnet_data) # Organize standalone servers for server in standalone_servers: server_data = { 'id': server.id, 'hostname': server.hostname, 'ip_address': server.ip_address, 'apps': [] } for app in server.apps: app_data = { 'id': app.id, 'name': app.name, 'ports': app.ports } server_data['apps'].append(app_data) hierarchy['standalone_servers'].append(server_data) return render_template( "dashboard/overview.html", title="Infrastructure Overview", hierarchy=hierarchy )