from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify, abort from flask_login import login_required, current_user import markdown from app.core.models import Server, App, Subnet, Port, Location from app.core.extensions import db, limiter from datetime import datetime from app.utils.app_utils import validate_app_data, is_port_in_use, validate_port_data 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""" # Get all subnets and locations for the current user subnets = Subnet.query.filter_by(user_id=current_user.id).all() locations = Location.query.filter_by(user_id=current_user.id).all() if request.method == "POST": hostname = request.form.get("hostname") ip_address = request.form.get("ip_address") subnet_id = request.form.get("subnet_id") or None location_id = request.form.get("location_id") or None description = request.form.get("description") # Validate inputs if not hostname or not ip_address: flash("Hostname and IP address are required", "danger") return render_template( "dashboard/server_form.html", title="New Server", subnets=subnets, locations=locations ) # If no subnet is selected, location is required if not subnet_id and not location_id: flash("Either a subnet or a location is required", "danger") return render_template( "dashboard/server_form.html", title="New Server", subnets=subnets, locations=locations ) # Create new server server = Server( hostname=hostname, ip_address=ip_address, subnet_id=subnet_id, location_id=location_id, description=description, user_id=current_user.id ) db.session.add(server) db.session.commit() flash(f"Server {hostname} 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, locations=locations ) @bp.route("/server//edit", methods=["GET", "POST"]) @login_required def server_edit(server_id): """Edit a server""" server = Server.query.get_or_404(server_id) subnets = Subnet.query.all() # Get all unique locations for datalist subnet_locations = db.session.query(Subnet.location).distinct().all() server_locations = db.session.query(Server.location).filter(Server.location != None).distinct().all() locations = sorted(set([loc[0] for loc in subnet_locations + server_locations if loc[0]])) if request.method == "POST": hostname = request.form.get("hostname") ip_address = request.form.get("ip_address") subnet_id = request.form.get("subnet_id") or None location = request.form.get("location") # Validate inputs if not hostname or not ip_address: flash("Hostname and IP address are required", "danger") return render_template( "dashboard/server_form.html", title="Edit Server", server=server, subnets=subnets, locations=locations, edit_mode=True ) # If no subnet is selected, location is required if not subnet_id and not location: flash("Location is required for servers without a subnet", "danger") return render_template( "dashboard/server_form.html", title="Edit Server", server=server, subnets=subnets, locations=locations, edit_mode=True ) # Update server server.hostname = hostname server.ip_address = ip_address server.subnet_id = subnet_id server.location = location if not subnet_id else None db.session.commit() flash(f"Server {hostname} updated successfully", "success") return redirect(url_for("dashboard.server_view", server_id=server.id)) return render_template( "dashboard/server_form.html", title="Edit Server", server=server, subnets=subnets, locations=locations, edit_mode=True ) @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", defaults={'server_id': None}, methods=["GET", "POST"]) @bp.route("/app/new/", methods=["GET", "POST"]) @login_required def app_new(server_id=None): """Create a new application for a server""" # Get all servers for the dropdown servers = Server.query.filter_by(user_id=current_user.id).all() # If server_id is provided, validate it selected_server = None if server_id: selected_server = Server.query.get_or_404(server_id) if selected_server.user_id != current_user.id: abort(403) if request.method == "POST": name = request.form.get("name") url = request.form.get("url", "") documentation = request.form.get("documentation", "") form_server_id = request.form.get("server_id") # Validate inputs if not name or not form_server_id: flash("Application name and server are required", "danger") return render_template( "dashboard/app_form.html", title="New Application", servers=servers, selected_server_id=form_server_id or server_id, edit_mode=False, app={} # Empty app object for the template ) # Verify the selected server belongs to the user server = Server.query.get_or_404(form_server_id) if server.user_id != current_user.id: abort(403) try: # Create new application app = App( name=name, server_id=form_server_id, user_id=current_user.id, # Set user_id explicitly documentation=documentation or "", url=url or "" ) db.session.add(app) db.session.commit() flash(f"Application {name} created successfully", "success") return redirect(url_for("dashboard.server_view", server_id=form_server_id)) except Exception as e: db.session.rollback() flash(f"Error creating application: {str(e)}", "danger") return render_template( "dashboard/app_form.html", title="New Application", servers=servers, selected_server_id=form_server_id or server_id, edit_mode=False, app={} # Empty app object for the template ) # GET request - show the form return render_template( "dashboard/app_form.html", title="New Application", servers=servers, selected_server_id=server_id, edit_mode=False, app={} # Empty app object for the template ) @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""" app = App.query.get_or_404(app_id) # Check that the current user owns this app if app.user_id != current_user.id: abort(403) # Get all servers for the dropdown servers = Server.query.filter_by(user_id=current_user.id).all() if request.method == "POST": name = request.form.get("name") url = request.form.get("url", "") documentation = request.form.get("documentation", "") server_id = request.form.get("server_id") port_data = request.form.getlist("port") port_descriptions = request.form.getlist("port_description") # Validate required fields if not name or not server_id: flash("Application name and server are required", "danger") return render_template("dashboard/app_form.html", app=app, servers=servers, edit_mode=True) # Validate port data - exclude current app's ports from conflict check port_error = validate_port_data(port_data, port_descriptions, server_id, app_id) if port_error: flash(port_error, "danger") return render_template("dashboard/app_form.html", app=app, servers=servers, edit_mode=True) try: # Update app details app.name = name app.server_id = server_id app.documentation = documentation app.url = url app.updated_at = datetime.utcnow() # Update ports # First, remove all existing ports Port.query.filter_by(app_id=app.id).delete() # Then add the new ports for i, port_number in enumerate(port_data): if not port_number: # Skip empty port entries continue description = port_descriptions[i] if i < len(port_descriptions) else "" port = Port( port_number=port_number, description=description, app_id=app.id ) db.session.add(port) db.session.commit() flash(f"Application {name} updated successfully", "success") return redirect(url_for("dashboard.server_view", server_id=server_id)) except Exception as e: db.session.rollback() flash(f"Error updating application: {str(e)}", "danger") return render_template("dashboard/app_form.html", app=app, servers=servers, edit_mode=True) # GET request - show the form return render_template("dashboard/app_form.html", app=app, servers=servers, edit_mode=True) @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(): """Display an overview of the infrastructure""" # Get all servers, subnets, apps for the current user servers = Server.query.filter_by(user_id=current_user.id).all() subnets = Subnet.query.filter_by(user_id=current_user.id).all() locations = Location.query.filter_by(user_id=current_user.id).all() # Create location lookup dictionary for quick access location_lookup = {loc.id: loc.name for loc in locations} # Count servers by subnet servers_by_subnet = {} unassigned_servers = [] # Process servers for server in servers: if server.subnet_id: if server.subnet_id not in servers_by_subnet: servers_by_subnet[server.subnet_id] = [] servers_by_subnet[server.subnet_id].append(server) else: unassigned_servers.append(server) # Prepare subnet data for the chart subnet_data = [] for subnet in subnets: # Get the location name using the location_id location_name = location_lookup.get(subnet.location_id, 'Unassigned') subnet_data.append({ 'id': subnet.id, 'cidr': subnet.cidr, 'location': location_name, 'server_count': len(servers_by_subnet.get(subnet.id, [])), 'servers': servers_by_subnet.get(subnet.id, []) }) # Count apps by server servers_with_app_count = [] for server in servers: app_count = len(server.apps) # Adjust this if needed based on your relationship servers_with_app_count.append({ 'id': server.id, 'hostname': server.hostname, 'ip_address': server.ip_address, 'app_count': app_count }) # Sort by app count descending servers_with_app_count.sort(key=lambda x: x['app_count'], reverse=True) return render_template( "dashboard/overview.html", subnet_count=len(subnets), server_count=len(servers), subnet_data=subnet_data, unassigned_servers=unassigned_servers, servers_with_app_count=servers_with_app_count[:5] # Top 5 servers ) @bp.route("/apps") @login_required def app_list(): """List all applications""" apps = App.query.order_by(App.name).all() return render_template( "dashboard/app_list.html", title="Applications", apps=apps )