509 lines
17 KiB
Python
509 lines
17 KiB
Python
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/<int:server_id>")
|
|
@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/<int:server_id>/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/<int:server_id>/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/<int:server_id>", 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/<int:app_id>", 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/<int:app_id>/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/<int:app_id>/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
|
|
)
|