This commit is contained in:
pika 2025-04-03 16:58:01 +02:00
parent 2b36992be1
commit 25087d055c
16 changed files with 1394 additions and 816 deletions

View file

@ -58,21 +58,14 @@ def create_app(config_name="development"):
print(f"Error with database setup: {e}") print(f"Error with database setup: {e}")
# Register blueprints # Register blueprints
from app.routes.auth import bp as auth_bp from app.routes import auth, dashboard, ipam
app.register_blueprint(auth.bp)
app.register_blueprint(auth_bp) app.register_blueprint(dashboard.bp)
app.register_blueprint(ipam.bp)
from app.routes.dashboard import bp as dashboard_bp
# Register API routes
app.register_blueprint(dashboard_bp) from app.routes import api
app.register_blueprint(api.bp)
from app.routes.ipam import bp as ipam_bp
app.register_blueprint(ipam_bp)
from app.routes.api import bp as api_bp
app.register_blueprint(api_bp)
from app.routes.importexport import bp as importexport_bp from app.routes.importexport import bp as importexport_bp

View file

@ -7,16 +7,22 @@ login_manager = LoginManager()
login_manager.login_view = "auth.login" login_manager.login_view = "auth.login"
class User(UserMixin, db.Model): class User(db.Model, UserMixin):
__tablename__ = "users" __tablename__ = "users"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, nullable=True) username = db.Column(db.String(64), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128), nullable=False) password_hash = db.Column(db.String(128))
is_admin = db.Column(db.Boolean, default=False) is_admin = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_seen = db.Column(db.DateTime, default=datetime.utcnow) last_seen = db.Column(db.DateTime, default=datetime.utcnow)
# User's assets
locations = db.relationship("Location", backref="owner", lazy=True, cascade="all, delete-orphan")
subnets = db.relationship("Subnet", backref="owner", lazy=True, cascade="all, delete-orphan")
servers = db.relationship("Server", backref="owner", lazy=True, cascade="all, delete-orphan")
apps = db.relationship("App", backref="owner", lazy=True, cascade="all, delete-orphan")
def __repr__(self): def __repr__(self):
return f"<User {self.username}>" return f"<User {self.username}>"

View file

@ -2,68 +2,49 @@ from app.core.extensions import db
import json import json
from datetime import datetime from datetime import datetime
import ipaddress import ipaddress
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin from flask_login import UserMixin
# Import User from auth instead of defining it here
# User model has been moved to app.core.auth from app.core.auth import User
# Import it from there instead if needed: from app.core.auth import User
class Port(db.Model): class Location(db.Model):
__tablename__ = "ports" __tablename__ = "locations"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
app_id = db.Column( name = db.Column(db.String(255), nullable=False)
db.Integer, db.ForeignKey("apps.id", ondelete="CASCADE"), nullable=False description = db.Column(db.Text, nullable=True)
) user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
port_number = db.Column(db.Integer, nullable=False)
protocol = db.Column(db.String(10), default="TCP") # TCP, UDP, etc.
description = db.Column(db.String(200))
# Relationship
app = db.relationship("App", back_populates="ports")
def __repr__(self):
return f"<Port {self.port_number}/{self.protocol}>"
class Server(db.Model):
__tablename__ = "servers"
id = db.Column(db.Integer, primary_key=True)
hostname = db.Column(db.String(64), nullable=False)
ip_address = db.Column(db.String(39), nullable=False) # IPv4 or IPv6
subnet_id = db.Column(db.Integer, db.ForeignKey("subnets.id"), nullable=False)
documentation = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column( updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
)
# Relationships # Relationships
subnet = db.relationship("Subnet", back_populates="servers") subnets = db.relationship("Subnet", backref="location_ref", lazy=True, cascade="all, delete-orphan")
apps = db.relationship("App", back_populates="server", cascade="all, delete-orphan") standalone_servers = db.relationship(
"Server",
primaryjoin="and_(Server.location_id==Location.id, Server.subnet_id==None)",
backref="location_ref",
lazy=True
)
def __repr__(self): def __repr__(self):
return f"<Server {self.hostname}>" return f"<Location {self.name}>"
class Subnet(db.Model): class Subnet(db.Model):
__tablename__ = "subnets" __tablename__ = "subnets"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
cidr = db.Column(db.String(18), unique=True, nullable=False) # e.g., 192.168.1.0/24 cidr = db.Column(db.String(45), nullable=False)
location = db.Column(db.String(64)) location_id = db.Column(db.Integer, db.ForeignKey("locations.id"), nullable=False)
active_hosts = db.Column(db.Text) # Store as JSON string user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
last_scanned = db.Column(db.DateTime)
auto_scan = db.Column(db.Boolean, default=False) auto_scan = db.Column(db.Boolean, default=False)
active_hosts = db.Column(db.Text, default='[]')
last_scanned = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column( updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
)
# Relationships # Relationships
servers = db.relationship("Server", back_populates="subnet") servers = db.relationship("Server", backref="subnet", lazy=True)
def __repr__(self): def __repr__(self):
return f"<Subnet {self.cidr}>" return f"<Subnet {self.cidr}>"
@ -85,22 +66,63 @@ class Subnet(db.Model):
self.active_hosts = json.dumps(hosts) self.active_hosts = json.dumps(hosts)
class Server(db.Model):
__tablename__ = "servers"
id = db.Column(db.Integer, primary_key=True)
hostname = db.Column(db.String(255), nullable=False)
ip_address = db.Column(db.String(45), nullable=False)
subnet_id = db.Column(db.Integer, db.ForeignKey("subnets.id"), nullable=True)
location_id = db.Column(db.Integer, db.ForeignKey("locations.id"), nullable=True)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
description = db.Column(db.Text, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
apps = db.relationship("App", back_populates="server", lazy=True, cascade="all, delete-orphan")
def __repr__(self):
return f"<Server {self.hostname}>"
class App(db.Model): class App(db.Model):
__tablename__ = "apps" __tablename__ = "apps"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), nullable=False) name = db.Column(db.String(64), nullable=False)
server_id = db.Column(db.Integer, db.ForeignKey("servers.id"), nullable=False) server_id = db.Column(db.Integer, db.ForeignKey("servers.id"), nullable=False)
documentation = db.Column(db.Text) user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
documentation = db.Column(db.Text, nullable=True)
url = db.Column(db.String(255), nullable=True) url = db.Column(db.String(255), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column( updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
)
# Relationships # Relationships
server = db.relationship("Server", back_populates="apps") server = db.relationship("Server", back_populates="apps")
ports = db.relationship("Port", back_populates="app", cascade="all, delete-orphan") ports = db.relationship("Port", back_populates="app", lazy=True, cascade="all, delete-orphan")
def __repr__(self): def __repr__(self):
return f"<App {self.name}>" return f"<App {self.name}>"
class Port(db.Model):
__tablename__ = "ports"
id = db.Column(db.Integer, primary_key=True)
app_id = db.Column(db.Integer, db.ForeignKey("apps.id"), nullable=False)
port_number = db.Column(db.Integer, nullable=False)
protocol = db.Column(db.String(10), nullable=False, default="TCP")
description = db.Column(db.String(255), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
app = db.relationship("App", back_populates="ports")
__table_args__ = (
db.UniqueConstraint('app_id', 'port_number', 'protocol', name='unique_port_per_app_protocol'),
)
def __repr__(self):
return f"<Port {self.port_number}/{self.protocol}>"

View file

@ -8,8 +8,8 @@ from flask import (
redirect, redirect,
url_for, url_for,
) )
from flask_login import login_required from flask_login import login_required, current_user
from app.core.models import Subnet, Server, App, Port from app.core.models import Subnet, Server, App, Port, Location
from app.core.extensions import db from app.core.extensions import db
from app.scripts.ip_scanner import scan from app.scripts.ip_scanner import scan
import random import random
@ -20,33 +20,51 @@ from datetime import datetime
from flask import flash from flask import flash
from app.utils.app_utils import is_port_in_use, validate_port_data from app.utils.app_utils import is_port_in_use, validate_port_data
from difflib import SequenceMatcher from difflib import SequenceMatcher
import json
bp = Blueprint("api", __name__, url_prefix="/api") bp = Blueprint("api", __name__, url_prefix="/api")
csrf = CSRFProtect() csrf = CSRFProtect()
@bp.route("/subnets", methods=["GET"]) @bp.route("/subnets", methods=["GET"])
@csrf.exempt
@login_required
def get_subnets(): def get_subnets():
"""Get all subnets grouped by site""" """Get all subnets grouped by site"""
subnets = Subnet.query.all() try:
subnets = Subnet.query.filter_by(user_id=current_user.id).all()
# Group subnets by location (site)
sites = {} # Group subnets by location (site)
for subnet in subnets: sites = {}
location = subnet.location for subnet in subnets:
if location not in sites: location = subnet.location_ref # Make sure this attribute matches your model relationship
sites[location] = [] if not location:
location_name = "Unassigned"
sites[location].append( location_id = None
{"id": subnet.id, "cidr": subnet.cidr, "location": location} else:
) location_name = location.name
location_id = location.id
# Convert to list of site objects
result = [ if location_id not in sites:
{"name": site_name, "subnets": subnets} for site_name, subnets in sites.items() sites[location_id] = {
] "name": location_name,
"id": location_id,
return jsonify(result) "subnets": []
}
sites[location_id]["subnets"].append({
"id": subnet.id,
"cidr": subnet.cidr,
"location_id": location_id
})
# Convert to list of site objects
result = list(sites.values())
return jsonify(result)
except Exception as e:
print(f"Error loading subnets: {e}") # Add debugging
return jsonify({"error": str(e)}), 500
@bp.route("/subnets/<int:subnet_id>", methods=["GET"]) @bp.route("/subnets/<int:subnet_id>", methods=["GET"])
@ -288,40 +306,60 @@ def add_app_port(app_id):
"""Add a port to an application""" """Add a port to an application"""
app = App.query.get_or_404(app_id) app = App.query.get_or_404(app_id)
# Check if the user owns this app
if app.user_id != current_user.id:
flash("Unauthorized access", "danger")
return redirect(url_for("dashboard.app_view", app_id=app_id))
# Get port details from the form # Get port details from the form
port_number = request.form.get("port_number") port_number = request.form.get("port_number")
protocol = request.form.get("protocol", "TCP") protocol = request.form.get("protocol", "TCP")
description = request.form.get("description", "") description = request.form.get("description", "")
# Validate the port # Validate the port
valid, clean_port, error = validate_port_data ( # validate_port_data( valid, clean_port, error = validate_port_data(
port_number, port_number,
protocol, descriptions=description,
description, server_id=app.server_id,
app.server_id, exclude_app_id=app_id,
app_id protocol=protocol
) )
if not valid: if not valid:
flash(error, "danger") flash(error, "danger")
return redirect(url_for("dashboard.app_view", app_id=app_id)) return redirect(url_for("dashboard.app_view", app_id=app_id))
# Create the new port
try: try:
new_port = Port( # Check if the port already exists for this app
existing_port = Port.query.filter_by(
app_id=app_id, app_id=app_id,
port_number=clean_port, port_number=clean_port,
protocol=protocol, protocol=protocol
description=description ).first()
)
db.session.add(new_port) if existing_port:
# Update the existing port description
existing_port.description = description
flash(f"Port {clean_port}/{protocol} updated", "success")
else:
# Create a new port
port = Port(
port_number=clean_port,
protocol=protocol,
description=description,
app_id=app_id
)
db.session.add(port)
flash(f"Port {clean_port}/{protocol} added successfully", "success")
db.session.commit() db.session.commit()
flash(f"Port {clean_port}/{protocol} added successfully", "success") return redirect(url_for("dashboard.app_view", app_id=app_id))
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
flash(f"Error adding port: {str(e)}", "danger") flash(f"Error adding port: {str(e)}", "danger")
return redirect(url_for("dashboard.app_view", app_id=app_id))
return redirect(url_for("dashboard.app_view", app_id=app_id))
@bp.route("/app/<int:app_id>/ports", methods=["GET"]) @bp.route("/app/<int:app_id>/ports", methods=["GET"])
@ -337,7 +375,7 @@ def get_app_ports(app_id):
"ports": [ "ports": [
{ {
"id": port.id, "id": port.id,
"number": port.number, "port_number": port.port_number,
"protocol": port.protocol, "protocol": port.protocol,
"description": port.description, "description": port.description,
} }
@ -396,7 +434,7 @@ def get_server_ports(server_id):
# Get all ports associated with this server # Get all ports associated with this server
ports = Port.query.filter_by(server_id=server_id).all() ports = Port.query.filter_by(server_id=server_id).all()
used_ports = [port.number for port in ports] used_ports = [port.port_number for port in ports]
return jsonify({"server_id": server_id, "used_ports": used_ports}) return jsonify({"server_id": server_id, "used_ports": used_ports})
@ -409,7 +447,7 @@ def get_free_port(server_id):
# Get all ports associated with this server # Get all ports associated with this server
used_ports = [ used_ports = [
port.number for port in Port.query.filter_by(server_id=server_id).all() port.port_number for port in Port.query.filter_by(server_id=server_id).all()
] ]
# Find the first free port (starting from 8000) # Find the first free port (starting from 8000)
@ -524,3 +562,65 @@ def validate_app_port():
except ValueError: except ValueError:
return jsonify({"valid": False, "message": "Invalid port number"}) return jsonify({"valid": False, "message": "Invalid port number"})
@bp.route("/locations", methods=["POST"])
@login_required
def create_location():
"""API endpoint to create a new location"""
data = request.json
if not data or not data.get('name'):
return jsonify({'error': 'Location name is required'}), 400
try:
location = Location(
name=data.get('name'),
description=data.get('description', ''),
user_id=current_user.id
)
db.session.add(location)
db.session.commit()
return jsonify({
'id': location.id,
'name': location.name,
'description': location.description
}), 201
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500
@bp.route("/subnets", methods=["POST"])
@login_required
def create_subnet():
"""API endpoint to create a new subnet"""
data = request.json
if not data or not data.get('cidr') or not data.get('location_id'):
return jsonify({'error': 'CIDR and location_id are required'}), 400
try:
subnet = Subnet(
cidr=data.get('cidr'),
location_id=data.get('location_id'),
user_id=current_user.id,
auto_scan=data.get('auto_scan', False),
active_hosts=json.dumps([])
)
db.session.add(subnet)
db.session.commit()
return jsonify({
'id': subnet.id,
'cidr': subnet.cidr,
'location_id': subnet.location_id
}), 201
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500

View file

@ -1,10 +1,10 @@
from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify, abort
from flask_login import login_required, current_user from flask_login import login_required, current_user
import markdown import markdown
from app.core.models import Server, App, Subnet, Port from app.core.models import Server, App, Subnet, Port, Location
from app.core.extensions import db, limiter from app.core.extensions import db, limiter
from datetime import datetime from datetime import datetime
from app.utils.app_utils import validate_app_data, is_port_in_use from app.utils.app_utils import validate_app_data, is_port_in_use, validate_port_data
bp = Blueprint("dashboard", __name__, url_prefix="/dashboard") bp = Blueprint("dashboard", __name__, url_prefix="/dashboard")
@ -77,130 +77,121 @@ def server_view(server_id):
@login_required @login_required
def server_new(): def server_new():
"""Create a new server""" """Create a new server"""
subnets = Subnet.query.all() # 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": if request.method == "POST":
hostname = request.form.get("hostname") hostname = request.form.get("hostname")
ip_address = request.form.get("ip_address") ip_address = request.form.get("ip_address")
subnet_id = request.form.get("subnet_id") subnet_id = request.form.get("subnet_id") or None
documentation = request.form.get("documentation", "") location_id = request.form.get("location_id") or None
description = request.form.get("description")
# Basic validation
if not hostname or not ip_address or not subnet_id: # Validate inputs
flash("Please fill in all required fields", "danger") if not hostname or not ip_address:
flash("Hostname and IP address are required", "danger")
return render_template( return render_template(
"dashboard/server_form.html", "dashboard/server_form.html",
title="New Server", title="New Server",
subnets=subnets, subnets=subnets,
now=datetime.now(), locations=locations
) )
# Check if hostname or IP already exists # If no subnet is selected, location is required
if Server.query.filter_by(hostname=hostname).first(): if not subnet_id and not location_id:
flash("Hostname already exists", "danger") flash("Either a subnet or a location is required", "danger")
return render_template( return render_template(
"dashboard/server_form.html", "dashboard/server_form.html",
title="New Server", title="New Server",
subnets=subnets, subnets=subnets,
now=datetime.now(), locations=locations
) )
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 # Create new server
server = Server( server = Server(
hostname=hostname, hostname=hostname,
ip_address=ip_address, ip_address=ip_address,
subnet_id=subnet_id, subnet_id=subnet_id,
documentation=documentation, location_id=location_id,
description=description,
user_id=current_user.id
) )
db.session.add(server) db.session.add(server)
db.session.commit() db.session.commit()
flash("Server created successfully", "success") flash(f"Server {hostname} created successfully", "success")
return redirect(url_for("dashboard.server_view", server_id=server.id)) return redirect(url_for("dashboard.server_view", server_id=server.id))
return render_template( return render_template(
"dashboard/server_form.html", "dashboard/server_form.html",
title="New Server", title="New Server",
subnets=subnets, subnets=subnets,
now=datetime.now(), locations=locations
) )
@bp.route("/server/<int:server_id>/edit", methods=["GET", "POST"]) @bp.route("/server/<int:server_id>/edit", methods=["GET", "POST"])
@login_required @login_required
def server_edit(server_id): def server_edit(server_id):
"""Edit an existing server""" """Edit a server"""
server = Server.query.get_or_404(server_id) server = Server.query.get_or_404(server_id)
subnets = Subnet.query.all() 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": if request.method == "POST":
hostname = request.form.get("hostname") hostname = request.form.get("hostname")
ip_address = request.form.get("ip_address") ip_address = request.form.get("ip_address")
subnet_id = request.form.get("subnet_id") subnet_id = request.form.get("subnet_id") or None
documentation = request.form.get("documentation", "") location = request.form.get("location")
if not hostname or not ip_address or not subnet_id: # Validate inputs
flash("All fields are required", "danger") if not hostname or not ip_address:
flash("Hostname and IP address are required", "danger")
return render_template( return render_template(
"dashboard/server_form.html", "dashboard/server_form.html",
title="Edit Server", title="Edit Server",
server=server, server=server,
subnets=subnets, subnets=subnets,
locations=locations,
edit_mode=True
) )
# Check if hostname changed and already exists # If no subnet is selected, location is required
if ( if not subnet_id and not location:
hostname != server.hostname flash("Location is required for servers without a subnet", "danger")
and Server.query.filter_by(hostname=hostname).first()
):
flash("Hostname already exists", "danger")
return render_template( return render_template(
"dashboard/server_form.html", "dashboard/server_form.html",
title="Edit Server", title="Edit Server",
server=server, server=server,
subnets=subnets, subnets=subnets,
locations=locations,
edit_mode=True
) )
# 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 # Update server
server.hostname = hostname server.hostname = hostname
server.ip_address = ip_address server.ip_address = ip_address
server.subnet_id = subnet_id server.subnet_id = subnet_id
server.documentation = documentation server.location = location if not subnet_id else None
db.session.commit() db.session.commit()
flash("Server updated successfully", "success") flash(f"Server {hostname} updated successfully", "success")
return redirect(url_for("dashboard.server_view", server_id=server.id)) return redirect(url_for("dashboard.server_view", server_id=server.id))
# GET request - show form with current values
return render_template( return render_template(
"dashboard/server_form.html", "dashboard/server_form.html",
title=f"Edit Server - {server.hostname}", title="Edit Server",
server=server, server=server,
subnets=subnets, subnets=subnets,
locations=locations,
edit_mode=True
) )
@ -221,120 +212,79 @@ def server_delete(server_id):
return redirect(url_for("dashboard.dashboard_home")) return redirect(url_for("dashboard.dashboard_home"))
@bp.route("/app/new", methods=["GET", "POST"]) @bp.route("/app/new", defaults={'server_id': None}, methods=["GET", "POST"])
@bp.route("/app/new/<int:server_id>", methods=["GET", "POST"]) @bp.route("/app/new/<int:server_id>", methods=["GET", "POST"])
@login_required @login_required
def app_new(server_id=None): def app_new(server_id=None):
"""Create a new application""" """Create a new application for a server"""
servers = Server.query.all() # 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": if request.method == "POST":
name = request.form.get("name", "").strip() name = request.form.get("name")
server_id = request.form.get("server_id")
documentation = request.form.get("documentation", "")
url = request.form.get("url", "") url = request.form.get("url", "")
documentation = request.form.get("documentation", "")
form_server_id = request.form.get("server_id")
# Validate application name # Validate inputs
if not name: if not name or not form_server_id:
flash("Application name is required", "danger") flash("Application name and server are required", "danger")
return render_template( return render_template(
"dashboard/app_form.html", "dashboard/app_form.html",
edit_mode=False, title="New Application",
servers=servers, servers=servers,
selected_server_id=server_id selected_server_id=form_server_id or server_id,
edit_mode=False,
app={} # Empty app object for the template
) )
# Check for duplicate application names on the same server # Verify the selected server belongs to the user
existing_app = App.query.filter_by(name=name, server_id=server_id).first() server = Server.query.get_or_404(form_server_id)
if existing_app: if server.user_id != current_user.id:
flash(f"An application with the name '{name}' already exists on this server", "danger") abort(403)
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: try:
new_app = App( # Create new application
app = App(
name=name, name=name,
server_id=server_id, server_id=form_server_id,
documentation=documentation, user_id=current_user.id, # Set user_id explicitly
url=url documentation=documentation or "",
url=url or ""
) )
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.add(app)
db.session.commit() db.session.commit()
flash(f"Application '{name}' created successfully", "success")
return redirect(url_for("dashboard.app_view", app_id=new_app.id)) flash(f"Application {name} created successfully", "success")
return redirect(url_for("dashboard.server_view", server_id=form_server_id))
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
flash(f"Error creating application: {str(e)}", "danger") 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 or validation failed - render the form # GET request - show the form
return render_template( return render_template(
"dashboard/app_form.html", "dashboard/app_form.html",
edit_mode=False, title="New Application",
servers=servers, servers=servers,
selected_server_id=server_id selected_server_id=server_id,
edit_mode=False,
app={} # Empty app object for the template
) )
@ -357,118 +307,71 @@ def app_view(app_id):
@bp.route("/app/<int:app_id>/edit", methods=["GET", "POST"]) @bp.route("/app/<int:app_id>/edit", methods=["GET", "POST"])
@login_required @login_required
def app_edit(app_id): def app_edit(app_id):
"""Edit an existing application with comprehensive error handling""" """Edit an existing application"""
app = App.query.get_or_404(app_id) app = App.query.get_or_404(app_id)
servers = Server.query.all()
# 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": if request.method == "POST":
# Get form data name = request.form.get("name")
name = request.form.get("name", "").strip() url = request.form.get("url", "")
server_id = request.form.get("server_id")
documentation = request.form.get("documentation", "") documentation = request.form.get("documentation", "")
url = request.form.get("url", "") # Get the URL server_id = request.form.get("server_id")
port_data = request.form.getlist("port")
# Validate application name port_descriptions = request.form.getlist("port_description")
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) # Validate required fields
existing_app = App.query.filter( if not name or not server_id:
App.name == name, flash("Application name and server are required", "danger")
App.server_id == server_id, return render_template("dashboard/app_form.html", app=app, servers=servers, edit_mode=True)
App.id != app_id
).first()
if existing_app: # Validate port data - exclude current app's ports from conflict check
flash(f"An application with the name '{name}' already exists on this server", "danger") port_error = validate_port_data(port_data, port_descriptions, server_id, app_id)
return render_template( if port_error:
"dashboard/app_form.html", flash(port_error, "danger")
edit_mode=True, return render_template("dashboard/app_form.html", app=app, servers=servers, edit_mode=True)
app=app,
servers=servers try:
) # Update app details
app.name = name
# Process port data from form app.server_id = server_id
port_data = [] app.documentation = documentation
port_numbers = request.form.getlist("port_numbers[]") app.url = url
protocols = request.form.getlist("protocols[]") app.updated_at = datetime.utcnow()
descriptions = request.form.getlist("port_descriptions[]")
# Update ports
for i in range(len(port_numbers)): # First, remove all existing ports
if port_numbers[i] and port_numbers[i].strip(): Port.query.filter_by(app_id=app.id).delete()
protocol = protocols[i] if i < len(protocols) else "TCP"
description = descriptions[i] if i < len(descriptions) else "" # Then add the new ports
port_data.append((port_numbers[i], protocol, description)) for i, port_number in enumerate(port_data):
if not port_number: # Skip empty port entries
# 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 continue
seen_ports.add(port_key)
description = port_descriptions[i] if i < len(port_descriptions) else ""
# Check if the port is in use by another application port = Port(
in_use, conflicting_app_name = is_port_in_use( port_number=port_number,
clean_port, protocol, server_id, exclude_app_id=app_id description=description,
app_id=app.id
) )
db.session.add(port)
if in_use:
conflicts.append((clean_port, protocol, f"Port {clean_port}/{protocol} is already in use by application '{conflicting_app_name}'")) db.session.commit()
except (ValueError, TypeError): flash(f"Application {name} updated successfully", "success")
continue return redirect(url_for("dashboard.server_view", server_id=server_id))
if conflicts: except Exception as e:
for conflict in conflicts: db.session.rollback()
flash(f"Conflict: {conflict[0]}/{conflict[1]} - {conflict[2]}", "danger") flash(f"Error updating application: {str(e)}", "danger")
return render_template( return render_template("dashboard/app_form.html", app=app, servers=servers, edit_mode=True)
"dashboard/app_form.html",
edit_mode=True, # GET request - show the form
app=app, return render_template("dashboard/app_form.html", app=app, servers=servers, edit_mode=True)
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/<int:app_id>/delete", methods=["POST"]) @bp.route("/app/<int:app_id>/delete", methods=["POST"])
@ -534,73 +437,73 @@ def settings():
@bp.route("/overview") @bp.route("/overview")
@login_required @login_required
def overview(): def overview():
"""Hierarchical overview of subnets, servers, and applications""" """Display an overview of the infrastructure"""
# Get all subnets with their servers # Get all servers, subnets, apps for the current user
subnets = Subnet.query.all() 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()
# Get servers without subnets # Create location lookup dictionary for quick access
standalone_servers = Server.query.filter(Server.subnet_id.is_(None)).all() location_lookup = {loc.id: loc.name for loc in locations}
# Create a hierarchical structure # Count servers by subnet
hierarchy = { servers_by_subnet = {}
'subnets': [], unassigned_servers = []
'standalone_servers': []
}
# Organize subnets and their 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: for subnet in subnets:
subnet_data = { # Get the location name using the location_id
location_name = location_lookup.get(subnet.location_id, 'Unassigned')
subnet_data.append({
'id': subnet.id, 'id': subnet.id,
'cidr': subnet.cidr, 'cidr': subnet.cidr,
'location': subnet.location, 'location': location_name,
'servers': [] 'server_count': len(servers_by_subnet.get(subnet.id, [])),
} 'servers': servers_by_subnet.get(subnet.id, [])
})
# Only add description if it exists as an attribute
if hasattr(subnet, 'description'):
subnet_data['description'] = subnet.description
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 # Count apps by server
for server in standalone_servers: servers_with_app_count = []
server_data = { 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, 'id': server.id,
'hostname': server.hostname, 'hostname': server.hostname,
'ip_address': server.ip_address, 'ip_address': server.ip_address,
'apps': [] 'app_count': app_count
} })
for app in server.apps: # Sort by app count descending
app_data = { servers_with_app_count.sort(key=lambda x: x['app_count'], reverse=True)
'id': app.id,
'name': app.name,
'ports': app.ports
}
server_data['apps'].append(app_data)
hierarchy['standalone_servers'].append(server_data)
return render_template( return render_template(
"dashboard/overview.html", "dashboard/overview.html",
title="Infrastructure Overview", subnet_count=len(subnets),
hierarchy=hierarchy 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
) )

View file

@ -1,6 +1,6 @@
from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify
from flask_login import login_required from flask_login import login_required, current_user
from app.core.models import Subnet, Server, App from app.core.models import Subnet, Server, App, Location
from app.core.extensions import db from app.core.extensions import db
from app.scripts.ip_scanner import scan from app.scripts.ip_scanner import scan
import ipaddress import ipaddress
@ -36,32 +36,24 @@ def ipam_home():
@login_required @login_required
def subnet_new(): def subnet_new():
"""Create a new subnet""" """Create a new subnet"""
# Get all locations for the dropdown
locations = Location.query.filter_by(user_id=current_user.id).all()
if request.method == "POST": if request.method == "POST":
cidr = request.form.get("cidr") cidr = request.form.get("cidr")
location = request.form.get("location") location_id = request.form.get("location_id")
auto_scan = request.form.get("auto_scan") == "on" auto_scan = request.form.get("auto_scan") == "on"
# Basic validation # Basic validation
if not cidr or not location: if not cidr or not location_id:
flash("Please fill in all required fields", "danger") flash("CIDR notation and location are required", "danger")
return render_template("ipam/subnet_form.html", title="New Subnet") return render_template("ipam/subnet_form.html", title="New Subnet", locations=locations)
# Validate CIDR format # Create new subnet
try:
ipaddress.ip_network(cidr, strict=False)
except ValueError:
flash("Invalid CIDR format", "danger")
return render_template("ipam/subnet_form.html", title="New Subnet")
# Check if CIDR already exists
if Subnet.query.filter_by(cidr=cidr).first():
flash("Subnet already exists", "danger")
return render_template("ipam/subnet_form.html", title="New Subnet")
# Create new subnet with JSON string for active_hosts, not a Python list
subnet = Subnet( subnet = Subnet(
cidr=cidr, cidr=cidr,
location=location, location_id=location_id,
user_id=current_user.id,
active_hosts=json.dumps([]), # Convert empty list to JSON string active_hosts=json.dumps([]), # Convert empty list to JSON string
last_scanned=None, last_scanned=None,
auto_scan=auto_scan, auto_scan=auto_scan,
@ -73,7 +65,7 @@ def subnet_new():
flash("Subnet created successfully", "success") flash("Subnet created successfully", "success")
return redirect(url_for("ipam.subnet_view", subnet_id=subnet.id)) return redirect(url_for("ipam.subnet_view", subnet_id=subnet.id))
return render_template("ipam/subnet_form.html", title="New Subnet") return render_template("ipam/subnet_form.html", title="New Subnet", locations=locations)
@bp.route("/subnet/<int:subnet_id>") @bp.route("/subnet/<int:subnet_id>")
@ -267,43 +259,39 @@ def subnet_create_ajax():
return jsonify({"success": False, "error": str(e)}) return jsonify({"success": False, "error": str(e)})
@bp.route("/location/<location>") @bp.route("/location/<int:location_id>")
@login_required @login_required
def location_overview(location): def location_overview(location_id):
"""View all subnets and servers in a specific location""" """View all subnets and servers in a specific location"""
# Get all subnets in this location # Get the location (ensure it belongs to the current user)
subnets = Subnet.query.filter_by(location=location).all() location = Location.query.filter_by(id=location_id, user_id=current_user.id).first_or_404()
# Get servers in these subnets
servers = []
for subnet in subnets:
subnet_servers = Server.query.filter_by(subnet_id=subnet.id).all()
servers.extend(subnet_servers)
# Create a hierarchical structure # Create a hierarchical structure
hierarchy = { hierarchy = {
'subnets': [], 'locations': {
'standalone_servers': [] location.name: {
'id': location.id,
'description': location.description,
'subnets': [],
'standalone_servers': []
}
}
} }
# Organize subnets and their servers # Organize subnets and their servers
for subnet in subnets: for subnet in location.subnets:
subnet_data = { subnet_data = {
'id': subnet.id, 'id': subnet.id,
'cidr': subnet.cidr, 'cidr': subnet.cidr,
'location': subnet.location,
'servers': [] 'servers': []
} }
# Only add description if it exists as an attribute
if hasattr(subnet, 'description'):
subnet_data['description'] = subnet.description
for server in subnet.servers: for server in subnet.servers:
server_data = { server_data = {
'id': server.id, 'id': server.id,
'hostname': server.hostname, 'hostname': server.hostname,
'ip_address': server.ip_address, 'ip_address': server.ip_address,
'description': server.description,
'apps': [] 'apps': []
} }
@ -311,17 +299,59 @@ def location_overview(location):
app_data = { app_data = {
'id': app.id, 'id': app.id,
'name': app.name, 'name': app.name,
'ports': app.ports 'url': app.url,
'ports': []
} }
for port in app.ports:
port_data = {
'id': port.id,
'number': port.port_number,
'protocol': port.protocol,
'description': port.description
}
app_data['ports'].append(port_data)
server_data['apps'].append(app_data) server_data['apps'].append(app_data)
subnet_data['servers'].append(server_data) subnet_data['servers'].append(server_data)
hierarchy['subnets'].append(subnet_data) hierarchy['locations'][location.name]['subnets'].append(subnet_data)
# Add standalone servers
for server in location.standalone_servers:
server_data = {
'id': server.id,
'hostname': server.hostname,
'ip_address': server.ip_address,
'description': server.description,
'apps': []
}
for app in server.apps:
app_data = {
'id': app.id,
'name': app.name,
'url': app.url,
'ports': []
}
for port in app.ports:
port_data = {
'id': port.id,
'number': port.port_number,
'protocol': port.protocol,
'description': port.description
}
app_data['ports'].append(port_data)
server_data['apps'].append(app_data)
hierarchy['locations'][location.name]['standalone_servers'].append(server_data)
return render_template( return render_template(
"dashboard/overview.html", "ipam/location_overview.html",
title=f"{location} Overview", title=f"{location.name} Overview",
hierarchy=hierarchy, hierarchy=hierarchy,
location=location location=location
) )

View file

@ -0,0 +1,56 @@
// API Functions for reuse across the application
const apiFunctions = {
// Create a new location
createLocation: function (name, description, csrfToken) {
return fetch('/api/locations', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
name: name,
description: description
})
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to create location');
}
return response.json();
});
},
// Create a new subnet
createSubnet: function (cidr, locationId, autoScan, csrfToken) {
return fetch('/api/subnets', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
cidr: cidr,
location_id: locationId,
auto_scan: autoScan
})
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to create subnet');
}
return response.json();
});
},
// Get all subnets
getSubnets: function () {
return fetch('/api/subnets')
.then(response => {
if (!response.ok) {
throw new Error('Failed to load subnets');
}
return response.json();
});
}
};

53
app/static/js/sidebar.js Normal file
View file

@ -0,0 +1,53 @@
// Function to load subnets in the sidebar
function loadSubnets() {
fetch('/api/subnets')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
// Update the sidebar with the fetched subnets
const subnetList = document.getElementById('subnet-list');
subnetList.innerHTML = ''; // Clear existing items
if (data.length === 0) {
subnetList.innerHTML = '<div class="nav-item"><span class="nav-link">No subnets found</span></div>';
return;
}
data.forEach(site => {
// Create site header if it has subnets
if (site.subnets && site.subnets.length > 0) {
const siteHeader = document.createElement('div');
siteHeader.className = 'nav-item';
siteHeader.innerHTML = `<span class="nav-link text-muted">${site.name || 'Unassigned'}</span>`;
subnetList.appendChild(siteHeader);
// Add each subnet under this site
site.subnets.forEach(subnet => {
const subnetItem = document.createElement('div');
subnetItem.className = 'nav-item';
subnetItem.innerHTML = `
<a href="/ipam/subnet/${subnet.id}" class="nav-link">
<span class="nav-link-icon">
<i class="ti ti-network"></i>
</span>
<span>${subnet.cidr}</span>
</a>
`;
subnetList.appendChild(subnetItem);
});
}
});
})
.catch(error => {
console.error('Error loading subnets:', error);
const subnetList = document.getElementById('subnet-list');
subnetList.innerHTML = '<div class="nav-item text-danger">Error loading subnets</div>';
});
}
// Call this function when the page loads
document.addEventListener('DOMContentLoaded', loadSubnets);

View file

@ -435,4 +435,4 @@
} }
}); });
</script> </script>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,97 @@
{% extends "layout.html" %}
{% block content %}
<div class="container-xl">
<div class="page-header d-print-none">
<div class="row align-items-center">
<div class="col">
<h2 class="page-title">
Applications
</h2>
</div>
<div class="col-auto ms-auto d-print-none">
<div class="btn-list">
<a href="{{ url_for('dashboard.app_new') }}" class="btn btn-primary d-none d-sm-inline-block">
<span class="ti ti-plus me-2"></span>
New Application
</a>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">All Applications</h3>
</div>
<div class="table-responsive">
<table class="table table-vcenter card-table">
<thead>
<tr>
<th>Name</th>
<th>Server</th>
<th>Ports</th>
<th>Created</th>
<th class="w-1"></th>
</tr>
</thead>
<tbody>
{% for app in apps %}
<tr>
<td>
<a href="{{ url_for('dashboard.app_view', app_id=app.id) }}">{{ app.name }}</a>
</td>
<td>
<a href="{{ url_for('dashboard.server_view', server_id=app.server.id) }}">
{{ app.server.hostname }}
</a>
</td>
<td>
{% if app.ports %}
{% for port in app.ports %}
<span class="badge bg-azure me-1">{{ port.port_number }}/{{ port.protocol }}</span>
{% endfor %}
{% else %}
<span class="text-muted">No ports defined</span>
{% endif %}
</td>
<td class="text-muted">{{ app.created_at.strftime('%Y-%m-%d') }}</td>
<td>
<div class="btn-list flex-nowrap">
<a href="{{ url_for('dashboard.app_edit', app_id=app.id) }}" class="btn btn-sm btn-outline-primary">
Edit
</a>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="text-center py-4">
<div class="empty">
<div class="empty-img">
<span class="ti ti-apps" style="font-size: 3rem;"></span>
</div>
<p class="empty-title">No applications found</p>
<p class="empty-subtitle text-muted">
Start by creating a new application.
</p>
<div class="empty-action">
<a href="{{ url_for('dashboard.app_new') }}" class="btn btn-primary">
<span class="ti ti-plus me-2"></span>
New Application
</a>
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -6,7 +6,7 @@
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col"> <div class="col">
<h2 class="page-title"> <h2 class="page-title">
Infrastructure Overview {{ title }}
</h2> </h2>
</div> </div>
<div class="col-auto ms-auto d-print-none"> <div class="col-auto ms-auto d-print-none">
@ -19,16 +19,11 @@
<span class="ti ti-plus me-2"></span> <span class="ti ti-plus me-2"></span>
New Server New Server
</a> </a>
<a href="{{ url_for('dashboard.app_new') }}" class="btn btn-primary d-none d-sm-inline-block">
<span class="ti ti-plus me-2"></span>
New Application
</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Hierarchical View -->
<div class="row mt-3"> <div class="row mt-3">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
@ -36,47 +31,115 @@
<h3 class="card-title">Network Infrastructure</h3> <h3 class="card-title">Network Infrastructure</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
<!-- Subnets --> {% if hierarchy.locations %}
{% if hierarchy.subnets %} {% for location_name, location_data in hierarchy.locations.items() %}
{% for subnet in hierarchy.subnets %} <div class="location-container mb-5">
<div class="subnet-container mb-4"> <div class="location-header d-flex align-items-center p-2 bg-primary-lt rounded">
<div class="subnet-header d-flex align-items-center p-2 bg-azure-lt rounded"> <span class="ti ti-building me-2"></span>
<span class="ti ti-network me-2"></span> <h3 class="m-0">{{ location_name }}</h3>
<h4 class="m-0">
<a href="{{ url_for('ipam.subnet_view', subnet_id=subnet.id) }}" class="text-reset">
{{ subnet.cidr }}
</a>
{% if subnet.location %}
<small class="text-muted ms-2">({{ subnet.location }})</small>
{% endif %}
{% if subnet.description is defined and subnet.description %}
<small class="text-muted ms-2">{{ subnet.description }}</small>
{% endif %}
</h4>
<div class="ms-auto">
<a href="{{ url_for('dashboard.server_new') }}?subnet_id={{ subnet.id }}"
class="btn btn-sm btn-outline-primary">
<span class="ti ti-plus me-1"></span> Add Server
</a>
</div>
</div> </div>
<!-- Servers in this subnet --> <!-- Subnets in this location -->
{% if subnet.servers %} {% if location_data.subnets %}
<div class="ps-4 mt-2"> <div class="ms-4 mt-3">
{% for server in subnet.servers %} <h4 class="mb-3">Subnets</h4>
<div class="server-container mb-3 border-start ps-3"> {% for subnet in location_data.subnets %}
<div class="server-header d-flex align-items-center p-2 bg-light rounded"> <div class="subnet-container mb-4">
<div class="subnet-header d-flex align-items-center p-2 bg-azure-lt rounded">
<span class="ti ti-network me-2"></span>
<h4 class="m-0">
<a href="{{ url_for('ipam.subnet_view', subnet_id=subnet.id) }}" class="text-reset">
{{ subnet.cidr }}
</a>
{% if subnet.description is defined and subnet.description %}
<small class="text-muted ms-2">{{ subnet.description }}</small>
{% endif %}
</h4>
<div class="ms-auto">
<a href="{{ url_for('dashboard.server_new') }}?subnet_id={{ subnet.id }}"
class="btn btn-sm btn-outline-primary">
<span class="ti ti-plus me-1"></span> Add Server
</a>
</div>
</div>
<!-- Servers in this subnet -->
{% if subnet.servers %}
<div class="ms-4 mt-2">
{% for server in subnet.servers %}
<div class="server-container mb-3">
<div class="server-header d-flex align-items-center p-2 bg-light rounded border-start border-3">
<span class="ti ti-server me-2"></span>
<h5 class="m-0">
<a href="{{ url_for('dashboard.server_view', server_id=server.id) }}" class="text-reset">
{{ server.hostname }}
</a>
<small class="text-muted ms-2">{{ server.ip_address }}</small>
</h5>
<div class="ms-auto">
<a href="{{ url_for('dashboard.app_new') }}?server_id={{ server.id }}"
class="btn btn-sm btn-outline-secondary">
<span class="ti ti-plus me-1"></span> Add App
</a>
</div>
</div>
<!-- Apps on this server -->
{% if server.apps %}
<div class="ms-4 mt-1">
{% for app in server.apps %}
<div class="app-container mb-2">
<div class="app-header d-flex align-items-center p-2 bg-white rounded border-start border-3">
<span class="ti ti-app-window me-2"></span>
<h6 class="m-0">
<a href="{{ url_for('dashboard.app_view', app_id=app.id) }}" class="text-reset">
{{ app.name }}
</a>
{% if app.ports %}
<small class="text-muted ms-2">
Ports:
{% for port in app.ports %}
<span class="badge bg-blue-lt">{{ port.port }}/{{ port.protocol }}</span>
{% endfor %}
</small>
{% endif %}
</h6>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<div class="ms-4 mt-2 text-muted">
<em>No servers in this subnet</em>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
<!-- Standalone servers in this location -->
{% if location_data.standalone_servers %}
<div class="ms-4 mt-3">
<h4 class="mb-3">Standalone Servers</h4>
{% for server in location_data.standalone_servers %}
<div class="server-container mb-3">
<div class="server-header d-flex align-items-center p-2 bg-light rounded border-start border-3">
<span class="ti ti-server me-2"></span> <span class="ti ti-server me-2"></span>
<h5 class="m-0"> <h5 class="m-0">
<a href="{{ url_for('dashboard.server_view', server_id=server.id) }}" class="text-reset"> <a href="{{ url_for('dashboard.server_view', server_id=server.id) }}" class="text-reset">
{{ server.hostname }} {{ server.hostname }}
</a> </a>
<small class="text-muted ms-2">{{ server.ip_address }}</small> <small class="text-muted ms-2">{{ server.ip_address }}</small>
<span class="badge bg-purple-lt ms-2">Public IP</span>
</h5> </h5>
<div class="ms-auto"> <div class="ms-auto">
<a href="{{ url_for('dashboard.app_new') }}?server_id={{ server.id }}" <a href="{{ url_for('dashboard.app_new') }}?server_id={{ server.id }}"
class="btn btn-sm btn-outline-primary"> class="btn btn-sm btn-outline-secondary">
<span class="ti ti-plus me-1"></span> Add App <span class="ti ti-plus me-1"></span> Add App
</a> </a>
</div> </div>
@ -84,102 +147,36 @@
<!-- Apps on this server --> <!-- Apps on this server -->
{% if server.apps %} {% if server.apps %}
<div class="ps-4 mt-2"> <div class="ms-4 mt-1">
{% for app in server.apps %} {% for app in server.apps %}
<div class="app-container mb-2 border-start ps-3"> <div class="app-container mb-2">
<div class="app-header d-flex align-items-center p-2 bg-light-lt rounded"> <div class="app-header d-flex align-items-center p-2 bg-white rounded border-start border-3">
<span class="ti ti-app-window me-2"></span> <span class="ti ti-app-window me-2"></span>
<h6 class="m-0"> <h6 class="m-0">
<a href="{{ url_for('dashboard.app_view', app_id=app.id) }}" class="text-reset"> <a href="{{ url_for('dashboard.app_view', app_id=app.id) }}" class="text-reset">
{{ app.name }} {{ app.name }}
</a> </a>
{% if app.ports %}
<small class="text-muted ms-2">
Ports:
{% for port in app.ports %}
<span class="badge bg-blue-lt">{{ port.port }}/{{ port.protocol }}</span>
{% endfor %}
</small>
{% endif %}
</h6> </h6>
<div class="ms-2">
{% for port in app.ports %}
<span class="badge bg-blue-lt">{{ port.port_number }}/{{ port.protocol }}</span>
{% endfor %}
</div>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% else %}
<div class="ps-4 mt-2">
<div class="text-muted fst-italic">No applications</div>
</div>
{% endif %} {% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% else %}
<div class="ps-4 mt-2">
<div class="text-muted fst-italic">No servers in this subnet</div>
</div>
{% endif %} {% endif %}
</div> </div>
{% endfor %} {% endfor %}
{% endif %} {% else %}
<!-- Standalone Servers -->
{% if hierarchy.standalone_servers %}
<div class="standalone-servers-container mb-4">
<div class="subnet-header d-flex align-items-center p-2 bg-yellow-lt rounded">
<span class="ti ti-server me-2"></span>
<h4 class="m-0">Standalone Servers</h4>
</div>
<div class="ps-4 mt-2">
{% for server in hierarchy.standalone_servers %}
<div class="server-container mb-3 border-start ps-3">
<div class="server-header d-flex align-items-center p-2 bg-light rounded">
<span class="ti ti-server me-2"></span>
<h5 class="m-0">
<a href="{{ url_for('dashboard.server_view', server_id=server.id) }}" class="text-reset">
{{ server.hostname }}
</a>
<small class="text-muted ms-2">{{ server.ip_address }}</small>
</h5>
<div class="ms-auto">
<a href="{{ url_for('dashboard.app_new') }}?server_id={{ server.id }}"
class="btn btn-sm btn-outline-primary">
<span class="ti ti-plus me-1"></span> Add App
</a>
</div>
</div>
<!-- Apps on this server -->
{% if server.apps %}
<div class="ps-4 mt-2">
{% for app in server.apps %}
<div class="app-container mb-2 border-start ps-3">
<div class="app-header d-flex align-items-center p-2 bg-light-lt rounded">
<span class="ti ti-app-window me-2"></span>
<h6 class="m-0">
<a href="{{ url_for('dashboard.app_view', app_id=app.id) }}" class="text-reset">
{{ app.name }}
</a>
</h6>
<div class="ms-2">
{% for port in app.ports %}
<span class="badge bg-blue-lt">{{ port.port_number }}/{{ port.protocol }}</span>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="ps-4 mt-2">
<div class="text-muted fst-italic">No applications</div>
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if not hierarchy.subnets and not hierarchy.standalone_servers %}
<div class="text-center py-4"> <div class="text-center py-4">
<div class="empty"> <div class="empty">
<div class="empty-img"> <div class="empty-img">
@ -209,6 +206,7 @@
</div> </div>
<style> <style>
.location-container,
.subnet-container, .subnet-container,
.server-container, .server-container,
.app-container { .app-container {
@ -224,12 +222,14 @@
border-left-color: var(--tblr-primary) !important; border-left-color: var(--tblr-primary) !important;
} }
.location-header,
.subnet-header, .subnet-header,
.server-header, .server-header,
.app-header { .app-header {
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.location-header:hover,
.subnet-header:hover, .subnet-header:hover,
.server-header:hover, .server-header:hover,
.app-header:hover { .app-header:hover {

View file

@ -6,163 +6,285 @@
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col"> <div class="col">
<h2 class="page-title"> <h2 class="page-title">
{% if server %}Edit Server{% else %}Add New Server{% endif %} {{ title }}
</h2> </h2>
</div> </div>
</div> </div>
</div> </div>
<div class="card mt-3"> <div class="row mt-3">
<div class="card-body"> <div class="col-12">
{% with messages = get_flashed_messages(with_categories=true) %} <div class="card">
{% if messages %} <div class="card-body">
{% for category, message in messages %} <form method="POST">
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{{ message }} <div class="mb-3">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <label for="hostname" class="form-label">Hostname</label>
</div> <input type="text" class="form-control" id="hostname" name="hostname" required
{% endfor %} value="{{ server.hostname if server else '' }}">
{% endif %} </div>
{% endwith %}
<form method="POST" <div class="mb-3">
action="{% if server %}{{ url_for('dashboard.server_edit', server_id=server.id) }}{% else %}{{ url_for('dashboard.server_new') }}{% endif %}"> <label for="ip_address" class="form-label">IP Address</label>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="text" class="form-control" id="ip_address" name="ip_address" required
<div class="mb-3"> value="{{ server.ip_address if server else '' }}">
<label class="form-label required">Hostname</label> </div>
<input type="text" class="form-control" name="hostname" required
value="{% if server %}{{ server.hostname }}{% endif %}"> <div class="mb-3">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<label for="subnet_id" class="form-label">Subnet (optional for public IPs)</label>
<select class="form-select" id="subnet_id" name="subnet_id">
<option value="">No subnet (standalone server)</option>
{% for subnet in subnets %}
<option value="{{ subnet.id }}" {% if server and server.subnet_id==subnet.id %}selected{% endif %}>
{{ subnet.cidr }} ({{ subnet.location_ref.name }})
</option>
{% endfor %}
</select>
</div>
<div class="ms-2 pt-4">
<button type="button" class="btn btn-outline-primary btn-icon" data-bs-toggle="modal"
data-bs-target="#add-subnet-modal">
<span class="ti ti-plus"></span>
</button>
</div>
</div>
</div>
<div class="mb-3">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<label for="location_id" class="form-label">Location (required for standalone servers)</label>
<select class="form-select" id="location_id" name="location_id">
<option value="">Select a location</option>
{% for location in locations %}
<option value="{{ location.id }}" {% if server and server.location_id==location.id %}selected{%
endif %}>
{{ location.name }}
</option>
{% endfor %}
</select>
</div>
<div class="ms-2 pt-4">
<button type="button" class="btn btn-outline-primary btn-icon" data-bs-toggle="modal"
data-bs-target="#add-location-modal">
<span class="ti ti-plus"></span>
</button>
</div>
</div>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description (optional)</label>
<textarea class="form-control" id="description" name="description"
rows="3">{{ server.description if server else '' }}</textarea>
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary">Save</button>
<a href="{{ url_for('dashboard.server_list') }}" class="btn btn-link">Cancel</a>
</div>
</form>
</div> </div>
<div class="mb-3"> </div>
<label class="form-label required">IP Address</label>
<input type="text" class="form-control" name="ip_address" placeholder="192.168.1.10" required
value="{% if server %}{{ server.ip_address }}{% endif %}">
</div>
<div class="mb-3">
<label class="form-label required">Subnet</label>
<div class="input-group">
<select class="form-select" name="subnet_id" required id="subnet-select">
<option value="">Select a subnet</option>
{% for subnet in subnets %}
<option value="{{ subnet.id }}" {% if server and server.subnet_id==subnet.id %}selected{% endif %}>
{{ subnet.cidr }} ({{ subnet.location }})
</option>
{% endfor %}
</select>
<button type="button" class="btn btn-outline-primary" data-bs-toggle="modal"
data-bs-target="#quickSubnetModal">
<span class="ti ti-plus"></span>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">Documentation</label>
<textarea class="form-control" name="documentation"
rows="10">{% if server %}{{ server.documentation }}{% endif %}</textarea>
<div class="form-text">Markdown is supported</div>
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary">Save</button>
{% if server %}
<a href="{{ url_for('dashboard.server_view', server_id=server.id) }}"
class="btn btn-outline-secondary ms-2">Cancel</a>
{% else %}
<a href="{{ url_for('dashboard.dashboard_home') }}" class="btn btn-outline-secondary ms-2">Cancel</a>
{% endif %}
</div>
</form>
</div> </div>
</div> </div>
</div> </div>
<!-- Quick Subnet Creation Modal --> <!-- Add Subnet Modal -->
<div class="modal fade" id="quickSubnetModal" tabindex="-1" aria-labelledby="quickSubnetModalLabel" aria-hidden="true"> <div class="modal modal-blur fade" id="add-subnet-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="quickSubnetModalLabel">Quick Subnet Creation</h5> <h5 class="modal-title">Add New Subnet</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="quickSubnetForm"> <form id="add-subnet-form">
<div class="mb-3"> <div class="row mb-3">
<label class="form-label required">IP Address</label> <div class="col-md-8">
<input type="text" class="form-control" id="subnet-ip" placeholder="192.168.1.0" required> <label class="form-label required">IP Address</label>
<input type="text" class="form-control" id="new-subnet-ip" placeholder="192.168.1.0" required>
</div>
<div class="col-md-4">
<label class="form-label required">Prefix</label>
<select class="form-select" id="new-subnet-prefix" required>
{% for i in range(8, 31) %}
<option value="{{ i }}">{{ i }} ({{ 2**(32-i) }} hosts)</option>
{% endfor %}
</select>
</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label required">Prefix</label> <label class="form-label required">Location</label>
<select class="form-select" id="subnet-prefix" required> <select class="form-select" id="new-subnet-location" required>
{% for i in range(8, 31) %} <option value="">Select a location</option>
<option value="{{ i }}" {% if i==24 %}selected{% endif %}>{{ i }}</option> {% for location in locations %}
<option value="{{ location.id }}">{{ location.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label required">Location</label> <div class="form-check">
<input type="text" class="form-control" id="subnet-location" required> <input class="form-check-input" type="checkbox" id="new-subnet-auto-scan">
<label class="form-check-label" for="new-subnet-auto-scan">
Auto-scan for active hosts
</label>
</div>
</div> </div>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> <button type="button" class="btn btn-link link-secondary" data-bs-dismiss="modal">
<button type="button" class="btn btn-primary" id="createSubnetBtn">Create</button> Cancel
</button>
<button type="button" class="btn btn-primary ms-auto" id="save-subnet-btn">
<span class="ti ti-plus me-2"></span>
Add Subnet
</button>
</div>
</div>
</div>
</div>
<!-- Add Location Modal -->
<div class="modal modal-blur fade" id="add-location-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add New Location</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="add-location-form">
<div class="mb-3">
<label class="form-label required">Name</label>
<input type="text" class="form-control" id="new-location-name" required>
</div>
<div class="mb-3">
<label class="form-label">Description (optional)</label>
<textarea class="form-control" id="new-location-description" rows="3"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link link-secondary" data-bs-dismiss="modal">
Cancel
</button>
<button type="button" class="btn btn-primary ms-auto" id="save-location-btn">
<span class="ti ti-plus me-2"></span>
Add Location
</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Add this JavaScript at the end -->
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const createSubnetBtn = document.getElementById('createSubnetBtn'); const subnetSelect = document.getElementById('subnet_id');
const locationField = document.querySelector('.mb-3:has(#location_id)');
createSubnetBtn.addEventListener('click', function () { function updateLocationVisibility() {
const ip = document.getElementById('subnet-ip').value; if (subnetSelect.value === '') {
const prefix = document.getElementById('subnet-prefix').value; locationField.style.display = 'block';
const location = document.getElementById('subnet-location').value; document.getElementById('location_id').setAttribute('required', 'required');
} else {
// Validate inputs locationField.style.display = 'none';
if (!ip || !prefix || !location) { document.getElementById('location_id').removeAttribute('required');
alert('All fields are required');
return;
} }
}
// Create the subnet via AJAX // Initial state
fetch('/ipam/subnet/create-ajax', { if (locationField) {
method: 'POST', updateLocationVisibility();
headers: { // Update on change
'Content-Type': 'application/json', subnetSelect.addEventListener('change', updateLocationVisibility);
'X-CSRFToken': document.querySelector('input[name="csrf_token"]').value }
},
body: JSON.stringify({
cidr: `${ip}/${prefix}`,
location: location,
auto_scan: false
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Add the new subnet to the dropdown
const selectElement = document.getElementById('subnet-select');
const option = document.createElement('option');
option.value = data.subnet_id;
option.text = `${data.cidr} (${data.location})`;
option.selected = true;
selectElement.appendChild(option);
// Close the modal // Add subnet functionality
const modal = bootstrap.Modal.getInstance(document.getElementById('quickSubnetModal')); const saveSubnetBtn = document.getElementById('save-subnet-btn');
modal.hide(); const addSubnetModal = document.getElementById('add-subnet-modal');
} else {
alert(data.error || 'Failed to create subnet'); if (saveSubnetBtn && addSubnetModal) {
} const bsSubnetModal = new bootstrap.Modal(addSubnetModal);
})
.catch(error => { saveSubnetBtn.addEventListener('click', function () {
console.error('Error:', error); const ip = document.getElementById('new-subnet-ip').value.trim();
alert('An error occurred. Please try again.'); const prefix = document.getElementById('new-subnet-prefix').value;
}); const locationId = document.getElementById('new-subnet-location').value;
}); const autoScan = document.getElementById('new-subnet-auto-scan').checked;
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
if (!ip || !prefix || !locationId) {
alert('All fields are required');
return;
}
apiFunctions.createSubnet(`${ip}/${prefix}`, locationId, autoScan, csrfToken)
.then(data => {
// Add new option to select dropdown
const locationName = document.querySelector(`#new-subnet-location option[value="${locationId}"]`).textContent;
const newOption = new Option(`${data.cidr} (${locationName})`, data.id, true, true);
subnetSelect.add(newOption);
// Reset form and close modal
document.getElementById('new-subnet-ip').value = '';
document.getElementById('new-subnet-prefix').value = '24';
document.getElementById('new-subnet-location').value = '';
document.getElementById('new-subnet-auto-scan').checked = false;
bsSubnetModal.hide();
// Trigger the subnet change event to hide location if needed
const event = new Event('change');
subnetSelect.dispatchEvent(event);
})
.catch(error => {
console.error('Error:', error);
alert('Failed to create subnet: ' + error.message);
});
});
}
// Add location functionality
const saveLocationBtn = document.getElementById('save-location-btn');
const locationSelect = document.getElementById('location_id');
const addLocationModal = document.getElementById('add-location-modal');
if (saveLocationBtn && locationSelect && addLocationModal) {
const bsLocationModal = new bootstrap.Modal(addLocationModal);
saveLocationBtn.addEventListener('click', function () {
const name = document.getElementById('new-location-name').value.trim();
const description = document.getElementById('new-location-description').value.trim();
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
if (!name) {
alert('Location name is required');
return;
}
apiFunctions.createLocation(name, description, csrfToken)
.then(data => {
// Add new option to select dropdown
const newOption = new Option(data.name, data.id, true, true);
locationSelect.add(newOption);
// Reset form and close modal
document.getElementById('new-location-name').value = '';
document.getElementById('new-location-description').value = '';
bsLocationModal.hide();
})
.catch(error => {
console.error('Error:', error);
alert('Failed to create location: ' + error.message);
});
});
}
}); });
</script> </script>
{% endblock %} {% endblock %}

View file

@ -52,9 +52,26 @@
<input type="hidden" name="cidr" id="cidr-value" value="{% if subnet %}{{ subnet.cidr }}{% endif %}"> <input type="hidden" name="cidr" id="cidr-value" value="{% if subnet %}{{ subnet.cidr }}{% endif %}">
<div class="mb-3"> <div class="mb-3">
<label class="form-label required">Location</label> <div class="d-flex align-items-center">
<input type="text" class="form-control" name="location" required <div class="flex-grow-1">
value="{% if subnet %}{{ subnet.location }}{% endif %}"> <label for="location_id" class="form-label required">Location</label>
<select class="form-select" id="location_id" name="location_id" required>
<option value="">Select a location</option>
{% for location in locations %}
<option value="{{ location.id }}" {% if subnet and subnet.location_id==location.id %}selected{% endif
%}>
{{ location.name }}
</option>
{% endfor %}
</select>
</div>
<div class="ms-2 pt-4">
<button type="button" class="btn btn-outline-primary btn-icon" data-bs-toggle="modal"
data-bs-target="#add-location-modal">
<span class="ti ti-plus"></span>
</button>
</div>
</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@ -77,48 +94,154 @@
</div> </div>
</div> </div>
<!-- Add Location Modal -->
<div class="modal modal-blur fade" id="add-location-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add New Location</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="add-location-form">
<div class="mb-3">
<label class="form-label required">Location Name</label>
<input type="text" class="form-control" id="new-location-name" required>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea class="form-control" id="new-location-description" rows="3"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link link-secondary" data-bs-dismiss="modal">
Cancel
</button>
<button type="button" class="btn btn-primary ms-auto" id="save-location-btn">
<span class="ti ti-plus me-2"></span>
Add Location
</button>
</div>
</div>
</div>
</div>
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
// Handle CIDR combination
const ipAddressInput = document.getElementById('ip-address'); const ipAddressInput = document.getElementById('ip-address');
const prefixSelect = document.getElementById('prefix'); const prefixSelect = document.getElementById('prefix');
const cidrValueInput = document.getElementById('cidr-value'); const cidrValue = document.getElementById('cidr-value');
// Function to update the hidden CIDR field function updateCIDR() {
function updateCidrValue() {
const ip = ipAddressInput.value.trim(); const ip = ipAddressInput.value.trim();
const prefix = prefixSelect.value; const prefix = prefixSelect.value;
if (ip) { if (ip) {
cidrValueInput.value = `${ip}/${prefix}`; cidrValue.value = `${ip}/${prefix}`;
} }
} }
// Add event listeners if (ipAddressInput && prefixSelect && cidrValue) {
ipAddressInput.addEventListener('input', updateCidrValue); ipAddressInput.addEventListener('input', updateCIDR);
prefixSelect.addEventListener('change', updateCidrValue); prefixSelect.addEventListener('change', updateCIDR);
}
// Initialize CIDR value if editing // Add location functionality
if (cidrValueInput.value) { const saveLocationBtn = document.getElementById('save-location-btn');
const parts = cidrValueInput.value.split('/'); const locationSelect = document.getElementById('location_id');
if (parts.length === 2) { const addLocationModal = document.getElementById('add-location-modal');
ipAddressInput.value = parts[0];
const prefix = parseInt(parts[1]); if (saveLocationBtn && locationSelect && addLocationModal) {
if (!isNaN(prefix) && prefix >= 8 && prefix <= 30) { const bsModal = new bootstrap.Modal(addLocationModal);
prefixSelect.value = prefix;
saveLocationBtn.addEventListener('click', function () {
const name = document.getElementById('new-location-name').value.trim();
const description = document.getElementById('new-location-description').value.trim();
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
if (!name) {
alert('Location name is required');
return;
} }
apiFunctions.createLocation(name, description, csrfToken)
.then(data => {
// Add new option to select dropdown
const newOption = new Option(data.name, data.id, true, true);
locationSelect.add(newOption);
// Reset form and close modal
document.getElementById('new-location-name').value = '';
document.getElementById('new-location-description').value = '';
bsModal.hide();
})
.catch(error => {
console.error('Error:', error);
alert('Failed to create location: ' + error.message);
});
});
}
});
</script>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function () {
// Handle CIDR combination
const ipAddressInput = document.getElementById('ip-address');
const prefixSelect = document.getElementById('prefix');
const cidrValue = document.getElementById('cidr-value');
function updateCIDR() {
const ip = ipAddressInput.value.trim();
const prefix = prefixSelect.value;
if (ip) {
cidrValue.value = `${ip}/${prefix}`;
} }
} }
// Ensure form submission updates the CIDR value if (ipAddressInput && prefixSelect && cidrValue) {
document.querySelector('form').addEventListener('submit', function (e) { ipAddressInput.addEventListener('input', updateCIDR);
updateCidrValue(); prefixSelect.addEventListener('change', updateCIDR);
}
// Basic validation // Add location functionality
if (!cidrValueInput.value) { const saveLocationBtn = document.getElementById('save-location-btn');
e.preventDefault(); const locationSelect = document.getElementById('location_id');
alert('Please enter a valid IP address and prefix'); const addLocationModal = document.getElementById('add-location-modal');
}
}); if (saveLocationBtn && locationSelect && addLocationModal) {
const bsModal = new bootstrap.Modal(addLocationModal);
saveLocationBtn.addEventListener('click', function () {
const name = document.getElementById('new-location-name').value.trim();
const description = document.getElementById('new-location-description').value.trim();
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
if (!name) {
alert('Location name is required');
return;
}
apiFunctions.createLocation(name, description, csrfToken)
.then(data => {
// Add new option to select dropdown
const newOption = new Option(data.name, data.id, true, true);
locationSelect.add(newOption);
// Reset form and close modal
document.getElementById('new-location-name').value = '';
document.getElementById('new-location-description').value = '';
bsModal.hide();
})
.catch(error => {
console.error('Error:', error);
alert('Failed to create location: ' + error.message);
});
});
}
}); });
</script> </script>
{% endblock %} {% endblock %}

View file

@ -35,62 +35,102 @@ def validate_app_data(name, server_id, existing_app_id=None):
return True, None return True, None
def is_port_in_use(port_number, protocol, server_id, exclude_app_id=None): def is_port_in_use(port, server_id, exclude_app_id=None):
""" """
Check if a port+protocol combination is already in use by any application on the server Check if a port is already in use on a server
"""
from sqlalchemy import and_
# Join App and Port models to find ports used by apps on this server
query = db.session.query(Port, App).join(App).filter(
Port.port_number == port_number,
Port.protocol == protocol,
App.server_id == server_id
)
# # Exclude the current app if editing
# if exclude_app_id:
# query = query.filter(App.id != exclude_app_id)
result = query.first()
if result:
return True, result.App.name
return False, None
def validate_port_data(port_number, protocol, description, server_id=None, app_id=None):
"""
Validate port data for an application
Args: Args:
port_number: The port number to validate port: The port number to check
protocol: The protocol (TCP/UDP) server_id: The ID of the server
description: Port description exclude_app_id: Optional app ID to exclude from the check (for editing an app)
server_id: ID of the server
app_id: ID of the application (for excluding the current app when checking conflicts) 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: Returns:
Tuple of (valid, clean_port, error_message) For form validation: Error message string or None if valid
For API validation: (valid, clean_port, error_message) tuple
""" """
# Check if port number is provided # Handle the API call format
if not port_number: if protocol is not None:
return False, None, "Port number is required" # This is the API validation path
try:
try: port = int(ports)
clean_port = int(port_number) if port < 1 or port > 65535:
except ValueError: return False, None, f"Port {port} is out of valid range (1-65535)"
return False, None, "Port number must be a valid integer"
# Check if port is already in use
if clean_port < 1 or clean_port > 65535: if is_port_in_use(port, server_id, exclude_app_id):
return False, None, "Port number must be between 1 and 65535" return False, None, f"Port {port} is already in use on this server"
# Always check for port conflicts return True, port, None
in_use, app_name = is_port_in_use(clean_port, protocol, server_id, app_id) except ValueError:
if in_use: return False, None, "Invalid port number"
return False, clean_port, f"Port {clean_port}/{protocol} is already in use by application '{app_name}'"
# Handle the form submission format (list of ports)
return True, clean_port, None 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): def process_app_ports(app_id, port_data, server_id=None):
@ -128,7 +168,7 @@ def process_app_ports(app_id, port_data, server_id=None):
# Validate the port data # Validate the port data
valid, clean_port, error = validate_port_data( valid, clean_port, error = validate_port_data(
port_number, protocol, description, server_id, app_id [port_number], [description], server_id, app_id
) )
if not valid: if not valid:

79
run.py
View file

@ -4,7 +4,7 @@ import importlib.util
from flask import Flask, render_template from flask import Flask, render_template
from app import create_app from app import create_app
from app.core.extensions import db from app.core.extensions import db
from app.core.models import Server, Subnet, App, Port from app.core.models import Server, Subnet, App, Port, Location
from app.core.auth import User # Import User from auth module from app.core.auth import User # Import User from auth module
from datetime import datetime from datetime import datetime
import random import random
@ -93,6 +93,7 @@ def make_shell_context():
"Subnet": Subnet, "Subnet": Subnet,
"App": App, "App": App,
"Port": Port, "Port": Port,
"Location": Location,
} }
@ -118,63 +119,95 @@ def seed_data():
"""Add some sample data to the database""" """Add some sample data to the database"""
with app.app_context(): with app.app_context():
# Only seed if the database is empty # Only seed if the database is empty
if Subnet.query.count() == 0: if User.query.count() == 0:
# Create a default admin user
admin = User(username="admin", email="admin@example.com", is_admin=True)
admin.set_password("admin")
db.session.add(admin)
db.session.commit()
# Create sample locations
office = Location(
name="Office",
description="Main office network",
user_id=admin.id
)
datacenter = Location(
name="Datacenter",
description="Datacenter network",
user_id=admin.id
)
db.session.add_all([office, datacenter])
db.session.commit()
# Create sample subnets # Create sample subnets
subnet1 = Subnet( subnet1 = Subnet(
cidr="192.168.1.0/24", location="Office", active_hosts=json.dumps([]) cidr="192.168.1.0/24",
location_id=office.id,
user_id=admin.id,
active_hosts=json.dumps([])
) )
subnet2 = Subnet( subnet2 = Subnet(
cidr="10.0.0.0/24", location="Datacenter", active_hosts=json.dumps([]) cidr="10.0.0.0/24",
location_id=datacenter.id,
user_id=admin.id,
active_hosts=json.dumps([])
) )
db.session.add_all([subnet1, subnet2]) db.session.add_all([subnet1, subnet2])
db.session.commit() db.session.commit()
# Create sample servers # Create sample servers
server1 = Server( server1 = Server(
hostname="web-server", ip_address="192.168.1.10", subnet=subnet1 hostname="web-server",
ip_address="192.168.1.10",
subnet_id=subnet1.id,
user_id=admin.id
) )
server2 = Server( server2 = Server(
hostname="db-server", ip_address="192.168.1.11", subnet=subnet1 hostname="db-server",
ip_address="192.168.1.11",
subnet_id=subnet1.id,
user_id=admin.id
) )
server3 = Server( server3 = Server(
hostname="app-server", ip_address="10.0.0.5", subnet=subnet2 hostname="app-server",
ip_address="10.0.0.5",
subnet_id=subnet2.id,
user_id=admin.id
) )
db.session.add_all([server1, server2, server3]) db.session.add_all([server1, server2, server3])
db.session.commit() db.session.commit()
# Create sample apps # Create sample apps
app1 = App( app1 = App(
name="Website", name="Website",
server=server1, server_id=server1.id,
user_id=admin.id,
documentation="# Company Website\nRunning on Nginx/PHP", documentation="# Company Website\nRunning on Nginx/PHP",
) )
app2 = App( app2 = App(
name="PostgreSQL", name="PostgreSQL",
server=server2, server_id=server2.id,
user_id=admin.id,
documentation="# Database Server\nPostgreSQL 15", documentation="# Database Server\nPostgreSQL 15",
) )
app3 = App( app3 = App(
name="API Service", name="API Service",
server=server3, server_id=server3.id,
user_id=admin.id,
documentation="# REST API\nNode.js service", documentation="# REST API\nNode.js service",
) )
db.session.add_all([app1, app2, app3]) db.session.add_all([app1, app2, app3])
db.session.commit() db.session.commit()
# Create sample ports # Create sample ports
port1 = Port(app=app1, port_number=80, protocol="TCP", description="HTTP") ports = [
port2 = Port(app=app1, port_number=443, protocol="TCP", description="HTTPS") Port(app_id=app1.id, port_number=80, protocol="TCP", description="HTTP"),
port3 = Port( Port(app_id=app1.id, port_number=443, protocol="TCP", description="HTTPS"),
app=app2, port_number=5432, protocol="TCP", description="PostgreSQL" Port(app_id=app2.id, port_number=5432, protocol="TCP", description="PostgreSQL"),
) Port(app_id=app3.id, port_number=3000, protocol="TCP", description="Node.js API")
port4 = Port( ]
app=app3, port_number=3000, protocol="TCP", description="Node.js API" db.session.add_all(ports)
)
db.session.add_all([port1, port2, port3, port4])
db.session.commit() db.session.commit()
print("Sample data has been added to the database") print("Sample data has been added to the database")

View file

@ -19,4 +19,4 @@ application = app
if __name__ == "__main__": if __name__ == "__main__":
# Only for development # Only for development
debug = flask_env != "production" debug = flask_env != "production"
app.run(host="0.0.0.0", port=8000, debug=debug) app.run(host="0.0.0.0", port=5001, debug=debug)