wip
This commit is contained in:
parent
3b2f1db4ce
commit
5c16964b76
47 changed files with 2080 additions and 1053 deletions
Binary file not shown.
|
@ -2,21 +2,21 @@ from flask import Flask, g, redirect, url_for, render_template
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
|
|
||||||
def create_app(config_name='development'):
|
|
||||||
app = Flask(__name__,
|
def create_app(config_name="development"):
|
||||||
static_folder='static',
|
app = Flask(__name__, static_folder="static", template_folder="templates")
|
||||||
template_folder='templates')
|
|
||||||
|
|
||||||
# Load configuration
|
# Load configuration
|
||||||
if config_name == 'production':
|
if config_name == "production":
|
||||||
app.config.from_object('config.ProductionConfig')
|
app.config.from_object("config.ProductionConfig")
|
||||||
elif config_name == 'testing':
|
elif config_name == "testing":
|
||||||
app.config.from_object('config.TestingConfig')
|
app.config.from_object("config.TestingConfig")
|
||||||
else:
|
else:
|
||||||
app.config.from_object('config.DevelopmentConfig')
|
app.config.from_object("config.DevelopmentConfig")
|
||||||
|
|
||||||
# Initialize extensions
|
# Initialize extensions
|
||||||
from app.core.extensions import db, migrate, login_manager, bcrypt, limiter, csrf
|
from app.core.extensions import db, migrate, login_manager, bcrypt, limiter, csrf
|
||||||
|
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
migrate.init_app(app, db)
|
migrate.init_app(app, db)
|
||||||
login_manager.init_app(app)
|
login_manager.init_app(app)
|
||||||
|
@ -31,12 +31,13 @@ def create_app(config_name='development'):
|
||||||
def load_user(user_id):
|
def load_user(user_id):
|
||||||
return User.query.get(int(user_id))
|
return User.query.get(int(user_id))
|
||||||
|
|
||||||
login_manager.login_view = 'auth.login'
|
login_manager.login_view = "auth.login"
|
||||||
login_manager.login_message = 'Please log in to access this page.'
|
login_manager.login_message = "Please log in to access this page."
|
||||||
login_manager.login_message_category = 'info'
|
login_manager.login_message_category = "info"
|
||||||
|
|
||||||
# Register template filters
|
# Register template filters
|
||||||
from app.core.template_filters import bp as filters_bp
|
from app.core.template_filters import bp as filters_bp
|
||||||
|
|
||||||
app.register_blueprint(filters_bp)
|
app.register_blueprint(filters_bp)
|
||||||
|
|
||||||
# Create database tables without seeding any data
|
# Create database tables without seeding any data
|
||||||
|
@ -49,28 +50,41 @@ def create_app(config_name='development'):
|
||||||
|
|
||||||
# Register blueprints
|
# Register blueprints
|
||||||
from app.routes.auth import bp as auth_bp
|
from app.routes.auth import bp as auth_bp
|
||||||
|
|
||||||
app.register_blueprint(auth_bp)
|
app.register_blueprint(auth_bp)
|
||||||
|
|
||||||
from app.routes.dashboard import bp as dashboard_bp
|
from app.routes.dashboard import bp as dashboard_bp
|
||||||
|
|
||||||
app.register_blueprint(dashboard_bp)
|
app.register_blueprint(dashboard_bp)
|
||||||
|
|
||||||
from app.routes.ipam import bp as ipam_bp
|
from app.routes.ipam import bp as ipam_bp
|
||||||
|
|
||||||
app.register_blueprint(ipam_bp)
|
app.register_blueprint(ipam_bp)
|
||||||
|
|
||||||
from app.routes.api import bp as api_bp
|
from app.routes.api import bp as api_bp
|
||||||
|
|
||||||
app.register_blueprint(api_bp)
|
app.register_blueprint(api_bp)
|
||||||
|
|
||||||
|
from app.routes.importexport import bp as importexport_bp
|
||||||
|
|
||||||
|
app.register_blueprint(importexport_bp)
|
||||||
|
|
||||||
|
# Add static assets blueprint
|
||||||
|
from app.routes.static import bp as static_bp
|
||||||
|
|
||||||
|
app.register_blueprint(static_bp)
|
||||||
|
|
||||||
# Add error handlers
|
# Add error handlers
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def page_not_found(e):
|
def page_not_found(e):
|
||||||
return render_template('errors/404.html', title='Page Not Found'), 404
|
return render_template("errors/404.html", title="Page Not Found"), 404
|
||||||
|
|
||||||
@app.errorhandler(500)
|
@app.errorhandler(500)
|
||||||
def internal_server_error(e):
|
def internal_server_error(e):
|
||||||
return render_template('errors/500.html', title='Server Error'), 500
|
return render_template("errors/500.html", title="Server Error"), 500
|
||||||
|
|
||||||
@app.errorhandler(403)
|
@app.errorhandler(403)
|
||||||
def forbidden(e):
|
def forbidden(e):
|
||||||
return render_template('errors/403.html', title='Forbidden'), 403
|
return render_template("errors/403.html", title="Forbidden"), 403
|
||||||
|
|
||||||
return app
|
return app
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -4,10 +4,11 @@ from .extensions import db, bcrypt
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
login_manager.login_view = 'auth.login'
|
login_manager.login_view = "auth.login"
|
||||||
|
|
||||||
|
|
||||||
class User(UserMixin, db.Model):
|
class User(UserMixin, db.Model):
|
||||||
__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=True)
|
||||||
|
@ -18,7 +19,7 @@ class User(UserMixin, db.Model):
|
||||||
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
|
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<User {self.username}>'
|
return f"<User {self.username}>"
|
||||||
|
|
||||||
def set_password(self, password):
|
def set_password(self, password):
|
||||||
self.password_hash = generate_password_hash(password)
|
self.password_hash = generate_password_hash(password)
|
||||||
|
@ -29,6 +30,7 @@ class User(UserMixin, db.Model):
|
||||||
def get_id(self):
|
def get_id(self):
|
||||||
return str(self.id)
|
return str(self.id)
|
||||||
|
|
||||||
|
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
def load_user(user_id):
|
def load_user(user_id):
|
||||||
return User.query.get(int(user_id))
|
return User.query.get(int(user_id))
|
|
@ -10,13 +10,12 @@ from flask_wtf.csrf import CSRFProtect
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
migrate = Migrate()
|
migrate = Migrate()
|
||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
login_manager.login_view = 'auth.login'
|
login_manager.login_view = "auth.login"
|
||||||
login_manager.login_message = 'Please log in to access this page.'
|
login_manager.login_message = "Please log in to access this page."
|
||||||
login_manager.login_message_category = 'info'
|
login_manager.login_message_category = "info"
|
||||||
|
|
||||||
bcrypt = Bcrypt()
|
bcrypt = Bcrypt()
|
||||||
csrf = CSRFProtect()
|
csrf = CSRFProtect()
|
||||||
limiter = Limiter(
|
limiter = Limiter(
|
||||||
key_func=get_remote_address,
|
key_func=get_remote_address, default_limits=["200 per day", "50 per hour"]
|
||||||
default_limits=["200 per day", "50 per hour"]
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,41 +8,48 @@ from flask_login import UserMixin
|
||||||
# User model has been moved to app.core.auth
|
# User model has been moved to app.core.auth
|
||||||
# Import it from there instead if needed: from app.core.auth import User
|
# Import it from there instead if needed: from app.core.auth import User
|
||||||
|
|
||||||
|
|
||||||
class Port(db.Model):
|
class Port(db.Model):
|
||||||
__tablename__ = 'ports'
|
__tablename__ = "ports"
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
app_id = db.Column(db.Integer, db.ForeignKey('apps.id', ondelete='CASCADE'), nullable=False)
|
app_id = db.Column(
|
||||||
|
db.Integer, db.ForeignKey("apps.id", ondelete="CASCADE"), nullable=False
|
||||||
|
)
|
||||||
port_number = db.Column(db.Integer, nullable=False)
|
port_number = db.Column(db.Integer, nullable=False)
|
||||||
protocol = db.Column(db.String(10), default='TCP') # TCP, UDP, etc.
|
protocol = db.Column(db.String(10), default="TCP") # TCP, UDP, etc.
|
||||||
description = db.Column(db.String(200))
|
description = db.Column(db.String(200))
|
||||||
|
|
||||||
# Relationship
|
# Relationship
|
||||||
app = db.relationship('App', back_populates='ports')
|
app = db.relationship("App", back_populates="ports")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Port {self.port_number}/{self.protocol}>'
|
return f"<Port {self.port_number}/{self.protocol}>"
|
||||||
|
|
||||||
|
|
||||||
class Server(db.Model):
|
class Server(db.Model):
|
||||||
__tablename__ = 'servers'
|
__tablename__ = "servers"
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
hostname = db.Column(db.String(64), nullable=False)
|
hostname = db.Column(db.String(64), nullable=False)
|
||||||
ip_address = db.Column(db.String(39), nullable=False) # IPv4 or IPv6
|
ip_address = db.Column(db.String(39), nullable=False) # IPv4 or IPv6
|
||||||
subnet_id = db.Column(db.Integer, db.ForeignKey('subnets.id'), nullable=False)
|
subnet_id = db.Column(db.Integer, db.ForeignKey("subnets.id"), nullable=False)
|
||||||
documentation = db.Column(db.Text)
|
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(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
updated_at = db.Column(
|
||||||
|
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
|
||||||
|
)
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
subnet = db.relationship('Subnet', back_populates='servers')
|
subnet = db.relationship("Subnet", back_populates="servers")
|
||||||
apps = db.relationship('App', back_populates='server', cascade='all, delete-orphan')
|
apps = db.relationship("App", back_populates="server", cascade="all, delete-orphan")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Server {self.hostname}>'
|
return f"<Server {self.hostname}>"
|
||||||
|
|
||||||
|
|
||||||
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(18), unique=True, nullable=False) # e.g., 192.168.1.0/24
|
||||||
|
@ -51,13 +58,15 @@ class Subnet(db.Model):
|
||||||
last_scanned = db.Column(db.DateTime)
|
last_scanned = db.Column(db.DateTime)
|
||||||
auto_scan = db.Column(db.Boolean, default=False)
|
auto_scan = db.Column(db.Boolean, default=False)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
updated_at = db.Column(
|
||||||
|
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
|
||||||
|
)
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
servers = db.relationship('Server', back_populates='subnet')
|
servers = db.relationship("Server", back_populates="subnet")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Subnet {self.cidr}>'
|
return f"<Subnet {self.cidr}>"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def used_ips(self):
|
def used_ips(self):
|
||||||
|
@ -75,19 +84,22 @@ class Subnet(db.Model):
|
||||||
def active_hosts_list(self, hosts):
|
def active_hosts_list(self, hosts):
|
||||||
self.active_hosts = json.dumps(hosts)
|
self.active_hosts = json.dumps(hosts)
|
||||||
|
|
||||||
|
|
||||||
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)
|
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(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
updated_at = db.Column(
|
||||||
|
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", cascade="all, delete-orphan")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<App {self.name}>'
|
return f"<App {self.name}>"
|
||||||
|
|
|
@ -3,16 +3,17 @@ import markdown as md_package
|
||||||
import re
|
import re
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
|
|
||||||
bp = Blueprint('filters', __name__)
|
bp = Blueprint("filters", __name__)
|
||||||
|
|
||||||
|
|
||||||
def github_style_admonition(text):
|
def github_style_admonition(text):
|
||||||
"""Transform GitHub-style alerts (> [!NOTE], etc.) to custom HTML"""
|
"""Transform GitHub-style alerts (> [!NOTE], etc.) to custom HTML"""
|
||||||
patterns = {
|
patterns = {
|
||||||
r'> \[!NOTE\](.*?)(?:\n\n|\Z)': '<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title">Note</p>\\1</div>',
|
r"> \[!NOTE\](.*?)(?:\n\n|\Z)": '<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title">Note</p>\\1</div>',
|
||||||
r'> \[!TIP\](.*?)(?:\n\n|\Z)': '<div class="markdown-alert markdown-alert-tip"><p class="markdown-alert-title">Tip</p>\\1</div>',
|
r"> \[!TIP\](.*?)(?:\n\n|\Z)": '<div class="markdown-alert markdown-alert-tip"><p class="markdown-alert-title">Tip</p>\\1</div>',
|
||||||
r'> \[!IMPORTANT\](.*?)(?:\n\n|\Z)': '<div class="markdown-alert markdown-alert-important"><p class="markdown-alert-title">Important</p>\\1</div>',
|
r"> \[!IMPORTANT\](.*?)(?:\n\n|\Z)": '<div class="markdown-alert markdown-alert-important"><p class="markdown-alert-title">Important</p>\\1</div>',
|
||||||
r'> \[!WARNING\](.*?)(?:\n\n|\Z)': '<div class="markdown-alert markdown-alert-warning"><p class="markdown-alert-title">Warning</p>\\1</div>',
|
r"> \[!WARNING\](.*?)(?:\n\n|\Z)": '<div class="markdown-alert markdown-alert-warning"><p class="markdown-alert-title">Warning</p>\\1</div>',
|
||||||
r'> \[!CAUTION\](.*?)(?:\n\n|\Z)': '<div class="markdown-alert markdown-alert-caution"><p class="markdown-alert-title">Caution</p>\\1</div>'
|
r"> \[!CAUTION\](.*?)(?:\n\n|\Z)": '<div class="markdown-alert markdown-alert-caution"><p class="markdown-alert-title">Caution</p>\\1</div>',
|
||||||
}
|
}
|
||||||
|
|
||||||
for pattern, replacement in patterns.items():
|
for pattern, replacement in patterns.items():
|
||||||
|
@ -20,7 +21,8 @@ def github_style_admonition(text):
|
||||||
|
|
||||||
return text
|
return text
|
||||||
|
|
||||||
@bp.app_template_filter('markdown')
|
|
||||||
|
@bp.app_template_filter("markdown")
|
||||||
def markdown_filter(text):
|
def markdown_filter(text):
|
||||||
"""Convert markdown text to HTML with support for GitHub-style features"""
|
"""Convert markdown text to HTML with support for GitHub-style features"""
|
||||||
if text:
|
if text:
|
||||||
|
@ -29,19 +31,14 @@ def markdown_filter(text):
|
||||||
|
|
||||||
# Convert to HTML with regular markdown
|
# Convert to HTML with regular markdown
|
||||||
html = md_package.markdown(
|
html = md_package.markdown(
|
||||||
text,
|
text, extensions=["tables", "fenced_code", "codehilite", "nl2br"]
|
||||||
extensions=[
|
|
||||||
'tables',
|
|
||||||
'fenced_code',
|
|
||||||
'codehilite',
|
|
||||||
'nl2br'
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return html
|
return html
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@bp.app_template_filter('ip_network')
|
|
||||||
|
@bp.app_template_filter("ip_network")
|
||||||
def ip_network_filter(cidr):
|
def ip_network_filter(cidr):
|
||||||
"""Convert a CIDR string to an IP network object"""
|
"""Convert a CIDR string to an IP network object"""
|
||||||
try:
|
try:
|
||||||
|
@ -49,7 +46,8 @@ def ip_network_filter(cidr):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@bp.app_template_filter('ip_address')
|
|
||||||
|
@bp.app_template_filter("ip_address")
|
||||||
def ip_address_filter(ip):
|
def ip_address_filter(ip):
|
||||||
"""Convert an IP string to an IP address object"""
|
"""Convert an IP string to an IP address object"""
|
||||||
try:
|
try:
|
||||||
|
@ -57,7 +55,8 @@ def ip_address_filter(ip):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@bp.app_template_global('get_ip_network')
|
|
||||||
|
@bp.app_template_global("get_ip_network")
|
||||||
def get_ip_network(cidr):
|
def get_ip_network(cidr):
|
||||||
"""Global function to get an IP network object from CIDR"""
|
"""Global function to get an IP network object from CIDR"""
|
||||||
try:
|
try:
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
app/routes/__pycache__/importexport.cpython-313.pyc
Normal file
BIN
app/routes/__pycache__/importexport.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
app/routes/__pycache__/static.cpython-313.pyc
Normal file
BIN
app/routes/__pycache__/static.cpython-313.pyc
Normal file
Binary file not shown.
|
@ -1,14 +1,19 @@
|
||||||
from flask import Blueprint, jsonify, request, abort
|
from flask import Blueprint, jsonify, request, abort, current_app, render_template
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
from app.core.models import Subnet, Server, App, Port
|
from app.core.models import Subnet, Server, App, Port
|
||||||
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
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
from flask_wtf import CSRFProtect
|
||||||
|
import markdown
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
bp = Blueprint('api', __name__, url_prefix='/api')
|
bp = Blueprint("api", __name__, url_prefix="/api")
|
||||||
|
csrf = CSRFProtect()
|
||||||
|
|
||||||
@bp.route('/subnets', methods=['GET'])
|
|
||||||
|
@bp.route("/subnets", methods=["GET"])
|
||||||
def get_subnets():
|
def get_subnets():
|
||||||
"""Get all subnets grouped by site"""
|
"""Get all subnets grouped by site"""
|
||||||
subnets = Subnet.query.all()
|
subnets = Subnet.query.all()
|
||||||
|
@ -20,23 +25,19 @@ def get_subnets():
|
||||||
if location not in sites:
|
if location not in sites:
|
||||||
sites[location] = []
|
sites[location] = []
|
||||||
|
|
||||||
sites[location].append({
|
sites[location].append(
|
||||||
'id': subnet.id,
|
{"id": subnet.id, "cidr": subnet.cidr, "location": location}
|
||||||
'cidr': subnet.cidr,
|
)
|
||||||
'location': location
|
|
||||||
})
|
|
||||||
|
|
||||||
# Convert to list of site objects
|
# Convert to list of site objects
|
||||||
result = [
|
result = [
|
||||||
{
|
{"name": site_name, "subnets": subnets} for site_name, subnets in sites.items()
|
||||||
'name': site_name,
|
|
||||||
'subnets': subnets
|
|
||||||
} for site_name, subnets in sites.items()
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
@bp.route('/subnets/<int:subnet_id>', methods=['GET'])
|
|
||||||
|
@bp.route("/subnets/<int:subnet_id>", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def get_subnet(subnet_id):
|
def get_subnet(subnet_id):
|
||||||
"""Get details for a specific subnet"""
|
"""Get details for a specific subnet"""
|
||||||
|
@ -44,26 +45,29 @@ def get_subnet(subnet_id):
|
||||||
|
|
||||||
servers = []
|
servers = []
|
||||||
for server in Server.query.filter_by(subnet_id=subnet_id).all():
|
for server in Server.query.filter_by(subnet_id=subnet_id).all():
|
||||||
servers.append({
|
servers.append(
|
||||||
'id': server.id,
|
{
|
||||||
'hostname': server.hostname,
|
"id": server.id,
|
||||||
'ip_address': server.ip_address,
|
"hostname": server.hostname,
|
||||||
'created_at': server.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
"ip_address": server.ip_address,
|
||||||
})
|
"created_at": server.created_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
'id': subnet.id,
|
"id": subnet.id,
|
||||||
'cidr': subnet.cidr,
|
"cidr": subnet.cidr,
|
||||||
'location': subnet.location,
|
"location": subnet.location,
|
||||||
'used_ips': subnet.used_ips,
|
"used_ips": subnet.used_ips,
|
||||||
'auto_scan': subnet.auto_scan,
|
"auto_scan": subnet.auto_scan,
|
||||||
'created_at': subnet.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
"created_at": subnet.created_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
'servers': servers
|
"servers": servers,
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
@bp.route('/subnets/<int:subnet_id>/scan', methods=['POST'])
|
|
||||||
|
@bp.route("/subnets/<int:subnet_id>/scan", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def api_subnet_scan(subnet_id):
|
def api_subnet_scan(subnet_id):
|
||||||
"""Scan a subnet via API"""
|
"""Scan a subnet via API"""
|
||||||
|
@ -71,19 +75,22 @@ def api_subnet_scan(subnet_id):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
results = scan(subnet.cidr, save_results=True)
|
results = scan(subnet.cidr, save_results=True)
|
||||||
return jsonify({
|
return jsonify(
|
||||||
'success': True,
|
{
|
||||||
'subnet': subnet.cidr,
|
"success": True,
|
||||||
'hosts_found': len(results),
|
"subnet": subnet.cidr,
|
||||||
'results': results
|
"hosts_found": len(results),
|
||||||
})
|
"results": results,
|
||||||
|
}
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({
|
return (
|
||||||
'success': False,
|
jsonify({"success": False, "message": f"Error scanning subnet: {str(e)}"}),
|
||||||
'message': f'Error scanning subnet: {str(e)}'
|
500,
|
||||||
}), 500
|
)
|
||||||
|
|
||||||
@bp.route('/servers', methods=['GET'])
|
|
||||||
|
@bp.route("/servers", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def get_servers():
|
def get_servers():
|
||||||
"""Get all servers"""
|
"""Get all servers"""
|
||||||
|
@ -91,17 +98,20 @@ def get_servers():
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
for server in servers:
|
for server in servers:
|
||||||
result.append({
|
result.append(
|
||||||
'id': server.id,
|
{
|
||||||
'hostname': server.hostname,
|
"id": server.id,
|
||||||
'ip_address': server.ip_address,
|
"hostname": server.hostname,
|
||||||
'subnet_id': server.subnet_id,
|
"ip_address": server.ip_address,
|
||||||
'created_at': server.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
"subnet_id": server.subnet_id,
|
||||||
})
|
"created_at": server.created_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return jsonify({'servers': result})
|
return jsonify({"servers": result})
|
||||||
|
|
||||||
@bp.route('/servers/<int:server_id>', methods=['GET'])
|
|
||||||
|
@bp.route("/servers/<int:server_id>", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def get_server(server_id):
|
def get_server(server_id):
|
||||||
"""Get a specific server"""
|
"""Get a specific server"""
|
||||||
|
@ -111,33 +121,38 @@ def get_server(server_id):
|
||||||
for app in server.apps:
|
for app in server.apps:
|
||||||
ports = []
|
ports = []
|
||||||
for port in app.ports:
|
for port in app.ports:
|
||||||
ports.append({
|
ports.append(
|
||||||
'id': port.id,
|
{
|
||||||
'port_number': port.port_number,
|
"id": port.id,
|
||||||
'protocol': port.protocol,
|
"port_number": port.port_number,
|
||||||
'description': port.description
|
"protocol": port.protocol,
|
||||||
})
|
"description": port.description,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
apps.append({
|
apps.append(
|
||||||
'id': app.id,
|
{
|
||||||
'name': app.name,
|
"id": app.id,
|
||||||
'ports': ports,
|
"name": app.name,
|
||||||
'created_at': app.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
"ports": ports,
|
||||||
})
|
"created_at": app.created_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
'id': server.id,
|
"id": server.id,
|
||||||
'hostname': server.hostname,
|
"hostname": server.hostname,
|
||||||
'ip_address': server.ip_address,
|
"ip_address": server.ip_address,
|
||||||
'subnet_id': server.subnet_id,
|
"subnet_id": server.subnet_id,
|
||||||
'documentation': server.documentation,
|
"documentation": server.documentation,
|
||||||
'apps': apps,
|
"apps": apps,
|
||||||
'created_at': server.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
"created_at": server.created_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
@bp.route('/apps', methods=['GET'])
|
|
||||||
|
@bp.route("/apps", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def get_apps():
|
def get_apps():
|
||||||
"""Get all applications"""
|
"""Get all applications"""
|
||||||
|
@ -145,80 +160,90 @@ def get_apps():
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
for app in apps:
|
for app in apps:
|
||||||
result.append({
|
result.append(
|
||||||
'id': app.id,
|
{
|
||||||
'name': app.name,
|
"id": app.id,
|
||||||
'server_id': app.server_id,
|
"name": app.name,
|
||||||
'created_at': app.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
"server_id": app.server_id,
|
||||||
})
|
"created_at": app.created_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return jsonify({'apps': result})
|
return jsonify({"apps": result})
|
||||||
|
|
||||||
@bp.route('/apps/<int:app_id>', methods=['GET'])
|
|
||||||
|
@bp.route("/apps/<int:app_id>", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def get_app(app_id):
|
def get_app(app_id):
|
||||||
"""Get details for a specific application"""
|
"""Get details for a specific application"""
|
||||||
app = App.query.get_or_404(app_id)
|
app = App.query.get_or_404(app_id)
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
'id': app.id,
|
"id": app.id,
|
||||||
'name': app.name,
|
"name": app.name,
|
||||||
'server_id': app.server_id,
|
"server_id": app.server_id,
|
||||||
'documentation': app.documentation,
|
"documentation": app.documentation,
|
||||||
'created_at': app.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
"created_at": app.created_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
'ports': app.ports
|
"ports": app.ports,
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
@bp.route('/status', methods=['GET'])
|
|
||||||
def status():
|
|
||||||
return jsonify({'status': 'OK'})
|
|
||||||
|
|
||||||
@bp.route('/markdown-preview', methods=['POST'])
|
@bp.route("/status", methods=["GET"])
|
||||||
|
def status():
|
||||||
|
return jsonify({"status": "OK"})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/markdown-preview", methods=["POST"])
|
||||||
|
@csrf.exempt # Remove this line in production! Temporary fix for demo purposes
|
||||||
def markdown_preview():
|
def markdown_preview():
|
||||||
data = request.json
|
data = request.json
|
||||||
md_content = data.get('markdown', '')
|
md_content = data.get("markdown", "")
|
||||||
html = markdown.markdown(md_content)
|
html = markdown.markdown(md_content)
|
||||||
return jsonify({'html': html})
|
return jsonify({"html": html})
|
||||||
|
|
||||||
@bp.route('/ports/suggest', methods=['GET'])
|
|
||||||
|
@bp.route("/ports/suggest", methods=["GET"])
|
||||||
def suggest_ports():
|
def suggest_ports():
|
||||||
app_type = request.args.get('type', '').lower()
|
app_type = request.args.get("type", "").lower()
|
||||||
|
|
||||||
# Common port suggestions based on app type
|
# Common port suggestions based on app type
|
||||||
suggestions = {
|
suggestions = {
|
||||||
'web': [
|
"web": [
|
||||||
{'port': 80, 'type': 'tcp', 'desc': 'HTTP'},
|
{"port": 80, "type": "tcp", "desc": "HTTP"},
|
||||||
{'port': 443, 'type': 'tcp', 'desc': 'HTTPS'}
|
{"port": 443, "type": "tcp", "desc": "HTTPS"},
|
||||||
],
|
],
|
||||||
'database': [
|
"database": [
|
||||||
{'port': 3306, 'type': 'tcp', 'desc': 'MySQL'},
|
{"port": 3306, "type": "tcp", "desc": "MySQL"},
|
||||||
{'port': 5432, 'type': 'tcp', 'desc': 'PostgreSQL'},
|
{"port": 5432, "type": "tcp", "desc": "PostgreSQL"},
|
||||||
{'port': 1521, 'type': 'tcp', 'desc': 'Oracle'}
|
{"port": 1521, "type": "tcp", "desc": "Oracle"},
|
||||||
],
|
],
|
||||||
'mail': [
|
"mail": [
|
||||||
{'port': 25, 'type': 'tcp', 'desc': 'SMTP'},
|
{"port": 25, "type": "tcp", "desc": "SMTP"},
|
||||||
{'port': 143, 'type': 'tcp', 'desc': 'IMAP'},
|
{"port": 143, "type": "tcp", "desc": "IMAP"},
|
||||||
{'port': 110, 'type': 'tcp', 'desc': 'POP3'}
|
{"port": 110, "type": "tcp", "desc": "POP3"},
|
||||||
|
],
|
||||||
|
"file": [
|
||||||
|
{"port": 21, "type": "tcp", "desc": "FTP"},
|
||||||
|
{"port": 22, "type": "tcp", "desc": "SFTP/SSH"},
|
||||||
|
{"port": 445, "type": "tcp", "desc": "SMB"},
|
||||||
],
|
],
|
||||||
'file': [
|
|
||||||
{'port': 21, 'type': 'tcp', 'desc': 'FTP'},
|
|
||||||
{'port': 22, 'type': 'tcp', 'desc': 'SFTP/SSH'},
|
|
||||||
{'port': 445, 'type': 'tcp', 'desc': 'SMB'}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if app_type in suggestions:
|
if app_type in suggestions:
|
||||||
return jsonify(suggestions[app_type])
|
return jsonify(suggestions[app_type])
|
||||||
|
|
||||||
# Default suggestions
|
# Default suggestions
|
||||||
return jsonify([
|
return jsonify(
|
||||||
{'port': 80, 'type': 'tcp', 'desc': 'HTTP'},
|
[
|
||||||
{'port': 22, 'type': 'tcp', 'desc': 'SSH'}
|
{"port": 80, "type": "tcp", "desc": "HTTP"},
|
||||||
])
|
{"port": 22, "type": "tcp", "desc": "SSH"},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
@bp.route('/servers/<int:server_id>/suggest_port', methods=['GET'])
|
|
||||||
|
@bp.route("/servers/<int:server_id>/suggest_port", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def suggest_port(server_id):
|
def suggest_port(server_id):
|
||||||
"""Suggest a random unused port for a server"""
|
"""Suggest a random unused port for a server"""
|
||||||
|
@ -251,83 +276,136 @@ def suggest_port(server_id):
|
||||||
available_port = port
|
available_port = port
|
||||||
break
|
break
|
||||||
|
|
||||||
return jsonify({'port': available_port})
|
return jsonify({"port": available_port})
|
||||||
|
|
||||||
@bp.route('/apps/<int:app_id>/ports', methods=['GET'])
|
|
||||||
@login_required
|
|
||||||
def get_app_ports(app_id):
|
|
||||||
"""Get all ports for an app"""
|
|
||||||
app = App.query.get_or_404(app_id)
|
|
||||||
|
|
||||||
ports = []
|
@bp.route("/app/<int:app_id>/add-port", methods=["POST"])
|
||||||
for port in app.ports:
|
|
||||||
ports.append({
|
|
||||||
'id': port.id,
|
|
||||||
'port_number': port.port_number,
|
|
||||||
'protocol': port.protocol,
|
|
||||||
'description': port.description
|
|
||||||
})
|
|
||||||
|
|
||||||
return jsonify({'ports': ports})
|
|
||||||
|
|
||||||
@bp.route('/apps/<int:app_id>/ports', methods=['POST'])
|
|
||||||
@login_required
|
@login_required
|
||||||
def add_app_port(app_id):
|
def add_app_port(app_id):
|
||||||
"""Add a new port to an app"""
|
"""Add a port to an application"""
|
||||||
app = App.query.get_or_404(app_id)
|
app = App.query.get_or_404(app_id)
|
||||||
|
|
||||||
|
# Accept both JSON and form data
|
||||||
|
if request.is_json:
|
||||||
data = request.json
|
data = request.json
|
||||||
if not data or 'port_number' not in data:
|
else:
|
||||||
return jsonify({'error': 'Missing port number'}), 400
|
data = request.form
|
||||||
|
|
||||||
port_number = data.get('port_number')
|
port_number = data.get("port")
|
||||||
protocol = data.get('protocol', 'TCP')
|
protocol = data.get("protocol", "TCP")
|
||||||
description = data.get('description', '')
|
description = data.get("description", "")
|
||||||
|
|
||||||
|
if not port_number:
|
||||||
|
return jsonify({"success": False, "error": "Port number is required"}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
port_number = int(port_number)
|
||||||
|
|
||||||
# Check if port already exists for this app
|
# Check if port already exists for this app
|
||||||
existing_port = Port.query.filter_by(app_id=app_id, port_number=port_number).first()
|
existing_port = Port.query.filter_by(app_id=app_id, number=port_number).first()
|
||||||
if existing_port:
|
if existing_port:
|
||||||
return jsonify({'error': 'Port already exists for this app'}), 400
|
return (
|
||||||
|
jsonify(
|
||||||
new_port = Port(
|
{
|
||||||
app_id=app_id,
|
"success": False,
|
||||||
port_number=port_number,
|
"error": "Port already exists for this application",
|
||||||
protocol=protocol,
|
}
|
||||||
description=description
|
),
|
||||||
|
400,
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.add(new_port)
|
# Create new port
|
||||||
|
port = Port(
|
||||||
|
number=port_number,
|
||||||
|
protocol=protocol,
|
||||||
|
description=description,
|
||||||
|
app_id=app_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(port)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify({
|
return jsonify(
|
||||||
'id': new_port.id,
|
{
|
||||||
'port_number': new_port.port_number,
|
"success": True,
|
||||||
'protocol': new_port.protocol,
|
"message": f"Port {port_number} added to {app.name}",
|
||||||
'description': new_port.description
|
"port": {
|
||||||
})
|
"id": port.id,
|
||||||
|
"number": port.number,
|
||||||
|
"protocol": port.protocol,
|
||||||
|
"description": port.description,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
return jsonify({"success": False, "error": "Invalid port number"}), 400
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
@bp.route('/ports/<int:port_id>', methods=['DELETE'])
|
|
||||||
|
@bp.route("/app/<int:app_id>/ports", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def get_app_ports(app_id):
|
||||||
|
"""Get all ports for an application"""
|
||||||
|
app = App.query.get_or_404(app_id)
|
||||||
|
|
||||||
|
ports = Port.query.filter_by(app_id=app_id).all()
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"app_id": app_id,
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"id": port.id,
|
||||||
|
"number": port.number,
|
||||||
|
"protocol": port.protocol,
|
||||||
|
"description": port.description,
|
||||||
|
}
|
||||||
|
for port in ports
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/port/<int:port_id>/delete", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def delete_port(port_id):
|
def delete_port(port_id):
|
||||||
"""Delete a port"""
|
"""Delete a port"""
|
||||||
|
# Add CSRF validation
|
||||||
|
if request.is_json: # For AJAX requests
|
||||||
|
csrf_token = request.json.get("csrf_token")
|
||||||
|
if not csrf_token or not csrf.validate_csrf(csrf_token):
|
||||||
|
return jsonify({"success": False, "error": "CSRF validation failed"}), 403
|
||||||
|
|
||||||
port = Port.query.get_or_404(port_id)
|
port = Port.query.get_or_404(port_id)
|
||||||
|
|
||||||
|
try:
|
||||||
db.session.delete(port)
|
db.session.delete(port)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
return jsonify({"success": True, "message": f"Port {port.number} deleted"})
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
return jsonify({'success': True})
|
|
||||||
|
|
||||||
@bp.route('/subnets/<int:subnet_id>/servers', methods=['GET'])
|
@bp.route("/subnets/<int:subnet_id>/servers", methods=["GET"])
|
||||||
def get_subnet_servers(subnet_id):
|
def get_subnet_servers(subnet_id):
|
||||||
"""Get all servers for a specific subnet"""
|
"""Get all servers for a specific subnet"""
|
||||||
servers = Server.query.filter_by(subnet_id=subnet_id).all()
|
servers = Server.query.filter_by(subnet_id=subnet_id).all()
|
||||||
return jsonify([{
|
return jsonify(
|
||||||
'id': server.id,
|
[
|
||||||
'hostname': server.hostname,
|
{
|
||||||
'ip_address': server.ip_address
|
"id": server.id,
|
||||||
} for server in servers])
|
"hostname": server.hostname,
|
||||||
|
"ip_address": server.ip_address,
|
||||||
|
}
|
||||||
|
for server in servers
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
@bp.route('/server/<int:server_id>/ports', methods=['GET'])
|
|
||||||
|
@bp.route("/server/<int:server_id>/ports", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def get_server_ports(server_id):
|
def get_server_ports(server_id):
|
||||||
"""Get all used ports for a server"""
|
"""Get all used ports for a server"""
|
||||||
|
@ -337,29 +415,25 @@ def get_server_ports(server_id):
|
||||||
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.number for port in ports]
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({"server_id": server_id, "used_ports": used_ports})
|
||||||
'server_id': server_id,
|
|
||||||
'used_ports': used_ports
|
|
||||||
})
|
|
||||||
|
|
||||||
@bp.route('/server/<int:server_id>/free-port', methods=['GET'])
|
|
||||||
|
@bp.route("/server/<int:server_id>/free-port", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def get_free_port(server_id):
|
def get_free_port(server_id):
|
||||||
"""Find a free port for a server"""
|
"""Find a free port for a server"""
|
||||||
server = Server.query.get_or_404(server_id)
|
server = Server.query.get_or_404(server_id)
|
||||||
|
|
||||||
# Get all ports associated with this server
|
# Get all ports associated with this server
|
||||||
used_ports = [port.number for port in Port.query.filter_by(server_id=server_id).all()]
|
used_ports = [
|
||||||
|
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)
|
||||||
for port_number in range(8000, 9000):
|
for port_number in range(8000, 9000):
|
||||||
if port_number not in used_ports:
|
if port_number not in used_ports:
|
||||||
return jsonify({
|
return jsonify({"success": True, "port": port_number})
|
||||||
'success': True,
|
|
||||||
'port': port_number
|
|
||||||
})
|
|
||||||
|
|
||||||
return jsonify({
|
return jsonify(
|
||||||
'success': False,
|
{"success": False, "error": "No free ports available in the range 8000-9000"}
|
||||||
'error': 'No free ports available in the range 8000-9000'
|
)
|
||||||
})
|
|
||||||
|
|
|
@ -3,75 +3,87 @@ from flask_login import login_user, logout_user, current_user, login_required
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from app.core.extensions import db
|
from app.core.extensions import db
|
||||||
from app.core.auth import User
|
from app.core.auth import User
|
||||||
|
import re
|
||||||
|
from flask_wtf.csrf import CSRFProtect
|
||||||
|
|
||||||
bp = Blueprint('auth', __name__, url_prefix='/auth')
|
bp = Blueprint("auth", __name__, url_prefix="/auth")
|
||||||
|
csrf = CSRFProtect()
|
||||||
|
|
||||||
@bp.route('/login', methods=['GET', 'POST'])
|
|
||||||
|
@bp.route("/login", methods=["GET", "POST"])
|
||||||
def login():
|
def login():
|
||||||
"""User login"""
|
"""User login"""
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return redirect(url_for('dashboard.dashboard_home'))
|
return redirect(url_for("dashboard.dashboard_home"))
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
email = request.form.get('email')
|
email = request.form.get("email")
|
||||||
password = request.form.get('password')
|
password = request.form.get("password")
|
||||||
remember = 'remember' in request.form
|
remember = "remember" in request.form
|
||||||
|
|
||||||
user = User.query.filter_by(email=email).first()
|
user = User.query.filter_by(email=email).first()
|
||||||
|
|
||||||
if not user or not user.check_password(password):
|
if not user or not user.check_password(password):
|
||||||
flash('Invalid email or password', 'danger')
|
flash("Invalid email or password", "danger")
|
||||||
return render_template('auth/login.html', title='Login')
|
return render_template("auth/login.html", title="Login")
|
||||||
|
|
||||||
login_user(user, remember=remember)
|
login_user(user, remember=remember)
|
||||||
|
|
||||||
next_page = request.args.get('next')
|
next_page = request.args.get("next")
|
||||||
if not next_page or not next_page.startswith('/'):
|
if not next_page or not next_page.startswith("/"):
|
||||||
next_page = url_for('dashboard.dashboard_home')
|
next_page = url_for("dashboard.dashboard_home")
|
||||||
|
|
||||||
return redirect(next_page)
|
return redirect(next_page)
|
||||||
|
|
||||||
return render_template('auth/login.html', title='Login')
|
return render_template("auth/login.html", title="Login")
|
||||||
|
|
||||||
@bp.route('/register', methods=['GET', 'POST'])
|
|
||||||
|
@bp.route("/register", methods=["GET", "POST"])
|
||||||
|
@csrf.exempt # Remove for production! Temporary allow registration without CSRF
|
||||||
def register():
|
def register():
|
||||||
"""User registration"""
|
"""User registration"""
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return redirect(url_for('dashboard.dashboard_home'))
|
return redirect(url_for("dashboard.dashboard_home"))
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
email = request.form.get('email')
|
email = request.form.get("email")
|
||||||
password = request.form.get('password')
|
username = request.form.get("username")
|
||||||
|
password = request.form.get("password")
|
||||||
|
password_confirm = request.form.get("password_confirm")
|
||||||
|
|
||||||
# Validation
|
# Validate form data
|
||||||
if not email or not password:
|
error = None
|
||||||
flash('Email and password are required', 'danger')
|
if not email or not username or not password:
|
||||||
return render_template('auth/register.html', title='Register')
|
error = "All fields are required."
|
||||||
|
elif not re.match(r"[^@]+@[^@]+\.[^@]+", email):
|
||||||
if User.query.filter_by(email=email).first():
|
error = "Please enter a valid email address."
|
||||||
flash('Email already registered', 'danger')
|
elif password != password_confirm:
|
||||||
return render_template('auth/register.html', title='Register')
|
error = "Passwords do not match."
|
||||||
|
elif User.query.filter_by(email=email).first():
|
||||||
|
error = "Email address already registered."
|
||||||
|
elif User.query.filter_by(username=username).first():
|
||||||
|
error = "Username already taken."
|
||||||
|
|
||||||
|
if error:
|
||||||
|
flash(error, "danger")
|
||||||
|
else:
|
||||||
# Create new user
|
# Create new user
|
||||||
user = User(email=email)
|
new_user = User(email=email, username=username)
|
||||||
user.set_password(password)
|
new_user.set_password(password)
|
||||||
|
|
||||||
db.session.add(user)
|
db.session.add(new_user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
flash('Registration successful! You are now logged in.', 'success')
|
flash("Registration successful! You can now log in.", "success")
|
||||||
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
# Auto-login after registration
|
return render_template("auth/register.html", title="Register")
|
||||||
login_user(user)
|
|
||||||
|
|
||||||
return redirect(url_for('dashboard.dashboard_home'))
|
|
||||||
|
|
||||||
return render_template('auth/register.html', title='Register')
|
@bp.route("/logout")
|
||||||
|
|
||||||
@bp.route('/logout')
|
|
||||||
@login_required
|
@login_required
|
||||||
def logout():
|
def logout():
|
||||||
"""User logout"""
|
"""User logout"""
|
||||||
logout_user()
|
logout_user()
|
||||||
flash('You have been logged out', 'info')
|
flash("You have been logged out", "info")
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for("auth.login"))
|
||||||
|
|
|
@ -4,10 +4,12 @@ import markdown
|
||||||
from app.core.models import Server, App, Subnet, Port
|
from app.core.models import Server, App, Subnet, Port
|
||||||
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
|
||||||
|
|
||||||
bp = Blueprint('dashboard', __name__, url_prefix='/dashboard')
|
bp = Blueprint("dashboard", __name__, url_prefix="/dashboard")
|
||||||
|
|
||||||
@bp.route('/')
|
|
||||||
|
@bp.route("/")
|
||||||
@login_required
|
@login_required
|
||||||
def dashboard_home():
|
def dashboard_home():
|
||||||
"""Main dashboard view showing server statistics"""
|
"""Main dashboard view showing server statistics"""
|
||||||
|
@ -21,33 +23,37 @@ def dashboard_home():
|
||||||
# Get subnets with usage stats
|
# Get subnets with usage stats
|
||||||
subnets = Subnet.query.all()
|
subnets = Subnet.query.all()
|
||||||
for subnet in subnets:
|
for subnet in subnets:
|
||||||
subnet.usage_percent = subnet.used_ips / 254 * 100 if subnet.cidr.endswith('/24') else 0
|
subnet.usage_percent = (
|
||||||
|
subnet.used_ips / 254 * 100 if subnet.cidr.endswith("/24") else 0
|
||||||
|
)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'dashboard/index.html',
|
"dashboard/index.html",
|
||||||
title='Dashboard',
|
title="Dashboard",
|
||||||
server_count=server_count,
|
server_count=server_count,
|
||||||
app_count=app_count,
|
app_count=app_count,
|
||||||
subnet_count=subnet_count,
|
subnet_count=subnet_count,
|
||||||
latest_servers=latest_servers,
|
latest_servers=latest_servers,
|
||||||
subnets=subnets,
|
subnets=subnets,
|
||||||
now=datetime.now()
|
now=datetime.now(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@bp.route('/servers')
|
|
||||||
|
@bp.route("/servers")
|
||||||
@login_required
|
@login_required
|
||||||
def server_list():
|
def server_list():
|
||||||
"""List all servers"""
|
"""List all servers"""
|
||||||
servers = Server.query.order_by(Server.hostname).all()
|
servers = Server.query.order_by(Server.hostname).all()
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'dashboard/server_list.html',
|
"dashboard/server_list.html",
|
||||||
title='Servers',
|
title="Servers",
|
||||||
servers=servers,
|
servers=servers,
|
||||||
now=datetime.now()
|
now=datetime.now(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@bp.route('/server/<int:server_id>')
|
|
||||||
|
@bp.route("/server/<int:server_id>")
|
||||||
@login_required
|
@login_required
|
||||||
def server_view(server_id):
|
def server_view(server_id):
|
||||||
"""View server details"""
|
"""View server details"""
|
||||||
|
@ -55,52 +61,53 @@ def server_view(server_id):
|
||||||
apps = App.query.filter_by(server_id=server_id).all()
|
apps = App.query.filter_by(server_id=server_id).all()
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'dashboard/server_view.html',
|
"dashboard/server_view.html",
|
||||||
title=f'Server - {server.hostname}',
|
title=f"Server - {server.hostname}",
|
||||||
server=server,
|
server=server,
|
||||||
apps=apps,
|
apps=apps,
|
||||||
now=datetime.now()
|
now=datetime.now(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@bp.route('/server/new', methods=['GET', 'POST'])
|
|
||||||
|
@bp.route("/server/new", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def server_new():
|
def server_new():
|
||||||
"""Create a new server"""
|
"""Create a new server"""
|
||||||
subnets = Subnet.query.all()
|
subnets = Subnet.query.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")
|
||||||
documentation = request.form.get('documentation', '')
|
documentation = request.form.get("documentation", "")
|
||||||
|
|
||||||
# Basic validation
|
# Basic validation
|
||||||
if not hostname or not ip_address or not subnet_id:
|
if not hostname or not ip_address or not subnet_id:
|
||||||
flash('Please fill in all required fields', 'danger')
|
flash("Please fill in all required fields", "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()
|
now=datetime.now(),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if hostname or IP already exists
|
# Check if hostname or IP already exists
|
||||||
if Server.query.filter_by(hostname=hostname).first():
|
if Server.query.filter_by(hostname=hostname).first():
|
||||||
flash('Hostname already exists', 'danger')
|
flash("Hostname already exists", "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()
|
now=datetime.now(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if Server.query.filter_by(ip_address=ip_address).first():
|
if Server.query.filter_by(ip_address=ip_address).first():
|
||||||
flash('IP address already exists', 'danger')
|
flash("IP address already exists", "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()
|
now=datetime.now(),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create new server
|
# Create new server
|
||||||
|
@ -108,62 +115,69 @@ def server_new():
|
||||||
hostname=hostname,
|
hostname=hostname,
|
||||||
ip_address=ip_address,
|
ip_address=ip_address,
|
||||||
subnet_id=subnet_id,
|
subnet_id=subnet_id,
|
||||||
documentation=documentation
|
documentation=documentation,
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.add(server)
|
db.session.add(server)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
flash('Server created successfully', 'success')
|
flash("Server 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()
|
now=datetime.now(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@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 an existing 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()
|
||||||
|
|
||||||
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")
|
||||||
documentation = request.form.get('documentation', '')
|
documentation = request.form.get("documentation", "")
|
||||||
|
|
||||||
if not hostname or not ip_address or not subnet_id:
|
if not hostname or not ip_address or not subnet_id:
|
||||||
flash('All fields are required', 'danger')
|
flash("All fields 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,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if hostname changed and already exists
|
# Check if hostname changed and already exists
|
||||||
if hostname != server.hostname and Server.query.filter_by(hostname=hostname).first():
|
if (
|
||||||
flash('Hostname already exists', 'danger')
|
hostname != server.hostname
|
||||||
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if IP changed and already exists
|
# Check if IP changed and already exists
|
||||||
if ip_address != server.ip_address and Server.query.filter_by(ip_address=ip_address).first():
|
if (
|
||||||
flash('IP address already exists', 'danger')
|
ip_address != server.ip_address
|
||||||
|
and Server.query.filter_by(ip_address=ip_address).first()
|
||||||
|
):
|
||||||
|
flash("IP address 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,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update server
|
# Update server
|
||||||
|
@ -174,18 +188,19 @@ def server_edit(server_id):
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
flash('Server updated successfully', 'success')
|
flash("Server 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
|
# 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=f"Edit Server - {server.hostname}",
|
||||||
server=server,
|
server=server,
|
||||||
subnets=subnets
|
subnets=subnets,
|
||||||
)
|
)
|
||||||
|
|
||||||
@bp.route('/server/<int:server_id>/delete', methods=['POST'])
|
|
||||||
|
@bp.route("/server/<int:server_id>/delete", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def server_delete(server_id):
|
def server_delete(server_id):
|
||||||
"""Delete a server"""
|
"""Delete a server"""
|
||||||
|
@ -198,76 +213,61 @@ def server_delete(server_id):
|
||||||
db.session.delete(server)
|
db.session.delete(server)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
flash('Server deleted successfully', 'success')
|
flash("Server deleted successfully", "success")
|
||||||
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", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def app_new():
|
def app_new():
|
||||||
"""Create a new application"""
|
"""Create a new application with comprehensive error handling"""
|
||||||
|
# Get all servers for dropdown
|
||||||
servers = Server.query.all()
|
servers = Server.query.all()
|
||||||
|
|
||||||
if request.method == 'POST':
|
if not servers:
|
||||||
name = request.form.get('name')
|
flash("You need to create a server before adding applications", "warning")
|
||||||
server_id = request.form.get('server_id')
|
return redirect(url_for("dashboard.server_new"))
|
||||||
documentation = request.form.get('documentation', '')
|
|
||||||
|
|
||||||
# Get port data from form
|
if request.method == "POST":
|
||||||
port_numbers = request.form.getlist('port_numbers[]')
|
# Get form data
|
||||||
protocols = request.form.getlist('protocols[]')
|
name = request.form.get("name", "").strip()
|
||||||
port_descriptions = request.form.getlist('port_descriptions[]')
|
server_id = request.form.get("server_id")
|
||||||
|
documentation = request.form.get("documentation", "")
|
||||||
|
|
||||||
# Basic validation
|
# Process port data from form
|
||||||
if not name or not server_id:
|
port_data = []
|
||||||
flash('Please fill in all required fields', 'danger')
|
port_numbers = request.form.getlist("port_numbers[]")
|
||||||
return render_template(
|
protocols = request.form.getlist("protocols[]")
|
||||||
'dashboard/app_form.html',
|
descriptions = request.form.getlist("port_descriptions[]")
|
||||||
title='New Application',
|
|
||||||
servers=servers
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create new app
|
|
||||||
app = App(
|
|
||||||
name=name,
|
|
||||||
server_id=server_id,
|
|
||||||
documentation=documentation
|
|
||||||
)
|
|
||||||
|
|
||||||
db.session.add(app)
|
|
||||||
db.session.flush() # Get the app ID without committing
|
|
||||||
|
|
||||||
# Add ports if provided
|
|
||||||
for i in range(len(port_numbers)):
|
for i in range(len(port_numbers)):
|
||||||
if port_numbers[i] and port_numbers[i].strip():
|
if port_numbers[i] and port_numbers[i].strip():
|
||||||
try:
|
protocol = protocols[i] if i < len(protocols) else "TCP"
|
||||||
port_num = int(port_numbers[i])
|
description = descriptions[i] if i < len(descriptions) else ""
|
||||||
|
port_data.append((port_numbers[i], protocol, description))
|
||||||
|
|
||||||
# Get protocol and description, handling index errors
|
# Save application
|
||||||
protocol = protocols[i] if i < len(protocols) else 'TCP'
|
from app.utils.app_utils import save_app
|
||||||
description = port_descriptions[i] if i < len(port_descriptions) else ''
|
|
||||||
|
|
||||||
new_port = Port(
|
success, app, error = save_app(name, server_id, documentation, port_data)
|
||||||
app_id=app.id,
|
|
||||||
port_number=port_num,
|
|
||||||
protocol=protocol,
|
|
||||||
description=description
|
|
||||||
)
|
|
||||||
db.session.add(new_port)
|
|
||||||
except (ValueError, IndexError):
|
|
||||||
continue
|
|
||||||
|
|
||||||
db.session.commit()
|
if success:
|
||||||
|
flash("Application created successfully", "success")
|
||||||
flash('Application created successfully', 'success')
|
return redirect(url_for("dashboard.app_view", app_id=app.id))
|
||||||
return redirect(url_for('dashboard.server_view', server_id=server_id))
|
else:
|
||||||
|
flash(error, "danger")
|
||||||
|
|
||||||
|
# For GET requests or failed POSTs
|
||||||
return render_template(
|
return render_template(
|
||||||
'dashboard/app_form.html',
|
"dashboard/app_form.html",
|
||||||
title='New Application',
|
title="Create New Application",
|
||||||
servers=servers
|
edit_mode=False,
|
||||||
|
dashboard_link=url_for("dashboard.dashboard_home"),
|
||||||
|
servers=servers,
|
||||||
)
|
)
|
||||||
|
|
||||||
@bp.route('/app/<int:app_id>', methods=['GET'])
|
|
||||||
|
@bp.route("/app/<int:app_id>", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def app_view(app_id):
|
def app_view(app_id):
|
||||||
"""View a specific application"""
|
"""View a specific application"""
|
||||||
|
@ -275,68 +275,71 @@ def app_view(app_id):
|
||||||
server = Server.query.get(app.server_id)
|
server = Server.query.get(app.server_id)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'dashboard/app_view.html',
|
"dashboard/app_view.html",
|
||||||
title=f'Application - {app.name}',
|
title=f"Application - {app.name}",
|
||||||
app=app,
|
app=app,
|
||||||
server=server,
|
server=server,
|
||||||
now=datetime.now()
|
now=datetime.now(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@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"""
|
"""Edit an existing application with comprehensive error handling"""
|
||||||
|
# Get the application and all servers
|
||||||
app = App.query.get_or_404(app_id)
|
app = App.query.get_or_404(app_id)
|
||||||
servers = Server.query.all()
|
servers = Server.query.all()
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
name = request.form.get('name')
|
# Get form data
|
||||||
server_id = request.form.get('server_id')
|
name = request.form.get("name", "").strip()
|
||||||
documentation = request.form.get('documentation', '')
|
server_id = request.form.get("server_id")
|
||||||
|
documentation = request.form.get("documentation", "")
|
||||||
|
|
||||||
if not name or not server_id:
|
# Process port data from form
|
||||||
flash('All required fields must be filled', 'danger')
|
port_data = []
|
||||||
return render_template(
|
port_numbers = request.form.getlist("port_numbers[]")
|
||||||
'dashboard/app_form.html',
|
protocols = request.form.getlist("protocols[]")
|
||||||
title='Edit Application',
|
descriptions = request.form.getlist("port_descriptions[]")
|
||||||
app=app,
|
|
||||||
servers=servers
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if name changed and already exists on the same server
|
for i in range(len(port_numbers)):
|
||||||
existing_app = App.query.filter(App.name == name,
|
if port_numbers[i] and port_numbers[i].strip():
|
||||||
App.server_id == server_id,
|
protocol = protocols[i] if i < len(protocols) else "TCP"
|
||||||
App.id != app.id).first()
|
description = descriptions[i] if i < len(descriptions) else ""
|
||||||
if existing_app:
|
port_data.append((port_numbers[i], protocol, description))
|
||||||
flash('Application with this name already exists on the selected server', 'danger')
|
|
||||||
return render_template(
|
|
||||||
'dashboard/app_form.html',
|
|
||||||
title='Edit Application',
|
|
||||||
app=app,
|
|
||||||
servers=servers
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# Replace local validation with shared function
|
||||||
|
valid, error = validate_app_data(name, server_id, existing_app_id=app_id)
|
||||||
|
|
||||||
|
if valid:
|
||||||
# Update application
|
# Update application
|
||||||
app.name = name
|
from app.utils.app_utils import save_app
|
||||||
app.server_id = server_id
|
|
||||||
app.documentation = documentation
|
|
||||||
|
|
||||||
try:
|
success, updated_app, error = save_app(
|
||||||
db.session.commit()
|
name, server_id, documentation, port_data, app_id
|
||||||
flash('Application updated successfully', 'success')
|
|
||||||
return redirect(url_for('dashboard.app_view', app_id=app.id))
|
|
||||||
except Exception as e:
|
|
||||||
db.session.rollback()
|
|
||||||
flash(f'Error updating application: {str(e)}', 'danger')
|
|
||||||
|
|
||||||
return render_template(
|
|
||||||
'dashboard/app_form.html',
|
|
||||||
title=f'Edit Application - {app.name}',
|
|
||||||
app=app,
|
|
||||||
servers=servers
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@bp.route('/app/<int:app_id>/delete', methods=['POST'])
|
if success:
|
||||||
|
flash("Application updated successfully", "success")
|
||||||
|
return redirect(url_for("dashboard.app_view", app_id=app_id))
|
||||||
|
else:
|
||||||
|
flash(error, "danger")
|
||||||
|
else:
|
||||||
|
flash(error, "danger")
|
||||||
|
|
||||||
|
# For GET requests or failed POSTs
|
||||||
|
return render_template(
|
||||||
|
"dashboard/app_form.html",
|
||||||
|
title=f"Edit Application: {app.name}",
|
||||||
|
edit_mode=True,
|
||||||
|
app=app,
|
||||||
|
dashboard_link=url_for("dashboard.dashboard_home"),
|
||||||
|
servers=servers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/app/<int:app_id>/delete", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def app_delete(app_id):
|
def app_delete(app_id):
|
||||||
"""Delete an application"""
|
"""Delete an application"""
|
||||||
|
@ -346,41 +349,39 @@ def app_delete(app_id):
|
||||||
db.session.delete(app)
|
db.session.delete(app)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
flash('Application deleted successfully', 'success')
|
flash("Application deleted successfully", "success")
|
||||||
return redirect(url_for('dashboard.server_view', server_id=server_id))
|
return redirect(url_for("dashboard.server_view", server_id=server_id))
|
||||||
|
|
||||||
@bp.route('/settings', methods=['GET', 'POST'])
|
|
||||||
|
@bp.route("/settings", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def settings():
|
def settings():
|
||||||
"""User settings page"""
|
"""User settings page"""
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
# Handle user settings update
|
# Handle user settings update
|
||||||
current_password = request.form.get('current_password')
|
current_password = request.form.get("current_password")
|
||||||
new_password = request.form.get('new_password')
|
new_password = request.form.get("new_password")
|
||||||
confirm_password = request.form.get('confirm_password')
|
confirm_password = request.form.get("confirm_password")
|
||||||
|
|
||||||
# Validate inputs
|
# Validate inputs
|
||||||
if not current_password:
|
if not current_password:
|
||||||
flash('Current password is required', 'danger')
|
flash("Current password is required", "danger")
|
||||||
return redirect(url_for('dashboard.settings'))
|
return redirect(url_for("dashboard.settings"))
|
||||||
|
|
||||||
if new_password != confirm_password:
|
if new_password != confirm_password:
|
||||||
flash('New passwords do not match', 'danger')
|
flash("New passwords do not match", "danger")
|
||||||
return redirect(url_for('dashboard.settings'))
|
return redirect(url_for("dashboard.settings"))
|
||||||
|
|
||||||
# Verify current password
|
# Verify current password
|
||||||
if not current_user.check_password(current_password):
|
if not current_user.check_password(current_password):
|
||||||
flash('Current password is incorrect', 'danger')
|
flash("Current password is incorrect", "danger")
|
||||||
return redirect(url_for('dashboard.settings'))
|
return redirect(url_for("dashboard.settings"))
|
||||||
|
|
||||||
# Update password
|
# Update password
|
||||||
current_user.set_password(new_password)
|
current_user.set_password(new_password)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
flash('Password updated successfully', 'success')
|
flash("Password updated successfully", "success")
|
||||||
return redirect(url_for('dashboard.settings'))
|
return redirect(url_for("dashboard.settings"))
|
||||||
|
|
||||||
return render_template(
|
return render_template("dashboard/settings.html", title="User Settings")
|
||||||
'dashboard/settings.html',
|
|
||||||
title='User Settings'
|
|
||||||
)
|
|
||||||
|
|
|
@ -5,18 +5,15 @@ import csv
|
||||||
import io
|
import io
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
bp = Blueprint('importexport', __name__, url_prefix='/import-export')
|
bp = Blueprint("importexport", __name__, url_prefix="/import-export")
|
||||||
|
|
||||||
MODEL_MAP = {
|
MODEL_MAP = {"subnet": Subnet, "server": Server, "app": App}
|
||||||
'subnet': Subnet,
|
|
||||||
'server': Server,
|
|
||||||
'app': App
|
|
||||||
}
|
|
||||||
|
|
||||||
@bp.route('/export/<model_name>', methods=['GET'])
|
|
||||||
|
@bp.route("/export/<model_name>", methods=["GET"])
|
||||||
def export_model(model_name):
|
def export_model(model_name):
|
||||||
if model_name not in MODEL_MAP:
|
if model_name not in MODEL_MAP:
|
||||||
return jsonify({'error': 'Invalid model name'}), 400
|
return jsonify({"error": "Invalid model name"}), 400
|
||||||
|
|
||||||
model = MODEL_MAP[model_name]
|
model = MODEL_MAP[model_name]
|
||||||
instances = model.query.all()
|
instances = model.query.all()
|
||||||
|
@ -35,36 +32,37 @@ def export_model(model_name):
|
||||||
writer.writerow(row)
|
writer.writerow(row)
|
||||||
|
|
||||||
# Create response
|
# Create response
|
||||||
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
filename = f"{model_name}_{timestamp}.csv"
|
filename = f"{model_name}_{timestamp}.csv"
|
||||||
|
|
||||||
response = make_response(output.getvalue())
|
response = make_response(output.getvalue())
|
||||||
response.headers['Content-Disposition'] = f'attachment; filename={filename}'
|
response.headers["Content-Disposition"] = f"attachment; filename={filename}"
|
||||||
response.headers['Content-type'] = 'text/csv'
|
response.headers["Content-type"] = "text/csv"
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@bp.route('/import/<model_name>', methods=['GET', 'POST'])
|
|
||||||
|
@bp.route("/import/<model_name>", methods=["GET", "POST"])
|
||||||
def import_model(model_name):
|
def import_model(model_name):
|
||||||
if model_name not in MODEL_MAP:
|
if model_name not in MODEL_MAP:
|
||||||
return jsonify({'error': 'Invalid model name'}), 400
|
return jsonify({"error": "Invalid model name"}), 400
|
||||||
|
|
||||||
model = MODEL_MAP[model_name]
|
model = MODEL_MAP[model_name]
|
||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == "GET":
|
||||||
# Show import form
|
# Show import form
|
||||||
return render_template('import_form.html', model_name=model_name)
|
return render_template("import_form.html", model_name=model_name)
|
||||||
|
|
||||||
# Process CSV upload
|
# Process CSV upload
|
||||||
if 'file' not in request.files:
|
if "file" not in request.files:
|
||||||
return jsonify({'error': 'No file part'}), 400
|
return jsonify({"error": "No file part"}), 400
|
||||||
|
|
||||||
file = request.files['file']
|
file = request.files["file"]
|
||||||
if file.filename == '':
|
if file.filename == "":
|
||||||
return jsonify({'error': 'No selected file'}), 400
|
return jsonify({"error": "No selected file"}), 400
|
||||||
|
|
||||||
if not file.filename.endswith('.csv'):
|
if not file.filename.endswith(".csv"):
|
||||||
return jsonify({'error': 'File must be CSV format'}), 400
|
return jsonify({"error": "File must be CSV format"}), 400
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Read CSV
|
# Read CSV
|
||||||
|
@ -75,12 +73,15 @@ def import_model(model_name):
|
||||||
headers = next(csv_reader)
|
headers = next(csv_reader)
|
||||||
|
|
||||||
# Validate required columns
|
# Validate required columns
|
||||||
required_columns = [col.name for col in model.__table__.columns
|
required_columns = [
|
||||||
if not col.nullable and col.name != 'id']
|
col.name
|
||||||
|
for col in model.__table__.columns
|
||||||
|
if not col.nullable and col.name != "id"
|
||||||
|
]
|
||||||
|
|
||||||
for col in required_columns:
|
for col in required_columns:
|
||||||
if col not in headers:
|
if col not in headers:
|
||||||
return jsonify({'error': f'Required column {col} missing'}), 400
|
return jsonify({"error": f"Required column {col} missing"}), 400
|
||||||
|
|
||||||
# Process rows
|
# Process rows
|
||||||
imported = 0
|
imported = 0
|
||||||
|
@ -88,8 +89,8 @@ def import_model(model_name):
|
||||||
data = dict(zip(headers, row))
|
data = dict(zip(headers, row))
|
||||||
|
|
||||||
# Remove id to create new record
|
# Remove id to create new record
|
||||||
if 'id' in data:
|
if "id" in data:
|
||||||
del data['id']
|
del data["id"]
|
||||||
|
|
||||||
# Create new instance
|
# Create new instance
|
||||||
instance = model(**data)
|
instance = model(**data)
|
||||||
|
@ -97,8 +98,8 @@ def import_model(model_name):
|
||||||
imported += 1
|
imported += 1
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return jsonify({'success': f'Imported {imported} records successfully'})
|
return jsonify({"success": f"Imported {imported} records successfully"})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
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
|
||||||
from app.core.models import Subnet, Server
|
from app.core.models import Subnet, Server, App
|
||||||
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
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
|
|
||||||
bp = Blueprint('ipam', __name__, url_prefix='/ipam')
|
bp = Blueprint("ipam", __name__, url_prefix="/ipam")
|
||||||
|
|
||||||
@bp.route('/')
|
|
||||||
|
@bp.route("/")
|
||||||
@login_required
|
@login_required
|
||||||
def ipam_home():
|
def ipam_home():
|
||||||
"""Main IPAM dashboard"""
|
"""Main IPAM dashboard"""
|
||||||
|
@ -18,51 +19,44 @@ def ipam_home():
|
||||||
# Calculate usage for each subnet
|
# Calculate usage for each subnet
|
||||||
for subnet in subnets:
|
for subnet in subnets:
|
||||||
network = ipaddress.ip_network(subnet.cidr, strict=False)
|
network = ipaddress.ip_network(subnet.cidr, strict=False)
|
||||||
max_hosts = network.num_addresses - 2 if network.prefixlen < 31 else network.num_addresses
|
max_hosts = (
|
||||||
|
network.num_addresses - 2
|
||||||
|
if network.prefixlen < 31
|
||||||
|
else network.num_addresses
|
||||||
|
)
|
||||||
used_count = Server.query.filter_by(subnet_id=subnet.id).count()
|
used_count = Server.query.filter_by(subnet_id=subnet.id).count()
|
||||||
subnet.usage_percent = (used_count / max_hosts) * 100 if max_hosts > 0 else 0
|
subnet.usage_percent = (used_count / max_hosts) * 100 if max_hosts > 0 else 0
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'ipam/index.html',
|
"ipam/index.html", title="IPAM Dashboard", subnets=subnets, now=datetime.now()
|
||||||
title='IPAM Dashboard',
|
|
||||||
subnets=subnets,
|
|
||||||
now=datetime.now()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@bp.route('/subnet/new', methods=['GET', 'POST'])
|
|
||||||
|
@bp.route("/subnet/new", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def subnet_new():
|
def subnet_new():
|
||||||
"""Create a new subnet"""
|
"""Create a new subnet"""
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
cidr = request.form.get('cidr')
|
cidr = request.form.get("cidr")
|
||||||
location = request.form.get('location')
|
location = request.form.get("location")
|
||||||
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:
|
||||||
flash('Please fill in all required fields', 'danger')
|
flash("Please fill in all required fields", "danger")
|
||||||
return render_template(
|
return render_template("ipam/subnet_form.html", title="New Subnet")
|
||||||
'ipam/subnet_form.html',
|
|
||||||
title='New Subnet'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate CIDR format
|
# Validate CIDR format
|
||||||
try:
|
try:
|
||||||
ipaddress.ip_network(cidr, strict=False)
|
ipaddress.ip_network(cidr, strict=False)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
flash('Invalid CIDR format', 'danger')
|
flash("Invalid CIDR format", "danger")
|
||||||
return render_template(
|
return render_template("ipam/subnet_form.html", title="New Subnet")
|
||||||
'ipam/subnet_form.html',
|
|
||||||
title='New Subnet'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if CIDR already exists
|
# Check if CIDR already exists
|
||||||
if Subnet.query.filter_by(cidr=cidr).first():
|
if Subnet.query.filter_by(cidr=cidr).first():
|
||||||
flash('Subnet already exists', 'danger')
|
flash("Subnet already exists", "danger")
|
||||||
return render_template(
|
return render_template("ipam/subnet_form.html", title="New Subnet")
|
||||||
'ipam/subnet_form.html',
|
|
||||||
title='New Subnet'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create new subnet with JSON string for active_hosts, not a Python list
|
# Create new subnet with JSON string for active_hosts, not a Python list
|
||||||
subnet = Subnet(
|
subnet = Subnet(
|
||||||
|
@ -70,76 +64,81 @@ def subnet_new():
|
||||||
location=location,
|
location=location,
|
||||||
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.add(subnet)
|
db.session.add(subnet)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
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(
|
return render_template("ipam/subnet_form.html", title="New Subnet")
|
||||||
'ipam/subnet_form.html',
|
|
||||||
title='New Subnet'
|
|
||||||
)
|
|
||||||
|
|
||||||
@bp.route('/subnet/<int:subnet_id>')
|
|
||||||
|
@bp.route("/subnet/<int:subnet_id>")
|
||||||
@login_required
|
@login_required
|
||||||
def subnet_view(subnet_id):
|
def subnet_view(subnet_id):
|
||||||
"""View a specific subnet"""
|
"""View a subnet and all its hosts"""
|
||||||
subnet = Subnet.query.get_or_404(subnet_id)
|
subnet = Subnet.query.get_or_404(subnet_id)
|
||||||
|
|
||||||
# Get all servers in this subnet
|
# Get servers in this subnet
|
||||||
servers = Server.query.filter_by(subnet_id=subnet_id).all()
|
servers = Server.query.filter_by(subnet_id=subnet_id).all()
|
||||||
|
|
||||||
# Parse CIDR for display
|
# Get applications in this subnet
|
||||||
|
subnet_apps = []
|
||||||
|
for server in servers:
|
||||||
|
apps = App.query.filter_by(server_id=server.id).all()
|
||||||
|
subnet_apps.extend(apps)
|
||||||
|
|
||||||
|
# Calculate usage statistics
|
||||||
network = ipaddress.ip_network(subnet.cidr, strict=False)
|
network = ipaddress.ip_network(subnet.cidr, strict=False)
|
||||||
subnet_info = {
|
total_ips = network.num_addresses - 2 # Subtract network and broadcast addresses
|
||||||
'network_address': str(network.network_address),
|
used_ips = Server.query.filter_by(subnet_id=subnet_id).count()
|
||||||
'broadcast_address': str(network.broadcast_address),
|
|
||||||
'netmask': str(network.netmask),
|
|
||||||
'num_addresses': network.num_addresses,
|
|
||||||
'host_range': f"{str(network.network_address + 1)} - {str(network.broadcast_address - 1)}" if network.prefixlen < 31 else subnet.cidr
|
|
||||||
}
|
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'ipam/subnet_view.html',
|
"ipam/subnet_view.html",
|
||||||
title=subnet.cidr,
|
title=f"Subnet {subnet.cidr}",
|
||||||
subnet=subnet,
|
subnet=subnet,
|
||||||
subnet_info=subnet_info,
|
|
||||||
servers=servers,
|
servers=servers,
|
||||||
now=datetime.now()
|
subnet_apps=subnet_apps,
|
||||||
|
total_ips=total_ips,
|
||||||
|
used_ips=used_ips,
|
||||||
)
|
)
|
||||||
|
|
||||||
@bp.route('/subnet/<int:subnet_id>/edit', methods=['GET', 'POST'])
|
|
||||||
|
@bp.route("/subnet/<int:subnet_id>/edit", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def subnet_edit(subnet_id):
|
def subnet_edit(subnet_id):
|
||||||
"""Edit a subnet"""
|
"""Edit a subnet"""
|
||||||
subnet = Subnet.query.get_or_404(subnet_id)
|
subnet = Subnet.query.get_or_404(subnet_id)
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
cidr = request.form.get('cidr')
|
cidr = request.form.get("cidr")
|
||||||
location = request.form.get('location')
|
location = request.form.get("location")
|
||||||
auto_scan = request.form.get('auto_scan') == 'on'
|
auto_scan = request.form.get("auto_scan") == "on"
|
||||||
|
|
||||||
# Validate inputs
|
# Validate inputs
|
||||||
if not all([cidr, location]):
|
if not all([cidr, location]):
|
||||||
flash('All fields are required', 'danger')
|
flash("All fields are required", "danger")
|
||||||
return render_template('ipam/subnet_form.html',
|
return render_template(
|
||||||
title='Edit Subnet',
|
"ipam/subnet_form.html",
|
||||||
|
title="Edit Subnet",
|
||||||
subnet=subnet,
|
subnet=subnet,
|
||||||
edit_mode=True)
|
edit_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
# Validate CIDR format
|
# Validate CIDR format
|
||||||
try:
|
try:
|
||||||
ipaddress.ip_network(cidr, strict=False)
|
ipaddress.ip_network(cidr, strict=False)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
flash('Invalid CIDR format', 'danger')
|
flash("Invalid CIDR format", "danger")
|
||||||
return render_template('ipam/subnet_form.html',
|
return render_template(
|
||||||
title='Edit Subnet',
|
"ipam/subnet_form.html",
|
||||||
|
title="Edit Subnet",
|
||||||
subnet=subnet,
|
subnet=subnet,
|
||||||
edit_mode=True)
|
edit_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
# Update subnet
|
# Update subnet
|
||||||
subnet.cidr = cidr
|
subnet.cidr = cidr
|
||||||
|
@ -148,18 +147,18 @@ def subnet_edit(subnet_id):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(f'Subnet {cidr} has been updated', 'success')
|
flash(f"Subnet {cidr} has been updated", "success")
|
||||||
return redirect(url_for('ipam.subnet_view', subnet_id=subnet.id))
|
return redirect(url_for("ipam.subnet_view", subnet_id=subnet.id))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
flash(f'Error updating subnet: {str(e)}', 'danger')
|
flash(f"Error updating subnet: {str(e)}", "danger")
|
||||||
|
|
||||||
return render_template('ipam/subnet_form.html',
|
return render_template(
|
||||||
title='Edit Subnet',
|
"ipam/subnet_form.html", title="Edit Subnet", subnet=subnet, edit_mode=True
|
||||||
subnet=subnet,
|
)
|
||||||
edit_mode=True)
|
|
||||||
|
|
||||||
@bp.route('/subnet/<int:subnet_id>/delete', methods=['POST'])
|
|
||||||
|
@bp.route("/subnet/<int:subnet_id>/delete", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def subnet_delete(subnet_id):
|
def subnet_delete(subnet_id):
|
||||||
"""Delete a subnet"""
|
"""Delete a subnet"""
|
||||||
|
@ -168,16 +167,20 @@ def subnet_delete(subnet_id):
|
||||||
# Check if subnet has servers
|
# Check if subnet has servers
|
||||||
servers_count = Server.query.filter_by(subnet_id=subnet_id).count()
|
servers_count = Server.query.filter_by(subnet_id=subnet_id).count()
|
||||||
if servers_count > 0:
|
if servers_count > 0:
|
||||||
flash(f'Cannot delete subnet {subnet.cidr}. It has {servers_count} servers assigned.', 'danger')
|
flash(
|
||||||
return redirect(url_for('ipam.subnet_view', subnet_id=subnet_id))
|
f"Cannot delete subnet {subnet.cidr}. It has {servers_count} servers assigned.",
|
||||||
|
"danger",
|
||||||
|
)
|
||||||
|
return redirect(url_for("ipam.subnet_view", subnet_id=subnet_id))
|
||||||
|
|
||||||
db.session.delete(subnet)
|
db.session.delete(subnet)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
flash(f'Subnet {subnet.cidr} has been deleted', 'success')
|
flash(f"Subnet {subnet.cidr} has been deleted", "success")
|
||||||
return redirect(url_for('ipam.ipam_home'))
|
return redirect(url_for("ipam.ipam_home"))
|
||||||
|
|
||||||
@bp.route('/subnet/<int:subnet_id>/scan', methods=['POST'])
|
|
||||||
|
@bp.route("/subnet/<int:subnet_id>/scan", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def subnet_scan(subnet_id):
|
def subnet_scan(subnet_id):
|
||||||
"""Manually scan a subnet"""
|
"""Manually scan a subnet"""
|
||||||
|
@ -187,14 +190,15 @@ def subnet_scan(subnet_id):
|
||||||
# Call the scan function with manual_trigger=True
|
# Call the scan function with manual_trigger=True
|
||||||
scan(subnet, manual_trigger=True)
|
scan(subnet, manual_trigger=True)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(f'Scan completed for subnet {subnet.cidr}', 'success')
|
flash(f"Scan completed for subnet {subnet.cidr}", "success")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
flash(f'Error scanning subnet: {str(e)}', 'danger')
|
flash(f"Error scanning subnet: {str(e)}", "danger")
|
||||||
|
|
||||||
return redirect(url_for('ipam.subnet_view', subnet_id=subnet_id))
|
return redirect(url_for("ipam.subnet_view", subnet_id=subnet_id))
|
||||||
|
|
||||||
@bp.route('/subnet/<int:subnet_id>/force-delete', methods=['POST'])
|
|
||||||
|
@bp.route("/subnet/<int:subnet_id>/force-delete", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def subnet_force_delete(subnet_id):
|
def subnet_force_delete(subnet_id):
|
||||||
"""Force delete a subnet and all its related servers and applications"""
|
"""Force delete a subnet and all its related servers and applications"""
|
||||||
|
@ -209,52 +213,55 @@ def subnet_force_delete(subnet_id):
|
||||||
db.session.delete(subnet)
|
db.session.delete(subnet)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
flash(f'Subnet {subnet.cidr} and {server_count} related servers were deleted successfully', 'success')
|
flash(
|
||||||
return redirect(url_for('dashboard.ipam_home'))
|
f"Subnet {subnet.cidr} and {server_count} related servers were deleted successfully",
|
||||||
|
"success",
|
||||||
|
)
|
||||||
|
return redirect(url_for("dashboard.ipam_home"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
flash(f'Error deleting subnet: {str(e)}', 'danger')
|
flash(f"Error deleting subnet: {str(e)}", "danger")
|
||||||
return redirect(url_for('dashboard.subnet_view', subnet_id=subnet_id))
|
return redirect(url_for("dashboard.subnet_view", subnet_id=subnet_id))
|
||||||
|
|
||||||
@bp.route('/subnet/create-ajax', methods=['POST'])
|
|
||||||
|
@bp.route("/subnet/create-ajax", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def subnet_create_ajax():
|
def subnet_create_ajax():
|
||||||
"""Create a subnet via AJAX"""
|
"""Create a subnet via AJAX"""
|
||||||
data = request.json
|
data = request.json
|
||||||
if not data:
|
if not data:
|
||||||
return jsonify({'success': False, 'error': 'No data provided'})
|
return jsonify({"success": False, "error": "No data provided"})
|
||||||
|
|
||||||
cidr = data.get('cidr')
|
cidr = data.get("cidr")
|
||||||
location = data.get('location')
|
location = data.get("location")
|
||||||
auto_scan = data.get('auto_scan', False)
|
auto_scan = data.get("auto_scan", False)
|
||||||
|
|
||||||
if not cidr or not location:
|
if not cidr or not location:
|
||||||
return jsonify({'success': False, 'error': 'CIDR and location are required'})
|
return jsonify({"success": False, "error": "CIDR and location are required"})
|
||||||
|
|
||||||
# Validate CIDR
|
# Validate CIDR
|
||||||
try:
|
try:
|
||||||
network = ipaddress.ip_network(cidr, strict=False)
|
network = ipaddress.ip_network(cidr, strict=False)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return jsonify({'success': False, 'error': f'Invalid CIDR: {str(e)}'})
|
return jsonify({"success": False, "error": f"Invalid CIDR: {str(e)}"})
|
||||||
|
|
||||||
# Create subnet
|
# Create subnet
|
||||||
subnet = Subnet(
|
subnet = Subnet(
|
||||||
cidr=cidr,
|
cidr=cidr, location=location, auto_scan=auto_scan, active_hosts=json.dumps([])
|
||||||
location=location,
|
|
||||||
auto_scan=auto_scan,
|
|
||||||
active_hosts=json.dumps([])
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db.session.add(subnet)
|
db.session.add(subnet)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify({
|
return jsonify(
|
||||||
'success': True,
|
{
|
||||||
'subnet_id': subnet.id,
|
"success": True,
|
||||||
'cidr': subnet.cidr,
|
"subnet_id": subnet.id,
|
||||||
'location': subnet.location
|
"cidr": subnet.cidr,
|
||||||
})
|
"location": subnet.location,
|
||||||
|
}
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
return jsonify({'success': False, 'error': str(e)})
|
return jsonify({"success": False, "error": str(e)})
|
||||||
|
|
96
app/routes/static.py
Normal file
96
app/routes/static.py
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
from flask import Blueprint, send_from_directory, current_app
|
||||||
|
import os
|
||||||
|
|
||||||
|
bp = Blueprint("static_assets", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/static/libs/tabler-icons/tabler-icons.min.css")
|
||||||
|
def tabler_icons():
|
||||||
|
"""Serve tabler-icons CSS from node_modules or download if missing"""
|
||||||
|
icons_path = os.path.join(current_app.static_folder, "libs", "tabler-icons")
|
||||||
|
|
||||||
|
# Create directory if it doesn't exist
|
||||||
|
if not os.path.exists(icons_path):
|
||||||
|
os.makedirs(icons_path)
|
||||||
|
|
||||||
|
css_file = os.path.join(icons_path, "tabler-icons.min.css")
|
||||||
|
|
||||||
|
# If file doesn't exist, download from CDN
|
||||||
|
if not os.path.exists(css_file):
|
||||||
|
import requests
|
||||||
|
|
||||||
|
try:
|
||||||
|
cdn_url = "https://cdn.jsdelivr.net/npm/@tabler/icons@latest/iconfont/tabler-icons.min.css"
|
||||||
|
response = requests.get(cdn_url)
|
||||||
|
if response.status_code == 200:
|
||||||
|
with open(css_file, "wb") as f:
|
||||||
|
f.write(response.content)
|
||||||
|
print(f"Downloaded tabler-icons.min.css from CDN")
|
||||||
|
else:
|
||||||
|
print(f"Failed to download tabler-icons CSS: {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error downloading tabler-icons CSS: {e}")
|
||||||
|
|
||||||
|
return send_from_directory(icons_path, "tabler-icons.min.css")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/static/css/tabler.min.css")
|
||||||
|
def tabler_css():
|
||||||
|
"""Serve tabler CSS from static folder or download if missing"""
|
||||||
|
css_path = os.path.join(current_app.static_folder, "css")
|
||||||
|
|
||||||
|
# Create directory if it doesn't exist
|
||||||
|
if not os.path.exists(css_path):
|
||||||
|
os.makedirs(css_path)
|
||||||
|
|
||||||
|
css_file = os.path.join(css_path, "tabler.min.css")
|
||||||
|
|
||||||
|
# If file doesn't exist, download from CDN
|
||||||
|
if not os.path.exists(css_file):
|
||||||
|
import requests
|
||||||
|
|
||||||
|
try:
|
||||||
|
cdn_url = "https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/css/tabler.min.css"
|
||||||
|
response = requests.get(cdn_url)
|
||||||
|
if response.status_code == 200:
|
||||||
|
with open(css_file, "wb") as f:
|
||||||
|
f.write(response.content)
|
||||||
|
print(f"Downloaded tabler.min.css from CDN")
|
||||||
|
else:
|
||||||
|
print(f"Failed to download tabler CSS: {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error downloading tabler CSS: {e}")
|
||||||
|
|
||||||
|
return send_from_directory(css_path, "tabler.min.css")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/static/img/favicon.png")
|
||||||
|
def favicon():
|
||||||
|
"""Serve favicon from static folder or create a default one if missing"""
|
||||||
|
img_path = os.path.join(current_app.static_folder, "img")
|
||||||
|
|
||||||
|
# Create directory if it doesn't exist
|
||||||
|
if not os.path.exists(img_path):
|
||||||
|
os.makedirs(img_path)
|
||||||
|
|
||||||
|
favicon_file = os.path.join(img_path, "favicon.png")
|
||||||
|
|
||||||
|
# If file doesn't exist, create a simple one
|
||||||
|
if not os.path.exists(favicon_file):
|
||||||
|
# Try to download a default favicon
|
||||||
|
import requests
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Using a simple placeholder favicon
|
||||||
|
cdn_url = "https://www.google.com/favicon.ico"
|
||||||
|
response = requests.get(cdn_url)
|
||||||
|
if response.status_code == 200:
|
||||||
|
with open(favicon_file, "wb") as f:
|
||||||
|
f.write(response.content)
|
||||||
|
print(f"Created default favicon.png")
|
||||||
|
else:
|
||||||
|
print(f"Failed to download favicon: {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating favicon: {e}")
|
||||||
|
|
||||||
|
return send_from_directory(img_path, "favicon.png")
|
Binary file not shown.
|
@ -3,46 +3,47 @@ from app.core.models import Subnet, Server, App, Port
|
||||||
from app.core.auth import User # Import User from auth module
|
from app.core.auth import User # Import User from auth module
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
def seed_database():
|
def seed_database():
|
||||||
"""Add sample data to the database"""
|
"""Add sample data to the database"""
|
||||||
# Create a default subnet if none exists
|
# Create a default subnet if none exists
|
||||||
if Subnet.query.count() == 0:
|
if Subnet.query.count() == 0:
|
||||||
subnet = Subnet(
|
subnet = Subnet(
|
||||||
cidr='192.168.1.0/24',
|
cidr="192.168.1.0/24",
|
||||||
location='Office',
|
location="Office",
|
||||||
auto_scan=True,
|
auto_scan=True,
|
||||||
active_hosts=json.dumps([])
|
active_hosts=json.dumps([]),
|
||||||
)
|
)
|
||||||
db.session.add(subnet)
|
db.session.add(subnet)
|
||||||
|
|
||||||
# Create a sample server
|
# Create a sample server
|
||||||
server = Server(
|
server = Server(
|
||||||
hostname='server1',
|
hostname="server1",
|
||||||
ip_address='192.168.1.10',
|
ip_address="192.168.1.10",
|
||||||
subnet=subnet,
|
subnet=subnet,
|
||||||
documentation='# Server 1\n\nThis is a sample server.'
|
documentation="# Server 1\n\nThis is a sample server.",
|
||||||
)
|
)
|
||||||
db.session.add(server)
|
db.session.add(server)
|
||||||
|
|
||||||
# Create a sample app
|
# Create a sample app
|
||||||
app = App(
|
app = App(
|
||||||
name='Web App',
|
name="Web App",
|
||||||
server=server,
|
server=server,
|
||||||
documentation='# Welcome to Web App\n\nThis is a sample application.'
|
documentation="# Welcome to Web App\n\nThis is a sample application.",
|
||||||
)
|
)
|
||||||
db.session.add(app)
|
db.session.add(app)
|
||||||
|
|
||||||
# Add some ports
|
# Add some ports
|
||||||
ports = [
|
ports = [
|
||||||
Port(app=app, port_number=80, protocol='TCP', description='HTTP'),
|
Port(app=app, port_number=80, protocol="TCP", description="HTTP"),
|
||||||
Port(app=app, port_number=443, protocol='TCP', description='HTTPS')
|
Port(app=app, port_number=443, protocol="TCP", description="HTTPS"),
|
||||||
]
|
]
|
||||||
db.session.add_all(ports)
|
db.session.add_all(ports)
|
||||||
|
|
||||||
# Create a default user if none exists
|
# Create a default user if none exists
|
||||||
if User.query.count() == 0:
|
if User.query.count() == 0:
|
||||||
admin = User(username='admin', email='admin@example.com', is_admin=True)
|
admin = User(username="admin", email="admin@example.com", is_admin=True)
|
||||||
admin.set_password('admin')
|
admin.set_password("admin")
|
||||||
db.session.add(admin)
|
db.session.add(admin)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -11,6 +11,7 @@ import concurrent.futures
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
|
|
||||||
def scan(subnet, manual_trigger=False):
|
def scan(subnet, manual_trigger=False):
|
||||||
"""
|
"""
|
||||||
Scan a subnet for active hosts
|
Scan a subnet for active hosts
|
||||||
|
@ -43,29 +44,35 @@ def scan(subnet, manual_trigger=False):
|
||||||
print(f"Error scanning subnet {subnet.cidr}: {str(e)}")
|
print(f"Error scanning subnet {subnet.cidr}: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def scan_worker(ip_list, results, index):
|
def scan_worker(ip_list, results, index):
|
||||||
"""Worker function for threading"""
|
"""Worker function for threading"""
|
||||||
for ip in ip_list:
|
for ip in ip_list:
|
||||||
if ping(ip):
|
if ping(ip):
|
||||||
hostname = get_hostname(ip)
|
hostname = get_hostname(ip)
|
||||||
results[index].append({
|
results[index].append(
|
||||||
'ip': str(ip),
|
{
|
||||||
'hostname': hostname if hostname else str(ip),
|
"ip": str(ip),
|
||||||
'status': 'up'
|
"hostname": hostname if hostname else str(ip),
|
||||||
})
|
"status": "up",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def ping(host):
|
def ping(host):
|
||||||
"""
|
"""
|
||||||
Returns True if host responds to a ping request
|
Returns True if host responds to a ping request
|
||||||
"""
|
"""
|
||||||
# Ping parameters based on OS
|
# Ping parameters based on OS
|
||||||
param = '-n' if platform.system().lower() == 'windows' else '-c'
|
param = "-n" if platform.system().lower() == "windows" else "-c"
|
||||||
# Build the command
|
# Build the command
|
||||||
command = ['ping', param, '1', '-w', '1', host]
|
command = ["ping", param, "1", "-w", "1", host]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Run the command and capture output
|
# Run the command and capture output
|
||||||
output = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=2)
|
output = subprocess.run(
|
||||||
|
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=2
|
||||||
|
)
|
||||||
# Return True if ping was successful
|
# Return True if ping was successful
|
||||||
return output.returncode == 0
|
return output.returncode == 0
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
|
@ -73,6 +80,7 @@ def ping(host):
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_hostname(ip):
|
def get_hostname(ip):
|
||||||
"""Try to get the hostname for an IP address"""
|
"""Try to get the hostname for an IP address"""
|
||||||
try:
|
try:
|
||||||
|
@ -81,25 +89,32 @@ def get_hostname(ip):
|
||||||
except (socket.herror, socket.gaierror):
|
except (socket.herror, socket.gaierror):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def is_host_active_ping(ip):
|
def is_host_active_ping(ip):
|
||||||
"""Simple ICMP ping test (platform dependent)"""
|
"""Simple ICMP ping test (platform dependent)"""
|
||||||
import platform
|
import platform
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
param = '-n' if platform.system().lower() == 'windows' else '-c'
|
param = "-n" if platform.system().lower() == "windows" else "-c"
|
||||||
command = ['ping', param, '1', '-w', '1', ip]
|
command = ["ping", param, "1", "-w", "1", ip]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return subprocess.call(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0
|
return (
|
||||||
|
subprocess.call(
|
||||||
|
command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
||||||
|
)
|
||||||
|
== 0
|
||||||
|
)
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def save_scan_results(cidr, results):
|
def save_scan_results(cidr, results):
|
||||||
"""Save scan results to the database"""
|
"""Save scan results to the database"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
# Need to be in application context
|
# Need to be in application context
|
||||||
if not hasattr(current_app, 'app_context'):
|
if not hasattr(current_app, "app_context"):
|
||||||
print("Not in Flask application context, cannot save results")
|
print("Not in Flask application context, cannot save results")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -111,12 +126,15 @@ def save_scan_results(cidr, results):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get existing servers in this subnet
|
# Get existing servers in this subnet
|
||||||
existing_servers = {server.ip_address: server for server in Server.query.filter_by(subnet_id=subnet.id).all()}
|
existing_servers = {
|
||||||
|
server.ip_address: server
|
||||||
|
for server in Server.query.filter_by(subnet_id=subnet.id).all()
|
||||||
|
}
|
||||||
|
|
||||||
# Process scan results
|
# Process scan results
|
||||||
for host in results:
|
for host in results:
|
||||||
ip = host['ip']
|
ip = host["ip"]
|
||||||
hostname = host['hostname']
|
hostname = host["hostname"]
|
||||||
|
|
||||||
# Check if server already exists
|
# Check if server already exists
|
||||||
if ip in existing_servers:
|
if ip in existing_servers:
|
||||||
|
@ -130,7 +148,7 @@ def save_scan_results(cidr, results):
|
||||||
hostname=hostname,
|
hostname=hostname,
|
||||||
ip_address=ip,
|
ip_address=ip,
|
||||||
subnet_id=subnet.id,
|
subnet_id=subnet.id,
|
||||||
documentation=f"# {hostname}\n\nAutomatically discovered by network scan on {time.strftime('%Y-%m-%d %H:%M:%S')}"
|
documentation=f"# {hostname}\n\nAutomatically discovered by network scan on {time.strftime('%Y-%m-%d %H:%M:%S')}",
|
||||||
)
|
)
|
||||||
db.session.add(server)
|
db.session.add(server)
|
||||||
|
|
||||||
|
@ -141,6 +159,7 @@ def save_scan_results(cidr, results):
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
print(f"Error saving scan results: {e}")
|
print(f"Error saving scan results: {e}")
|
||||||
|
|
||||||
|
|
||||||
def schedule_subnet_scans():
|
def schedule_subnet_scans():
|
||||||
"""Schedule automatic scans for subnets marked as auto_scan"""
|
"""Schedule automatic scans for subnets marked as auto_scan"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
@ -152,11 +171,7 @@ def schedule_subnet_scans():
|
||||||
|
|
||||||
for subnet in subnets:
|
for subnet in subnets:
|
||||||
# Start a thread for each subnet
|
# Start a thread for each subnet
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(target=scan, args=(subnet,), daemon=True)
|
||||||
target=scan,
|
|
||||||
args=(subnet,),
|
|
||||||
daemon=True
|
|
||||||
)
|
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
# Sleep briefly to avoid overloading
|
# Sleep briefly to avoid overloading
|
||||||
|
|
9
app/static/css/tabler.min.css
vendored
Normal file
9
app/static/css/tabler.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
165
app/static/css/theme.css
Normal file
165
app/static/css/theme.css
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
/* Custom theme with unique color palette */
|
||||||
|
:root {
|
||||||
|
/* Base Colors - Teal and Purple Theme */
|
||||||
|
--primary-color: #3a86ff;
|
||||||
|
--secondary-color: #8338ec;
|
||||||
|
--accent-color: #ff006e;
|
||||||
|
--success-color: #06d6a0;
|
||||||
|
--info-color: #0dcaf0;
|
||||||
|
--warning-color: #ffbe0b;
|
||||||
|
--danger-color: #ef476f;
|
||||||
|
|
||||||
|
/* Light Mode */
|
||||||
|
--bg-color: #f8fafc;
|
||||||
|
--card-bg: #ffffff;
|
||||||
|
--text-color: #1a1d23;
|
||||||
|
--text-muted: #64748b;
|
||||||
|
--border-color: rgba(226, 232, 240, 0.8);
|
||||||
|
--hover-bg: rgba(226, 232, 240, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme="dark"] {
|
||||||
|
/* Dark Mode */
|
||||||
|
--bg-color: #0f172a;
|
||||||
|
--card-bg: #1e293b;
|
||||||
|
--text-color: #e2e8f0;
|
||||||
|
--text-muted: #94a3b8;
|
||||||
|
--border-color: rgba(51, 65, 85, 0.7);
|
||||||
|
--hover-bg: rgba(51, 65, 85, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override Bootstrap classes */
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #2563eb;
|
||||||
|
border-color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
border-color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-primary {
|
||||||
|
color: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-primary {
|
||||||
|
color: var(--primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-primary {
|
||||||
|
background-color: var(--primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card styling */
|
||||||
|
.card {
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background-color: transparent;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table styling */
|
||||||
|
.table {
|
||||||
|
--bs-table-hover-bg: var(--hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(100, 116, 139, 0.5);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(100, 116, 139, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Improved form controls */
|
||||||
|
.form-control,
|
||||||
|
.form-select {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus,
|
||||||
|
.form-select:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(58, 134, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar enhancements */
|
||||||
|
.sidebar {
|
||||||
|
background: linear-gradient(to bottom, #1a1d23, #0f172a);
|
||||||
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme="light"] .sidebar {
|
||||||
|
background: linear-gradient(to bottom, #f1f5f9, #e2e8f0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme="light"] .sidebar-item:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation for page transitions */
|
||||||
|
.page-transition {
|
||||||
|
animation: fadeIn 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pill badges */
|
||||||
|
.badge {
|
||||||
|
border-radius: 50rem;
|
||||||
|
padding: 0.35em 0.65em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.75em;
|
||||||
|
}
|
BIN
app/static/img/favicon.png
Normal file
BIN
app/static/img/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
|
@ -1,3 +1,7 @@
|
||||||
|
/**
|
||||||
|
* DITTO Application JavaScript
|
||||||
|
* Modern ES6+ syntax with proper error handling
|
||||||
|
*/
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
console.log('App script loaded.');
|
console.log('App script loaded.');
|
||||||
|
|
||||||
|
@ -76,8 +80,121 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
// Initialize notifications
|
// Initialize notifications
|
||||||
initNotifications();
|
initNotifications();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize Bootstrap components
|
||||||
|
initBootstrapComponents();
|
||||||
|
|
||||||
|
// Setup sidebar toggle functionality
|
||||||
|
setupSidebar();
|
||||||
|
|
||||||
|
// Add form validation
|
||||||
|
setupFormValidation();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Bootstrap components
|
||||||
|
*/
|
||||||
|
function initBootstrapComponents() {
|
||||||
|
// Initialize all tooltips
|
||||||
|
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||||
|
tooltipTriggerList.forEach(el => {
|
||||||
|
try {
|
||||||
|
new bootstrap.Tooltip(el);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error initializing tooltip:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize all popovers
|
||||||
|
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
|
||||||
|
popoverTriggerList.forEach(el => {
|
||||||
|
try {
|
||||||
|
new bootstrap.Popover(el);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error initializing popover:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup sidebar toggle functionality
|
||||||
|
*/
|
||||||
|
function setupSidebar() {
|
||||||
|
const sidebarToggler = document.querySelector('.sidebar-toggler');
|
||||||
|
const sidebar = document.querySelector('.sidebar');
|
||||||
|
|
||||||
|
if (sidebarToggler && sidebar) {
|
||||||
|
sidebarToggler.addEventListener('click', () => {
|
||||||
|
sidebar.classList.toggle('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close sidebar when clicking outside on mobile
|
||||||
|
document.addEventListener('click', (event) => {
|
||||||
|
const isClickInside = sidebar.contains(event.target) ||
|
||||||
|
sidebarToggler.contains(event.target);
|
||||||
|
|
||||||
|
if (!isClickInside && sidebar.classList.contains('show') && window.innerWidth < 992) {
|
||||||
|
sidebar.classList.remove('show');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup form validation
|
||||||
|
*/
|
||||||
|
function setupFormValidation() {
|
||||||
|
// Add custom validation for forms
|
||||||
|
const forms = document.querySelectorAll('.needs-validation');
|
||||||
|
|
||||||
|
forms.forEach(form => {
|
||||||
|
form.addEventListener('submit', event => {
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
form.classList.add('was-validated');
|
||||||
|
}, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safe query selector with error handling
|
||||||
|
* @param {string} selector - CSS selector
|
||||||
|
* @param {Element} parent - Parent element (optional)
|
||||||
|
* @returns {Element|null} - The selected element or null
|
||||||
|
*/
|
||||||
|
function $(selector, parent = document) {
|
||||||
|
try {
|
||||||
|
return parent.querySelector(selector);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Error selecting "${selector}":`, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format date for display
|
||||||
|
* @param {string|Date} date - Date to format
|
||||||
|
* @returns {string} - Formatted date string
|
||||||
|
*/
|
||||||
|
function formatDate(date) {
|
||||||
|
try {
|
||||||
|
const d = new Date(date);
|
||||||
|
return d.toLocaleDateString(undefined, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error formatting date:', e);
|
||||||
|
return String(date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function initTiptapEditor(element) {
|
function initTiptapEditor(element) {
|
||||||
// Load required Tiptap scripts
|
// Load required Tiptap scripts
|
||||||
const editorContainer = document.getElementById('editor-container');
|
const editorContainer = document.getElementById('editor-container');
|
||||||
|
|
4
app/static/libs/tabler-icons/tabler-icons.min.css
vendored
Normal file
4
app/static/libs/tabler-icons/tabler-icons.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,11 +1,14 @@
|
||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-narrow py-4">
|
<div class="container-tight py-4">
|
||||||
<div class="card card-md">
|
<div class="text-center mb-4">
|
||||||
<div class="card-body">
|
<h1>Create an account</h1>
|
||||||
<h2 class="card-title text-center mb-4">Create New Account</h2>
|
</div>
|
||||||
|
|
||||||
|
<form class="card card-md" method="POST" action="{{ url_for('auth.register') }}">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<div class="card-body">
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for category, message in messages %}
|
{% for category, message in messages %}
|
||||||
|
@ -17,33 +20,37 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<form method="POST" action="{{ url_for('auth.register') }}">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Email address</label>
|
<label class="form-label">Email address</label>
|
||||||
<input type="email" class="form-control" name="email" placeholder="your@email.com" required
|
<input type="email" name="email" class="form-control" placeholder="your@email.com" required>
|
||||||
autocomplete="username">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Username</label>
|
||||||
|
<input type="text" name="username" class="form-control" placeholder="Username" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Password</label>
|
<label class="form-label">Password</label>
|
||||||
<input type="password" class="form-control" name="password" placeholder="Password" required
|
<input type="password" name="password" class="form-control" placeholder="Password" required>
|
||||||
autocomplete="new-password">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Confirm Password</label>
|
||||||
|
<input type="password" name="password_confirm" class="form-control" placeholder="Confirm password" required>
|
||||||
|
<div class="form-text text-muted">
|
||||||
|
Make sure to use a strong, unique password
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-footer">
|
<div class="form-footer">
|
||||||
<button type="submit" class="btn btn-primary w-100">Create Account</button>
|
<button type="submit" class="btn btn-primary w-100">Create account</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
<div class="hr-text">or</div>
|
<div class="text-center text-muted mt-3">
|
||||||
<div class="card-body">
|
Already have an account? <a href="{{ url_for('auth.login') }}" tabindex="-1">Sign in</a>
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<a href="{{ url_for('auth.login') }}" class="btn w-100">
|
|
||||||
Login with existing account
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -6,8 +6,25 @@
|
||||||
<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 app %}Edit Application{% else %}Add New Application{% endif %}
|
{{ title }}
|
||||||
</h2>
|
</h2>
|
||||||
|
<div class="text-muted mt-1">
|
||||||
|
{% if edit_mode %}Edit{% else %}Create{% endif %} application details and configure ports
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto ms-auto">
|
||||||
|
<div class="btn-list">
|
||||||
|
<a href="{{ dashboard_link }}" class="btn btn-outline-primary">
|
||||||
|
<span class="ti ti-dashboard"></span>
|
||||||
|
Dashboard
|
||||||
|
</a>
|
||||||
|
{% if edit_mode %}
|
||||||
|
<a href="{{ url_for('dashboard.app_view', app_id=app.id) }}" class="btn btn-outline-secondary">
|
||||||
|
<span class="ti ti-eye"></span>
|
||||||
|
View Application
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,69 +43,99 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<form method="POST"
|
<form method="POST"
|
||||||
action="{% if app %}{{ url_for('dashboard.app_edit', app_id=app.id) }}{% else %}{{ url_for('dashboard.app_new') }}{% endif %}">
|
action="{{ url_for('dashboard.app_edit', app_id=app.id) if edit_mode else url_for('dashboard.app_new') }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label required">Application Name</label>
|
<label class="form-label required">Application Name</label>
|
||||||
<input type="text" class="form-control" name="name" required value="{% if app %}{{ app.name }}{% endif %}">
|
<input type="text" class="form-control" name="name" value="{{ app.name if app else '' }}" required
|
||||||
|
placeholder="Enter application name">
|
||||||
|
<small class="form-hint">Choose a unique name for this application</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label required">Server</label>
|
<label class="form-label required">Server</label>
|
||||||
<select class="form-select" name="server_id" required>
|
<select class="form-select" name="server_id" required>
|
||||||
<option value="">Select a server</option>
|
<option value="">Select a server</option>
|
||||||
{% for server in servers %}
|
{% for server in servers %}
|
||||||
<option value="{{ server.id }}" {% if app and app.server_id==server.id %}selected {% elif server_id and
|
<option value="{{ server.id }}" {% if app and server.id==app.server_id %}selected{% endif %}>
|
||||||
server.id|string==server_id|string %}selected{% endif %}>
|
|
||||||
{{ server.hostname }} ({{ server.ip_address }})
|
{{ server.hostname }} ({{ server.ip_address }})
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
<small class="form-hint">Select the server where this application runs</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Documentation</label>
|
<label class="form-label">Documentation</label>
|
||||||
<textarea class="form-control" name="documentation"
|
<ul class="nav nav-tabs mb-2" role="tablist">
|
||||||
rows="10">{% if app %}{{ app.documentation }}{% endif %}</textarea>
|
<li class="nav-item" role="presentation">
|
||||||
<div class="form-text">Markdown is supported</div>
|
<a href="#markdown-edit" class="nav-link active" data-bs-toggle="tab" role="tab">Edit</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<a href="#markdown-preview" class="nav-link" data-bs-toggle="tab" role="tab" id="preview-tab">Preview</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="tab-pane active" id="markdown-edit" role="tabpanel">
|
||||||
|
<textarea class="form-control" name="documentation" id="documentation" rows="6"
|
||||||
|
placeholder="Document your application using Markdown...">{{ app.documentation if app else '' }}</textarea>
|
||||||
|
<small class="form-hint">
|
||||||
|
Markdown formatting is supported. Include details about what this application does, contact info, etc.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane" id="markdown-preview" role="tabpanel">
|
||||||
|
<div class="markdown-content border rounded p-3" style="min-height: 12rem;">
|
||||||
|
<div id="preview-content">Preview will be shown here...</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-footer">
|
|
||||||
<button type="submit" class="btn btn-primary">Save</button>
|
|
||||||
{% if app %}
|
|
||||||
<a href="{{ url_for('dashboard.app_view', app_id=app.id) }}" class="btn btn-outline-secondary ms-2">Cancel</a>
|
|
||||||
{% elif server_id %}
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{% block extra_js %}
|
<div class="hr-text">Port Configuration</div>
|
||||||
<script>
|
|
||||||
let portRowIndex = 1;
|
|
||||||
|
|
||||||
function addPortRow() {
|
<div class="mb-3">
|
||||||
const tbody = document.querySelector('#ports-table tbody');
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
const tr = document.createElement('tr');
|
<label class="form-label mb-0">Application Ports</label>
|
||||||
tr.classList.add('port-row');
|
<div class="btn-group">
|
||||||
tr.innerHTML = `
|
<button type="button" class="btn btn-sm btn-outline-primary" id="add-port-btn">
|
||||||
|
<span class="ti ti-plus"></span> Add Port
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="random-port-btn"
|
||||||
|
title="Generate random available port">
|
||||||
|
<span class="ti ti-dice"></span> Random Port
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-vcenter card-table" id="ports-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 20%">Port Number</th>
|
||||||
|
<th style="width: 20%">Protocol</th>
|
||||||
|
<th style="width: 50%">Description</th>
|
||||||
|
<th style="width: 10%">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% if app and app.ports %}
|
||||||
|
{% for port in app.ports %}
|
||||||
|
<tr data-port-id="{{ port.id }}">
|
||||||
<td>
|
<td>
|
||||||
<input type="number" name="port_number_${portRowIndex}" class="form-control"
|
<input type="number" name="port_numbers[]" class="form-control" min="1" max="65535"
|
||||||
min="1" max="65535" placeholder="Port number">
|
value="{{ port.port_number }}" required>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<select name="protocol_${portRowIndex}" class="form-select">
|
<select name="protocols[]" class="form-select">
|
||||||
<option value="TCP">TCP</option>
|
<option value="TCP" {% if port.protocol=='TCP' %}selected{% endif %}>TCP</option>
|
||||||
<option value="UDP">UDP</option>
|
<option value="UDP" {% if port.protocol=='UDP' %}selected{% endif %}>UDP</option>
|
||||||
<option value="SCTP">SCTP</option>
|
<option value="SCTP" {% if port.protocol=='SCTP' %}selected{% endif %}>SCTP</option>
|
||||||
<option value="OTHER">OTHER</option>
|
<option value="OTHER" {% if port.protocol=='OTHER' %}selected{% endif %}>OTHER</option>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="description_${portRowIndex}" class="form-control"
|
<input type="text" name="port_descriptions[]" class="form-control" value="{{ port.description }}"
|
||||||
placeholder="Description">
|
placeholder="Description">
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -96,16 +143,48 @@
|
||||||
<span class="ti ti-trash"></span>
|
<span class="ti ti-trash"></span>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
`;
|
</tr>
|
||||||
tbody.appendChild(tr);
|
{% endfor %}
|
||||||
portRowIndex++;
|
{% endif %}
|
||||||
}
|
<!-- New rows will be added here dynamically -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<small class="form-hint">Configure the network ports used by this application</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
function removePortRow(button) {
|
<div class="form-footer">
|
||||||
const row = button.closest('tr');
|
<button type="submit" class="btn btn-primary">Save Application</button>
|
||||||
row.remove();
|
{% if edit_mode %}
|
||||||
}
|
<a href="{{ url_for('dashboard.app_view', app_id=app.id) }}" class="btn btn-outline-secondary ms-2">Cancel</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ dashboard_link }}" class="btn btn-outline-secondary ms-2">Cancel</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
{% if app and app.id %}
|
||||||
|
const appId = {{ app.id | tojson }};
|
||||||
|
{% else %}
|
||||||
|
const appId = null;
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
// Setup markdown preview
|
||||||
|
setupMarkdownPreview();
|
||||||
|
|
||||||
|
// Setup port management
|
||||||
|
setupPortHandlers();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.body.addEventListener('htmx:configRequest', (event) => {
|
||||||
|
event.detail.headers['X-CSRFToken'] = "{{ csrf_token() }}";
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -155,6 +155,69 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Applications in this Subnet Section -->
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card glass-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Applications in this Subnet</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if subnet_apps %}
|
||||||
|
<div class="app-grid">
|
||||||
|
{% for app in subnet_apps %}
|
||||||
|
<div class="app-card">
|
||||||
|
<div class="app-card-header">
|
||||||
|
<div class="app-card-title">
|
||||||
|
<h4>{{ app.name }}</h4>
|
||||||
|
<span class="text-muted small">on {{ app.server.hostname }} ({{ app.server.ip_address }})</span>
|
||||||
|
</div>
|
||||||
|
<div class="app-card-actions">
|
||||||
|
<a href="{{ url_for('dashboard.app_view', app_id=app.id) }}" class="btn btn-sm btn-outline-primary">
|
||||||
|
View
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="app-card-body markdown-content">
|
||||||
|
{% if app.documentation %}
|
||||||
|
{{ app.documentation|markdown|truncate(300, true) }}
|
||||||
|
<a href="{{ url_for('dashboard.app_view', app_id=app.id) }}" class="text-primary">Read more</a>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted">No documentation available for this application.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty">
|
||||||
|
<div class="empty-icon">
|
||||||
|
<span class="ti ti-app-window"></span>
|
||||||
|
</div>
|
||||||
|
<p class="empty-title">No applications found</p>
|
||||||
|
<p class="empty-subtitle text-muted">
|
||||||
|
No applications are running on servers in this subnet.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Update the usage progress bar section -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-label">Usage</div>
|
||||||
|
<div class="progress mb-2">
|
||||||
|
<div class="progress-bar" style="width: {{ (used_ips / total_ips * 100) if total_ips > 0 else 0 }}%"
|
||||||
|
role="progressbar"></div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span>{{ used_ips }} used</span>
|
||||||
|
<span>{{ total_ips }} total</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Delete Confirmation Modal -->
|
<!-- Delete Confirmation Modal -->
|
||||||
|
@ -185,4 +248,65 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.glass-card {
|
||||||
|
background-color: rgba(255, 255, 255, 0.03);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme="light"] .glass-card {
|
||||||
|
background-color: rgba(255, 255, 255, 0.7);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-card {
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme="light"] .app-card {
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-card-header {
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme="light"] .app-card-header {
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-card-title h4 {
|
||||||
|
margin: 0 0 0.25rem 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-card-body {
|
||||||
|
padding: 1rem;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -15,10 +15,13 @@
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
<!-- Custom CSS -->
|
<!-- Custom CSS -->
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/tabler.min.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/tabler.min.css') }}"
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='libs/tabler-icons/tabler-icons.min.css') }}">
|
onerror="this.onerror=null;this.href='https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/css/tabler.min.css';">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='libs/tabler-icons/tabler-icons.min.css') }}"
|
||||||
|
onerror="this.onerror=null;this.href='https://cdn.jsdelivr.net/npm/@tabler/icons@latest/iconfont/tabler-icons.min.css';">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/markdown.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/markdown.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/theme.css') }}">
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/favicon.png') }}">
|
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/favicon.png') }}">
|
||||||
{% block styles %}{% endblock %}
|
{% block styles %}{% endblock %}
|
||||||
|
@ -267,7 +270,8 @@
|
||||||
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">Logout</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">Logout</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item ms-2">
|
<div class="navbar-nav flex-row order-md-last">
|
||||||
|
<div class="nav-item me-2">
|
||||||
<button id="theme-toggle" class="btn btn-icon" aria-label="Toggle theme">
|
<button id="theme-toggle" class="btn btn-icon" aria-label="Toggle theme">
|
||||||
<span class="ti ti-moon dark-icon d-none"></span>
|
<span class="ti ti-moon dark-icon d-none"></span>
|
||||||
<span class="ti ti-sun light-icon"></span>
|
<span class="ti ti-sun light-icon"></span>
|
||||||
|
@ -275,6 +279,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Flash Messages -->
|
<!-- Flash Messages -->
|
||||||
|
@ -529,6 +534,73 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// Add transition class to main content
|
||||||
|
const mainContent = document.querySelector('.page-body');
|
||||||
|
if (mainContent) {
|
||||||
|
mainContent.classList.add('page-transition');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theme toggle enhancement
|
||||||
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.addEventListener('click', function () {
|
||||||
|
const currentTheme = document.documentElement.getAttribute('data-bs-theme');
|
||||||
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||||
|
|
||||||
|
document.documentElement.setAttribute('data-bs-theme', newTheme);
|
||||||
|
localStorage.setItem('theme', newTheme);
|
||||||
|
|
||||||
|
// Update toggle icon
|
||||||
|
const darkIcon = document.querySelector('.dark-icon');
|
||||||
|
const lightIcon = document.querySelector('.light-icon');
|
||||||
|
|
||||||
|
if (darkIcon && lightIcon) {
|
||||||
|
if (newTheme === 'dark') {
|
||||||
|
darkIcon.classList.remove('d-none');
|
||||||
|
lightIcon.classList.add('d-none');
|
||||||
|
} else {
|
||||||
|
darkIcon.classList.add('d-none');
|
||||||
|
lightIcon.classList.remove('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show theme change notification
|
||||||
|
showNotification('success', `${newTheme.charAt(0).toUpperCase() + newTheme.slice(1)} mode activated`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notification function
|
||||||
|
function showNotification(type, message) {
|
||||||
|
const notificationArea = document.getElementById('notification-area');
|
||||||
|
if (!notificationArea) return;
|
||||||
|
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `alert alert-${type} alert-dismissible fade show`;
|
||||||
|
notification.style.animation = 'fadeIn 0.3s ease-out';
|
||||||
|
notification.innerHTML = `
|
||||||
|
${message}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
notificationArea.appendChild(notification);
|
||||||
|
|
||||||
|
// Auto dismiss after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.animation = 'fadeOut 0.3s ease-in';
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.remove();
|
||||||
|
}, 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
from { opacity: 1; }
|
||||||
|
to { opacity: 0; }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
BIN
app/utils/__pycache__/app_utils.cpython-313.pyc
Normal file
BIN
app/utils/__pycache__/app_utils.cpython-313.pyc
Normal file
Binary file not shown.
129
app/utils/app_utils.py
Normal file
129
app/utils/app_utils.py
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
from app.core.models import App, Port, Server
|
||||||
|
from app.core.extensions import db
|
||||||
|
from flask import flash
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def validate_app_data(name, server_id, existing_app_id=None):
|
||||||
|
"""
|
||||||
|
Validate application data
|
||||||
|
Returns tuple (valid, error_message)
|
||||||
|
"""
|
||||||
|
if not name or not server_id:
|
||||||
|
return False, "Please fill in all required fields"
|
||||||
|
|
||||||
|
# Check if app exists with same name on same server
|
||||||
|
query = App.query.filter(App.name == name, App.server_id == server_id)
|
||||||
|
|
||||||
|
# If editing, exclude the current app
|
||||||
|
if existing_app_id:
|
||||||
|
query = query.filter(App.id != existing_app_id)
|
||||||
|
|
||||||
|
if query.first():
|
||||||
|
return False, f"Application '{name}' already exists on this server"
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
|
||||||
|
def validate_port_data(port_number, protocol, description=None):
|
||||||
|
"""
|
||||||
|
Validate port data
|
||||||
|
Returns tuple (valid, clean_port_number, error_message)
|
||||||
|
"""
|
||||||
|
# Clean and validate port number
|
||||||
|
try:
|
||||||
|
port = int(port_number)
|
||||||
|
if port < 1 or port > 65535:
|
||||||
|
return False, None, "Port must be between 1 and 65535"
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return False, None, "Invalid port number"
|
||||||
|
|
||||||
|
# Validate protocol
|
||||||
|
valid_protocols = ["TCP", "UDP", "SCTP", "OTHER"]
|
||||||
|
if protocol not in valid_protocols:
|
||||||
|
return False, None, f"Protocol must be one of: {', '.join(valid_protocols)}"
|
||||||
|
|
||||||
|
return True, port, None
|
||||||
|
|
||||||
|
|
||||||
|
def process_app_ports(app_id, port_data):
|
||||||
|
"""
|
||||||
|
Process port data for an application
|
||||||
|
port_data should be a list of tuples (port_number, protocol, description)
|
||||||
|
Returns (success, error_message)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
for port_number, protocol, description in port_data:
|
||||||
|
valid, clean_port, error = validate_port_data(
|
||||||
|
port_number, protocol, description
|
||||||
|
)
|
||||||
|
if not valid:
|
||||||
|
continue # Skip invalid ports
|
||||||
|
|
||||||
|
# Check if port already exists
|
||||||
|
existing_port = Port.query.filter_by(
|
||||||
|
app_id=app_id, port_number=clean_port
|
||||||
|
).first()
|
||||||
|
if existing_port:
|
||||||
|
# Update existing port
|
||||||
|
existing_port.protocol = protocol
|
||||||
|
existing_port.description = description
|
||||||
|
else:
|
||||||
|
# Create new port
|
||||||
|
new_port = Port(
|
||||||
|
app_id=app_id,
|
||||||
|
port_number=clean_port,
|
||||||
|
protocol=protocol,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
db.session.add(new_port)
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def save_app(name, server_id, documentation, port_data=None, existing_app_id=None):
|
||||||
|
"""
|
||||||
|
Save or update an application and its ports
|
||||||
|
Returns tuple (success, app_object_or_none, error_message)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Validate input data
|
||||||
|
valid, error = validate_app_data(name, server_id, existing_app_id)
|
||||||
|
if not valid:
|
||||||
|
return False, None, error
|
||||||
|
|
||||||
|
# Create or update app
|
||||||
|
if existing_app_id:
|
||||||
|
# Update existing app
|
||||||
|
app = App.query.get(existing_app_id)
|
||||||
|
if not app:
|
||||||
|
return False, None, f"Application with ID {existing_app_id} not found"
|
||||||
|
|
||||||
|
app.name = name
|
||||||
|
app.server_id = server_id
|
||||||
|
app.documentation = documentation
|
||||||
|
else:
|
||||||
|
# Create new app
|
||||||
|
app = App(name=name, server_id=server_id, documentation=documentation)
|
||||||
|
db.session.add(app)
|
||||||
|
|
||||||
|
# Flush to get the app ID if new
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
# Process ports if provided
|
||||||
|
if port_data:
|
||||||
|
success, error = process_app_ports(app.id, port_data)
|
||||||
|
if not success:
|
||||||
|
db.session.rollback()
|
||||||
|
return False, None, f"Error processing ports: {error}"
|
||||||
|
|
||||||
|
# Commit all changes
|
||||||
|
db.session.commit()
|
||||||
|
return True, app, None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return False, None, str(e)
|
16
config.py
16
config.py
|
@ -1,28 +1,36 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""Base config."""
|
"""Base config."""
|
||||||
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-key-placeholder')
|
|
||||||
|
SECRET_KEY = os.environ.get("SECRET_KEY", "dev-key-placeholder")
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
WTF_CSRF_ENABLED = True
|
WTF_CSRF_ENABLED = True
|
||||||
SESSION_COOKIE_SECURE = False # Set to True in production with HTTPS
|
SESSION_COOKIE_SECURE = False # Set to True in production with HTTPS
|
||||||
|
|
||||||
|
|
||||||
class DevelopmentConfig(Config):
|
class DevelopmentConfig(Config):
|
||||||
"""Development config."""
|
"""Development config."""
|
||||||
|
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', 'sqlite:///app.db')
|
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "sqlite:///app.db")
|
||||||
SQLALCHEMY_ECHO = True
|
SQLALCHEMY_ECHO = True
|
||||||
|
|
||||||
|
|
||||||
class ProductionConfig(Config):
|
class ProductionConfig(Config):
|
||||||
"""Production config."""
|
"""Production config."""
|
||||||
|
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
TESTING = False
|
TESTING = False
|
||||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', 'sqlite:///app.db')
|
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "sqlite:///app.db")
|
||||||
SESSION_COOKIE_SECURE = True
|
SESSION_COOKIE_SECURE = True
|
||||||
REMEMBER_COOKIE_SECURE = True
|
REMEMBER_COOKIE_SECURE = True
|
||||||
|
|
||||||
|
|
||||||
class TestingConfig(Config):
|
class TestingConfig(Config):
|
||||||
"""Testing config."""
|
"""Testing config."""
|
||||||
|
|
||||||
TESTING = True
|
TESTING = True
|
||||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
|
SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
|
||||||
WTF_CSRF_ENABLED = False
|
WTF_CSRF_ENABLED = False
|
Binary file not shown.
BIN
instance/app.db
BIN
instance/app.db
Binary file not shown.
109
run.py
109
run.py
|
@ -15,29 +15,33 @@ import json
|
||||||
current_dir = os.path.abspath(os.path.dirname(__file__))
|
current_dir = os.path.abspath(os.path.dirname(__file__))
|
||||||
sys.path.insert(0, current_dir)
|
sys.path.insert(0, current_dir)
|
||||||
|
|
||||||
|
|
||||||
def create_basic_app():
|
def create_basic_app():
|
||||||
"""Create a Flask app without database dependencies"""
|
"""Create a Flask app without database dependencies"""
|
||||||
app = Flask(__name__,
|
app = Flask(
|
||||||
template_folder=os.path.join(current_dir, 'app', 'templates'),
|
__name__,
|
||||||
static_folder=os.path.join(current_dir, 'app', 'static'))
|
template_folder=os.path.join(current_dir, "app", "templates"),
|
||||||
|
static_folder=os.path.join(current_dir, "app", "static"),
|
||||||
|
)
|
||||||
|
|
||||||
# Basic configuration
|
# Basic configuration
|
||||||
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-key-placeholder')
|
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "dev-key-placeholder")
|
||||||
app.config['DEBUG'] = True
|
app.config["DEBUG"] = True
|
||||||
|
|
||||||
# Register basic routes
|
# Register basic routes
|
||||||
register_routes(app)
|
register_routes(app)
|
||||||
|
|
||||||
# Add a fallback index route if no routes match
|
# Add a fallback index route if no routes match
|
||||||
@app.route('/')
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
return "Your Network Management Flask Application is running! Navigate to /dashboard to see content."
|
return "Your Network Management Flask Application is running! Navigate to /dashboard to see content."
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
def register_routes(app):
|
def register_routes(app):
|
||||||
"""Register blueprints without database dependencies"""
|
"""Register blueprints without database dependencies"""
|
||||||
routes_dir = os.path.join(current_dir, 'app', 'routes')
|
routes_dir = os.path.join(current_dir, "app", "routes")
|
||||||
|
|
||||||
# Check if routes directory exists
|
# Check if routes directory exists
|
||||||
if not os.path.isdir(routes_dir):
|
if not os.path.isdir(routes_dir):
|
||||||
|
@ -47,6 +51,7 @@ def register_routes(app):
|
||||||
# Try to register API blueprint which is simplest
|
# Try to register API blueprint which is simplest
|
||||||
try:
|
try:
|
||||||
from app.routes.api import bp as api_bp
|
from app.routes.api import bp as api_bp
|
||||||
|
|
||||||
app.register_blueprint(api_bp)
|
app.register_blueprint(api_bp)
|
||||||
print("Registered API blueprint")
|
print("Registered API blueprint")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -55,6 +60,7 @@ def register_routes(app):
|
||||||
# Try to register other blueprints with basic error handling
|
# Try to register other blueprints with basic error handling
|
||||||
try:
|
try:
|
||||||
from app.routes.dashboard import bp as dashboard_bp
|
from app.routes.dashboard import bp as dashboard_bp
|
||||||
|
|
||||||
app.register_blueprint(dashboard_bp)
|
app.register_blueprint(dashboard_bp)
|
||||||
print("Registered dashboard blueprint")
|
print("Registered dashboard blueprint")
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
|
@ -62,45 +68,47 @@ def register_routes(app):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from app.routes.ipam import bp as ipam_bp
|
from app.routes.ipam import bp as ipam_bp
|
||||||
|
|
||||||
app.register_blueprint(ipam_bp)
|
app.register_blueprint(ipam_bp)
|
||||||
print("Registered IPAM blueprint")
|
print("Registered IPAM blueprint")
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
print(f"Could not import IPAM blueprint: {e}")
|
print(f"Could not import IPAM blueprint: {e}")
|
||||||
|
|
||||||
|
|
||||||
# Create a development application instance
|
# Create a development application instance
|
||||||
print("Starting Flask app with SQLite database...")
|
print("Starting Flask app with SQLite database...")
|
||||||
app = create_app('development')
|
app = create_app("development")
|
||||||
|
|
||||||
|
|
||||||
@app.shell_context_processor
|
@app.shell_context_processor
|
||||||
def make_shell_context():
|
def make_shell_context():
|
||||||
return {
|
return {
|
||||||
'db': db,
|
"db": db,
|
||||||
'User': User,
|
"User": User,
|
||||||
'Server': Server,
|
"Server": Server,
|
||||||
'Subnet': Subnet,
|
"Subnet": Subnet,
|
||||||
'App': App,
|
"App": App,
|
||||||
'Port': Port
|
"Port": Port,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
"""Initialize database tables"""
|
"""Initialize database tables"""
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
|
||||||
|
|
||||||
def create_admin_user():
|
def create_admin_user():
|
||||||
"""Create an admin user if no users exist"""
|
"""Create an admin user if no users exist"""
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
if User.query.count() == 0:
|
if User.query.count() == 0:
|
||||||
admin = User(
|
admin = User(username="admin", email="admin@example.com", is_admin=True)
|
||||||
username='admin',
|
admin.set_password("admin")
|
||||||
email='admin@example.com',
|
|
||||||
is_admin=True
|
|
||||||
)
|
|
||||||
admin.set_password('admin')
|
|
||||||
db.session.add(admin)
|
db.session.add(admin)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
print("Created admin user: admin@example.com (password: admin)")
|
print("Created admin user: admin@example.com (password: admin)")
|
||||||
|
|
||||||
|
|
||||||
# Update seed_data to use consistent structures
|
# Update seed_data to use consistent structures
|
||||||
def seed_data():
|
def seed_data():
|
||||||
"""Add some sample data to the database"""
|
"""Add some sample data to the database"""
|
||||||
|
@ -108,42 +116,75 @@ def seed_data():
|
||||||
# Only seed if the database is empty
|
# Only seed if the database is empty
|
||||||
if Subnet.query.count() == 0:
|
if Subnet.query.count() == 0:
|
||||||
# Create sample subnets
|
# Create sample subnets
|
||||||
subnet1 = Subnet(cidr='192.168.1.0/24', location='Office', active_hosts=json.dumps([]))
|
subnet1 = Subnet(
|
||||||
subnet2 = Subnet(cidr='10.0.0.0/24', location='Datacenter', active_hosts=json.dumps([]))
|
cidr="192.168.1.0/24", location="Office", active_hosts=json.dumps([])
|
||||||
|
)
|
||||||
|
subnet2 = Subnet(
|
||||||
|
cidr="10.0.0.0/24", location="Datacenter", 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(hostname='web-server', ip_address='192.168.1.10', subnet=subnet1)
|
server1 = Server(
|
||||||
server2 = Server(hostname='db-server', ip_address='192.168.1.11', subnet=subnet1)
|
hostname="web-server", ip_address="192.168.1.10", subnet=subnet1
|
||||||
server3 = Server(hostname='app-server', ip_address='10.0.0.5', subnet=subnet2)
|
)
|
||||||
|
server2 = Server(
|
||||||
|
hostname="db-server", ip_address="192.168.1.11", subnet=subnet1
|
||||||
|
)
|
||||||
|
server3 = Server(
|
||||||
|
hostname="app-server", ip_address="10.0.0.5", subnet=subnet2
|
||||||
|
)
|
||||||
|
|
||||||
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(name='Website', server=server1, documentation='# Company Website\nRunning on Nginx/PHP')
|
app1 = App(
|
||||||
app2 = App(name='PostgreSQL', server=server2, documentation='# Database Server\nPostgreSQL 15')
|
name="Website",
|
||||||
app3 = App(name='API Service', server=server3, documentation='# REST API\nNode.js service')
|
server=server1,
|
||||||
|
documentation="# Company Website\nRunning on Nginx/PHP",
|
||||||
|
)
|
||||||
|
app2 = App(
|
||||||
|
name="PostgreSQL",
|
||||||
|
server=server2,
|
||||||
|
documentation="# Database Server\nPostgreSQL 15",
|
||||||
|
)
|
||||||
|
app3 = App(
|
||||||
|
name="API Service",
|
||||||
|
server=server3,
|
||||||
|
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')
|
port1 = Port(app=app1, port_number=80, protocol="TCP", description="HTTP")
|
||||||
port2 = Port(app=app1, port_number=443, protocol='TCP', description='HTTPS')
|
port2 = Port(app=app1, port_number=443, protocol="TCP", description="HTTPS")
|
||||||
port3 = Port(app=app2, port_number=5432, protocol='TCP', description='PostgreSQL')
|
port3 = Port(
|
||||||
port4 = Port(app=app3, port_number=3000, protocol='TCP', description='Node.js API')
|
app=app2, port_number=5432, protocol="TCP", description="PostgreSQL"
|
||||||
|
)
|
||||||
|
port4 = Port(
|
||||||
|
app=app3, port_number=3000, protocol="TCP", description="Node.js API"
|
||||||
|
)
|
||||||
|
|
||||||
db.session.add_all([port1, port2, port3, port4])
|
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")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Create the app first
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
# Initialize database if needed
|
# Initialize database if needed
|
||||||
if not os.path.exists('app.db') and 'sqlite' in app.config['SQLALCHEMY_DATABASE_URI']:
|
if (
|
||||||
|
not os.path.exists("app.db")
|
||||||
|
and "sqlite" in app.config["SQLALCHEMY_DATABASE_URI"]
|
||||||
|
):
|
||||||
print("Database not found, initializing...")
|
print("Database not found, initializing...")
|
||||||
try:
|
try:
|
||||||
init_db()
|
init_db()
|
||||||
|
|
BIN
scripts/__pycache__/check_routes.cpython-313-pytest-8.3.5.pyc
Normal file
BIN
scripts/__pycache__/check_routes.cpython-313-pytest-8.3.5.pyc
Normal file
Binary file not shown.
|
@ -9,18 +9,19 @@ import re
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
# Add the parent directory to sys.path
|
# Add the parent directory to sys.path
|
||||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
|
||||||
def find_all_routes():
|
def find_all_routes():
|
||||||
"""Find all route definitions in Python files"""
|
"""Find all route definitions in Python files"""
|
||||||
routes = []
|
routes = []
|
||||||
route_pattern = re.compile(r'@\w+\.route\([\'"]([^\'"]+)[\'"]')
|
route_pattern = re.compile(r'@\w+\.route\([\'"]([^\'"]+)[\'"]')
|
||||||
|
|
||||||
for root, _, files in os.walk('app'):
|
for root, _, files in os.walk("app"):
|
||||||
for file in files:
|
for file in files:
|
||||||
if file.endswith('.py'):
|
if file.endswith(".py"):
|
||||||
file_path = os.path.join(root, file)
|
file_path = os.path.join(root, file)
|
||||||
with open(file_path, 'r') as f:
|
with open(file_path, "r") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
matches = route_pattern.findall(content)
|
matches = route_pattern.findall(content)
|
||||||
for match in matches:
|
for match in matches:
|
||||||
|
@ -28,27 +29,46 @@ def find_all_routes():
|
||||||
|
|
||||||
return routes
|
return routes
|
||||||
|
|
||||||
|
|
||||||
def find_template_references():
|
def find_template_references():
|
||||||
"""Find all url_for calls in template files"""
|
"""Find all url_for calls in template files"""
|
||||||
references = []
|
references = []
|
||||||
url_for_pattern = re.compile(r'url_for\([\'"]([^\'"]+)[\'"]')
|
url_for_pattern = re.compile(r'url_for\([\'"]([^\'"]+)[\'"]')
|
||||||
|
|
||||||
for root, _, files in os.walk('app/templates'):
|
# Add patterns for direct links and HTMX references
|
||||||
|
direct_href_pattern = re.compile(r'href=[\'"]([^\'"]+)[\'"]')
|
||||||
|
htmx_pattern = re.compile(r'hx-(get|post|put|delete)=[\'"]([^\'"]+)[\'"]')
|
||||||
|
|
||||||
|
# Template files scanning
|
||||||
|
for root, _, files in os.walk("app/templates"):
|
||||||
for file in files:
|
for file in files:
|
||||||
if file.endswith('.html'):
|
if file.endswith(".html"):
|
||||||
file_path = os.path.join(root, file)
|
file_path = os.path.join(root, file)
|
||||||
with open(file_path, 'r') as f:
|
with open(file_path, "r") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
# Find url_for references
|
||||||
matches = url_for_pattern.findall(content)
|
matches = url_for_pattern.findall(content)
|
||||||
for match in matches:
|
for match in matches:
|
||||||
references.append(match)
|
references.append(match)
|
||||||
|
|
||||||
|
# Also check for direct route references in hrefs that aren't url_for
|
||||||
|
href_matches = direct_href_pattern.findall(content)
|
||||||
|
for href in href_matches:
|
||||||
|
if href.startswith("/") and not href.startswith("//"):
|
||||||
|
references.append(href)
|
||||||
|
|
||||||
|
# Check HTMX references
|
||||||
|
htmx_matches = htmx_pattern.findall(content)
|
||||||
|
for _, url in htmx_matches:
|
||||||
|
if url.startswith("/") and not url.startswith("//"):
|
||||||
|
references.append(url)
|
||||||
|
|
||||||
# Also check Python files for url_for calls
|
# Also check Python files for url_for calls
|
||||||
for root, _, files in os.walk('app'):
|
for root, _, files in os.walk("app"):
|
||||||
for file in files:
|
for file in files:
|
||||||
if file.endswith('.py'):
|
if file.endswith(".py"):
|
||||||
file_path = os.path.join(root, file)
|
file_path = os.path.join(root, file)
|
||||||
with open(file_path, 'r') as f:
|
with open(file_path, "r") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
matches = url_for_pattern.findall(content)
|
matches = url_for_pattern.findall(content)
|
||||||
for match in matches:
|
for match in matches:
|
||||||
|
@ -56,6 +76,7 @@ def find_template_references():
|
||||||
|
|
||||||
return references
|
return references
|
||||||
|
|
||||||
|
|
||||||
def check_unused_routes():
|
def check_unused_routes():
|
||||||
"""Find routes that are not referenced by url_for"""
|
"""Find routes that are not referenced by url_for"""
|
||||||
from app import create_app
|
from app import create_app
|
||||||
|
@ -74,10 +95,12 @@ def check_unused_routes():
|
||||||
unused_endpoints = all_endpoints - all_references
|
unused_endpoints = all_endpoints - all_references
|
||||||
|
|
||||||
if unused_endpoints:
|
if unused_endpoints:
|
||||||
print("The following routes are defined but not referenced in templates or code:")
|
print(
|
||||||
|
"The following routes are defined but not referenced in templates or code:"
|
||||||
|
)
|
||||||
for endpoint in sorted(unused_endpoints):
|
for endpoint in sorted(unused_endpoints):
|
||||||
# Skip static routes, error handlers, etc.
|
# Skip static routes, error handlers, etc.
|
||||||
if endpoint.startswith('static') or endpoint == 'static':
|
if endpoint.startswith("static") or endpoint == "static":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print(f" - {endpoint}")
|
print(f" - {endpoint}")
|
||||||
|
@ -90,5 +113,6 @@ def check_unused_routes():
|
||||||
else:
|
else:
|
||||||
print("All routes are referenced in templates or code. Good job!")
|
print("All routes are referenced in templates or code. Good job!")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
check_unused_routes()
|
check_unused_routes()
|
|
@ -8,17 +8,18 @@ import os
|
||||||
import shutil
|
import shutil
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
|
|
||||||
def cleanup(directory, verbose=False):
|
def cleanup(directory, verbose=False):
|
||||||
"""Clean up cache files and database files"""
|
"""Clean up cache files and database files"""
|
||||||
cleaned_dirs = 0
|
cleaned_dirs = 0
|
||||||
cleaned_files = 0
|
cleaned_files = 0
|
||||||
|
|
||||||
# Files to clean
|
# Files to clean
|
||||||
file_patterns = ['.pyc', '.pyo', '.~', '.swp', '.swo']
|
file_patterns = [".pyc", ".pyo", ".~", ".swp", ".swo"]
|
||||||
db_patterns = ['.db', '.sqlite', '.sqlite3', '-journal']
|
db_patterns = [".db", ".sqlite", ".sqlite3", "-journal"]
|
||||||
|
|
||||||
# Directories to clean
|
# Directories to clean
|
||||||
dir_patterns = ['__pycache__', '.pytest_cache', '.coverage', 'htmlcov']
|
dir_patterns = ["__pycache__", ".pytest_cache", ".coverage", "htmlcov"]
|
||||||
|
|
||||||
# Clean main directory
|
# Clean main directory
|
||||||
for root, dirs, files in os.walk(directory):
|
for root, dirs, files in os.walk(directory):
|
||||||
|
@ -42,7 +43,7 @@ def cleanup(directory, verbose=False):
|
||||||
cleaned_files += 1
|
cleaned_files += 1
|
||||||
|
|
||||||
# Clean instance directory
|
# Clean instance directory
|
||||||
instance_dir = os.path.join(directory, 'instance')
|
instance_dir = os.path.join(directory, "instance")
|
||||||
if os.path.exists(instance_dir):
|
if os.path.exists(instance_dir):
|
||||||
for file in os.listdir(instance_dir):
|
for file in os.listdir(instance_dir):
|
||||||
if any(file.endswith(pattern) for pattern in db_patterns):
|
if any(file.endswith(pattern) for pattern in db_patterns):
|
||||||
|
@ -52,12 +53,24 @@ def cleanup(directory, verbose=False):
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
cleaned_files += 1
|
cleaned_files += 1
|
||||||
|
|
||||||
print(f"Cleanup completed! Removed {cleaned_dirs} directories and {cleaned_files} files.")
|
print(
|
||||||
|
f"Cleanup completed! Removed {cleaned_dirs} directories and {cleaned_files} files."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="Clean up Flask application cache and database files")
|
parser = argparse.ArgumentParser(
|
||||||
parser.add_argument("-v", "--verbose", action="store_true", help="Show detailed output")
|
description="Clean up Flask application cache and database files"
|
||||||
parser.add_argument("-d", "--directory", default=".", help="Directory to clean (default: current directory)")
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-v", "--verbose", action="store_true", help="Show detailed output"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-d",
|
||||||
|
"--directory",
|
||||||
|
default=".",
|
||||||
|
help="Directory to clean (default: current directory)",
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
cleanup(args.directory, args.verbose)
|
cleanup(args.directory, args.verbose)
|
|
@ -10,12 +10,13 @@ from flask import Flask
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
# Add the parent directory to sys.path
|
# Add the parent directory to sys.path
|
||||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
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.auth import User
|
from app.core.auth import User
|
||||||
|
|
||||||
|
|
||||||
def create_admin_user(email=None, password=None):
|
def create_admin_user(email=None, password=None):
|
||||||
"""Create an admin user in the database"""
|
"""Create an admin user in the database"""
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
@ -25,7 +26,7 @@ def create_admin_user(email=None, password=None):
|
||||||
if User.query.count() > 0:
|
if User.query.count() > 0:
|
||||||
print("Users already exist in the database.")
|
print("Users already exist in the database.")
|
||||||
choice = input("Do you want to create another admin user? (y/n): ")
|
choice = input("Do you want to create another admin user? (y/n): ")
|
||||||
if choice.lower() != 'y':
|
if choice.lower() != "y":
|
||||||
print("Operation cancelled.")
|
print("Operation cancelled.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -57,6 +58,7 @@ def create_admin_user(email=None, password=None):
|
||||||
|
|
||||||
print(f"Admin user created successfully: {email}")
|
print(f"Admin user created successfully: {email}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
|
|
BIN
tests/__pycache__/conftest.cpython-313-pytest-8.3.5.pyc
Normal file
BIN
tests/__pycache__/conftest.cpython-313-pytest-8.3.5.pyc
Normal file
Binary file not shown.
6
wsgi.py
6
wsgi.py
|
@ -5,9 +5,9 @@ from app import create_app
|
||||||
# os.environ['DATABASE_URL'] = 'your_production_database_url'
|
# os.environ['DATABASE_URL'] = 'your_production_database_url'
|
||||||
|
|
||||||
# Create a production application
|
# Create a production application
|
||||||
app = create_app('production')
|
app = create_app("production")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
# This is only used for development
|
# This is only used for development
|
||||||
# In production, a WSGI server would import this file
|
# In production, a WSGI server would import this file
|
||||||
app.run(host='0.0.0.0', port=5000)
|
app.run(host="0.0.0.0", port=5000)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue