diff --git a/app/routes/api.py b/app/routes/api.py index 36c40d2..0dc4b2f 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -19,6 +19,7 @@ import markdown from datetime import datetime from flask import flash from app.utils.app_utils import is_port_in_use, validate_port_data +from difflib import SequenceMatcher bp = Blueprint("api", __name__, url_prefix="/api") csrf = CSRFProtect() @@ -424,7 +425,7 @@ def get_free_port(server_id): @bp.route("/validate/app-name", methods=["GET"]) @login_required def validate_app_name(): - """API endpoint to validate application name in real-time""" + """API endpoint to validate application name in real-time with fuzzy matching""" name = request.args.get("name", "").strip() server_id = request.args.get("server_id") app_id = request.args.get("app_id") @@ -448,18 +449,41 @@ def validate_app_name(): "edit_url": url_for('dashboard.app_edit', app_id=existing_app.id) }) - # Check for similar names - similar_apps = App.query.filter( - App.name.ilike(f"%{name}%"), - App.server_id == server_id - ).limit(3).all() + # Get all apps on this server for similarity check + server_apps = App.query.filter(App.server_id == server_id).all() - if similar_apps and (not app_id or not any(str(app.id) == app_id for app in similar_apps)): - similar_names = [{"name": app.name, "id": app.id} for app in similar_apps] + # Filter out the current app if we're editing + if app_id: + server_apps = [app for app in server_apps if str(app.id) != app_id] + + # Find similar apps using fuzzy matching + similar_apps = [] + + for app in server_apps: + # Calculate similarity ratio using SequenceMatcher + similarity = SequenceMatcher(None, name.lower(), app.name.lower()).ratio() + + # Consider similar if: + # 1. One is a substring of the other OR + # 2. They have high similarity ratio (over 0.7) + is_substring = name.lower() in app.name.lower() or app.name.lower() in name.lower() + is_similar = similarity > 0.7 + + if (is_substring or is_similar) and len(name) >= 3: + similar_apps.append({ + "name": app.name, + "id": app.id, + "similarity": similarity + }) + + # Sort by similarity (highest first) and limit to top 3 + similar_apps = sorted(similar_apps, key=lambda x: x["similarity"], reverse=True)[:3] + + if similar_apps: return jsonify({ "valid": True, "warning": "Similar application names found", - "similar_apps": similar_names + "similar_apps": similar_apps }) return jsonify({"valid": True}) diff --git a/app/routes/dashboard.py b/app/routes/dashboard.py index 0318fcb..26351d7 100644 --- a/app/routes/dashboard.py +++ b/app/routes/dashboard.py @@ -234,6 +234,27 @@ def app_new(server_id=None): documentation = request.form.get("documentation", "") url = request.form.get("url", "") + # Validate application name + if not name: + flash("Application name is required", "danger") + return render_template( + "dashboard/app_form.html", + edit_mode=False, + servers=servers, + selected_server_id=server_id + ) + + # Check for duplicate application names on the same server + existing_app = App.query.filter_by(name=name, server_id=server_id).first() + if existing_app: + flash(f"An application with the name '{name}' already exists on this server", "danger") + return render_template( + "dashboard/app_form.html", + edit_mode=False, + servers=servers, + selected_server_id=server_id + ) + # Process port data from form port_data = [] port_numbers = request.form.getlist("port_numbers[]") @@ -245,36 +266,75 @@ def app_new(server_id=None): protocol = protocols[i] if i < len(protocols) else "TCP" description = descriptions[i] if i < len(descriptions) else "" port_data.append((port_numbers[i], protocol, description)) + + # Check for port conflicts proactively + conflicts = [] + seen_ports = set() # To track ports already seen in this submission + + for i, (port_number, protocol, _) in enumerate(port_data): + try: + clean_port = int(port_number) + # Check if this port has already been seen in this submission + port_key = f"{clean_port}/{protocol}" + if port_key in seen_ports: + conflicts.append((clean_port, protocol, "Duplicate port in submission")) + continue + seen_ports.add(port_key) - # Server-side validation - from app.utils.app_utils import save_app + # Check if the port is in use by another application + in_use, conflicting_app_name = is_port_in_use( + clean_port, protocol, server_id + ) + + if in_use: + conflicts.append((clean_port, protocol, f"Port {clean_port}/{protocol} is already in use by application '{conflicting_app_name}'")) + except (ValueError, TypeError): + continue - success, app, error = save_app(name, server_id, documentation, port_data, url=url) + if conflicts: + for conflict in conflicts: + flash(f"Conflict: {conflict[0]}/{conflict[1]} - {conflict[2]}", "danger") + return render_template( + "dashboard/app_form.html", + edit_mode=False, + servers=servers, + selected_server_id=server_id + ) - if success: - flash(f"Application '{name}' created successfully", "success") - return redirect(url_for("dashboard.app_view", app_id=app.id)) - else: - flash(error, "danger") + # Create the application + try: + new_app = App( + name=name, + server_id=server_id, + documentation=documentation, + url=url + ) + db.session.add(new_app) + db.session.flush() # Get the app ID without committing - # Check if it's a port conflict and provide direct link - if "already in use by application" in error: - try: - app_name = error.split("'")[1] # Extract app name from error - conflict_app = App.query.filter_by(name=app_name, server_id=server_id).first() - if conflict_app: - edit_url = url_for('dashboard.app_edit', app_id=conflict_app.id) - flash(f'Edit conflicting application', "info") - except: - pass + # Add ports + for port_number, protocol, description in port_data: + new_port = Port( + app_id=new_app.id, + port_number=int(port_number), + protocol=protocol, + description=description + ) + db.session.add(new_port) + + db.session.commit() + flash(f"Application '{name}' created successfully", "success") + return redirect(url_for("dashboard.app_view", app_id=new_app.id)) + except Exception as e: + db.session.rollback() + flash(f"Error creating application: {str(e)}", "danger") # GET request or validation failed - render the form return render_template( "dashboard/app_form.html", - title="New Application", edit_mode=False, servers=servers, - selected_server_id=server_id, + selected_server_id=server_id ) @@ -308,6 +368,32 @@ def app_edit(app_id): documentation = request.form.get("documentation", "") url = request.form.get("url", "") # Get the URL + # Validate application name + if not name: + flash("Application name is required", "danger") + return render_template( + "dashboard/app_form.html", + edit_mode=True, + app=app, + servers=servers + ) + + # Check for duplicate application names on the same server (excluding this app) + existing_app = App.query.filter( + App.name == name, + App.server_id == server_id, + App.id != app_id + ).first() + + if existing_app: + flash(f"An application with the name '{name}' already exists on this server", "danger") + return render_template( + "dashboard/app_form.html", + edit_mode=True, + app=app, + servers=servers + ) + # Process port data from form port_data = [] port_numbers = request.form.getlist("port_numbers[]") @@ -347,7 +433,12 @@ def app_edit(app_id): if conflicts: for conflict in conflicts: flash(f"Conflict: {conflict[0]}/{conflict[1]} - {conflict[2]}", "danger") - return render_template("dashboard/app_edit.html", app=app, servers=servers) + return render_template( + "dashboard/app_form.html", + edit_mode=True, + app=app, + servers=servers + ) # Update application details app.name = name @@ -372,7 +463,12 @@ def app_edit(app_id): flash("Application updated successfully", "success") return redirect(url_for("dashboard.app_view", app_id=app_id)) - return render_template("dashboard/app_edit.html", app=app, servers=servers) + return render_template( + "dashboard/app_form.html", + edit_mode=True, + app=app, + servers=servers + ) @bp.route("/app//delete", methods=["POST"]) diff --git a/app/templates/dashboard/app_form.html b/app/templates/dashboard/app_form.html index d769517..d2f53b5 100644 --- a/app/templates/dashboard/app_form.html +++ b/app/templates/dashboard/app_form.html @@ -6,25 +6,8 @@

- {{ title }} + {% if edit_mode %}Edit Application: {{ app.name }}{% else %}New Application{% endif %}

-
- {% if edit_mode %}Edit{% else %}Create{% endif %} application details and configure ports -
-
-
-
- - - Dashboard - - {% if edit_mode %} - - - View Application - - {% endif %} -
@@ -35,7 +18,7 @@ {% if messages %} {% for category, message in messages %} {% endfor %} @@ -43,77 +26,55 @@ {% endwith %}
+ action="{{ url_for('dashboard.app_edit', app_id=app.id) if edit_mode else url_for('dashboard.app_new', server_id=selected_server_id) }}"> - {% if app %} - - {% endif %} -
- - -
+ +
-
- -
- - - - -
- - If provided, the application name will be clickable and link to this URL. - + + + Optional URL for accessing the application
-
- - + {% for server in servers %} - {% endfor %}
+
+ + +
-
- -
- - -
-
- +
- + - + - {% if app and app.ports %} + {% if edit_mode and app.ports %} {% for port in app.ports %} {% endfor %} {% else %} - @@ -157,94 +118,26 @@
PortPort Number Protocol Description
+ value="{{ port.port_number }}">
- +
- Configure the network ports used by this application -
- -
- - - - Use Markdown syntax - to format your documentation. You can use GitHub-style alerts: - -
- > [!NOTE] This is a note - > [!TIP] This is a tip - > [!IMPORTANT] Important info - > [!WARNING] Warning message - > [!CAUTION] Critical caution +
+
-{% endblock %} - -{% block extra_js %} - - - {% endblock %} - -{% block scripts %} -{{ super() }} - -{% endblock %} \ No newline at end of file