This commit is contained in:
pika 2025-03-31 00:19:49 +02:00
parent d79359cd65
commit 30e9c9328e
18 changed files with 320 additions and 141 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,4 +1,4 @@
from flask import Blueprint, jsonify, request, abort, current_app, render_template from flask import Blueprint, jsonify, request, abort, current_app, render_template, redirect, url_for
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
@ -197,15 +197,6 @@ def status():
return jsonify({"status": "OK"}) 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():
data = request.json
md_content = data.get("markdown", "")
html = markdown.markdown(md_content)
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()
@ -296,15 +287,15 @@ def add_app_port(app_id):
valid, clean_port, error = validate_port_data(port_number, protocol, description) valid, clean_port, error = validate_port_data(port_number, protocol, description)
if not valid: if not valid:
return jsonify({"success": False, "error": error}), 400 flash(error, "danger")
return redirect(url_for("dashboard.app_view", app_id=app_id)) if not request.is_xhr else jsonify({"success": False, "error": error}), 400
# Check if port already exists # Check if port already exists
existing_port = Port.query.filter_by(app_id=app_id, port_number=clean_port).first() existing_port = Port.query.filter_by(app_id=app_id, port_number=clean_port).first()
if existing_port: if existing_port:
return jsonify({ error_msg = f"Port {clean_port} already exists for this application"
"success": False, flash(error_msg, "warning")
"error": f"Port {clean_port} already exists for this application" return redirect(url_for("dashboard.app_view", app_id=app_id)) if not request.is_xhr else jsonify({"success": False, "error": error_msg}), 400
}), 400
# Create new port # Create new port
new_port = Port( new_port = Port(
@ -316,10 +307,17 @@ def add_app_port(app_id):
db.session.add(new_port) db.session.add(new_port)
db.session.commit() db.session.commit()
flash(f"Port {clean_port}/{protocol} added successfully", "success") success_msg = f"Port {clean_port}/{protocol} added successfully"
flash(success_msg, "success")
# If it's a regular form submission (not AJAX), redirect
if not request.is_xhr and not request.is_json:
return redirect(url_for("dashboard.app_view", app_id=app_id))
# Otherwise return JSON for API/AJAX calls
return jsonify({ return jsonify({
"success": True, "success": True,
"message": f"Port {clean_port}/{protocol} added successfully", "message": success_msg,
"port": { "port": {
"id": new_port.id, "id": new_port.id,
"number": new_port.port_number, "number": new_port.port_number,
@ -330,7 +328,8 @@ def add_app_port(app_id):
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
return jsonify({"success": False, "error": str(e)}), 500 flash(f"Error: {str(e)}", "danger")
return redirect(url_for("dashboard.app_view", app_id=app_id)) if not request.is_xhr else jsonify({"success": False, "error": str(e)}), 500
@bp.route("/app/<int:app_id>/ports", methods=["GET"]) @bp.route("/app/<int:app_id>/ports", methods=["GET"])
@ -357,25 +356,26 @@ def get_app_ports(app_id):
return jsonify(result) return jsonify(result)
@bp.route("/port/<int:port_id>/delete", methods=["POST"]) @bp.route("/app/<int:app_id>/port/<int:port_id>/delete", methods=["POST"])
@login_required @login_required
def delete_port(port_id): def delete_app_port(app_id, port_id):
"""Delete a port""" """Delete a port from an application"""
# Add CSRF validation app = App.query.get_or_404(app_id)
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)
if port.app_id != app.id:
flash("Port does not belong to this application", "danger")
return redirect(url_for("dashboard.app_view", app_id=app_id))
try: 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"}) flash(f"Port {port.port_number}/{port.protocol} deleted successfully", "success")
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
return jsonify({"success": False, "error": str(e)}), 500 flash(f"Error deleting port: {str(e)}", "danger")
return redirect(url_for("dashboard.app_view", app_id=app_id))
@bp.route("/subnets/<int:subnet_id>/servers", methods=["GET"]) @bp.route("/subnets/<int:subnet_id>/servers", methods=["GET"])

Binary file not shown.

View file

@ -66,34 +66,6 @@
<small class="form-hint">Select the server where this application runs</small> <small class="form-hint">Select the server where this application runs</small>
</div> </div>
<div class="mb-3">
<label class="form-label">Documentation</label>
<ul class="nav nav-tabs mb-2" role="tablist">
<li class="nav-item" role="presentation">
<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>
<div class="hr-text">Port Configuration</div>
<div class="mb-3"> <div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-2"> <div class="d-flex justify-content-between align-items-center mb-2">
<label class="form-label mb-0">Application Ports</label> <label class="form-label mb-0">Application Ports</label>
@ -112,16 +84,16 @@
<table class="table table-vcenter card-table" id="ports-table"> <table class="table table-vcenter card-table" id="ports-table">
<thead> <thead>
<tr> <tr>
<th style="width: 20%">Port Number</th> <th>Port</th>
<th style="width: 20%">Protocol</th> <th>Protocol</th>
<th style="width: 50%">Description</th> <th>Description</th>
<th style="width: 10%">Actions</th> <th width="40"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% if app and app.ports %} {% if app and app.ports %}
{% for port in app.ports %} {% for port in app.ports %}
<tr data-port-id="{{ port.id }}"> <tr>
<td> <td>
<input type="number" name="port_numbers[]" class="form-control" min="1" max="65535" <input type="number" name="port_numbers[]" class="form-control" min="1" max="65535"
value="{{ port.port_number }}" required> value="{{ port.port_number }}" required>
@ -146,13 +118,22 @@
</tr> </tr>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
<!-- New rows will be added here dynamically -->
</tbody> </tbody>
</table> </table>
</div> </div>
<small class="form-hint">Configure the network ports used by this application</small> <small class="form-hint">Configure the network ports used by this application</small>
</div> </div>
<div class="mb-3">
<label class="form-label">Documentation</label>
<textarea class="form-control" name="documentation" id="documentation" rows="10"
placeholder="Enter documentation in Markdown format">{{ app.documentation if app else '' }}</textarea>
<small class="form-hint">
Use <a href="https://www.markdownguide.org/basic-syntax/" target="_blank">Markdown syntax</a>
to format your documentation. The content will be rendered when viewing the application.
</small>
</div>
<div class="form-footer"> <div class="form-footer">
<button type="submit" class="btn btn-primary">Save Application</button> <button type="submit" class="btn btn-primary">Save Application</button>
{% if edit_mode %} {% if edit_mode %}
@ -175,11 +156,45 @@
const appId = null; const appId = null;
{% endif %} {% endif %}
// Setup markdown preview // Initialize everything once the DOM is loaded
setupMarkdownPreview(); document.addEventListener('DOMContentLoaded', function () {
// Connect port management buttons
const addPortBtn = document.getElementById('add-port-btn');
const randomPortBtn = document.getElementById('random-port-btn');
// Setup port management if (addPortBtn) {
setupPortHandlers(); addPortBtn.addEventListener('click', function () {
addPortRow();
});
}
if (randomPortBtn) {
randomPortBtn.addEventListener('click', function () {
generateRandomPort();
});
}
});
// Show notifications
function showNotification(message, type = 'info') {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
const container = document.querySelector('.card-body');
if (container) {
container.insertBefore(alertDiv, container.firstChild);
// Auto-dismiss after 5 seconds
setTimeout(() => {
alertDiv.classList.remove('show');
setTimeout(() => alertDiv.remove(), 150);
}, 5000);
}
}
</script> </script>
<script> <script>
@ -244,13 +259,5 @@
showNotification('Failed to generate random port', 'danger'); showNotification('Failed to generate random port', 'danger');
} }
} }
function setupPortHandlers() {
// Add port button
document.getElementById('add-port-btn')?.addEventListener('click', () => addPortRow());
// Random port button
document.getElementById('random-port-btn')?.addEventListener('click', generateRandomPort);
}
</script> </script>
{% endblock %} {% endblock %}

View file

@ -25,9 +25,9 @@
</div> </div>
</div> </div>
<div class="row mt-3"> <div class="row g-3">
<div class="col-md-4"> <div class="col-md-4">
<!-- Basic Information --> <!-- Basic Information card -->
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h3 class="card-title">Basic Information</h3> <h3 class="card-title">Basic Information</h3>
@ -35,44 +35,38 @@
<div class="card-body"> <div class="card-body">
<div class="mb-3"> <div class="mb-3">
<div class="form-label">Server</div> <div class="form-label">Server</div>
<div> <div><a href="{{ url_for('dashboard.server_view', server_id=app.server.id) }}">{{ app.server.name }}</a> ({{
<a href="{{ url_for('dashboard.server_view', server_id=server.id) }}"> app.server.ip_address }})</div>
{{ server.hostname }}
</a>
({{ server.ip_address }})
</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<div class="form-label">Created</div> <div class="form-label">Created</div>
<div>{{ app.created_at.strftime('%Y-%m-%d %H:%M') }}</div> <div>{{ app.created_at }}</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<div class="form-label">Last Updated</div> <div class="form-label">Last Updated</div>
<div>{{ app.updated_at.strftime('%Y-%m-%d %H:%M') }}</div> <div>{{ app.updated_at }}</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Ports --> <!-- Ports card -->
<div class="card mt-3"> <div class="card mt-3">
<div class="card-header"> <div class="card-header d-flex justify-content-between align-items-center">
<h3 class="card-title">Ports</h3> <h3 class="card-title">Ports</h3>
<div class="card-actions"> <button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addPortModal">
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addPortModal"> <span class="ti ti-plus"></span> Add Port
<span class="ti ti-plus me-1"></span> Add Port </button>
</button>
</div>
</div> </div>
<div class="card-body"> <div class="card-body p-0">
{% if app.ports %} {% if app.ports %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-vcenter"> <table class="table table-vcenter card-table">
<thead> <thead>
<tr> <tr>
<th>Port</th> <th>PORT</th>
<th>Protocol</th> <th>PROTOCOL</th>
<th>Description</th> <th>DESCRIPTION</th>
<th class="w-1"></th> <th width="40"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -80,12 +74,12 @@
<tr> <tr>
<td>{{ port.port_number }}</td> <td>{{ port.port_number }}</td>
<td>{{ port.protocol }}</td> <td>{{ port.protocol }}</td>
<td>{{ port.description or 'No description' }}</td> <td>{{ port.description }}</td>
<td> <td>
<a href="#" data-bs-toggle="modal" data-bs-target="#deletePortModal{{ port.id }}" <button type="button" class="btn btn-sm btn-ghost-danger"
class="btn btn-sm btn-ghost-danger"> onclick="confirmDeletePort({{ port.id }})">
<span class="ti ti-trash"></span> <span class="ti ti-trash"></span>
</a> </button>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -93,7 +87,7 @@
</table> </table>
</div> </div>
{% else %} {% else %}
<div class="empty"> <div class="empty p-4">
<div class="empty-icon"> <div class="empty-icon">
<span class="ti ti-plug"></span> <span class="ti ti-plug"></span>
</div> </div>
@ -113,14 +107,14 @@
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
<!-- Documentation section - ENHANCED --> <!-- Documentation section -->
<div class="card"> <div class="card h-100">
<div class="card-header"> <div class="card-header">
<h3 class="card-title">Documentation</h3> <h3 class="card-title">Documentation</h3>
</div> </div>
<div class="card-body markdown-content"> <div class="card-body markdown-content p-4">
{% if app.documentation %} {% if app.documentation %}
{{ app.documentation|markdown }} {{ app.documentation|markdown|safe }}
{% else %} {% else %}
<div class="empty"> <div class="empty">
<div class="empty-icon"> <div class="empty-icon">
@ -144,23 +138,28 @@
</div> </div>
<!-- Add Port Modal --> <!-- Add Port Modal -->
<div class="modal" id="addPortModal" tabindex="-1"> <div class="modal modal-blur fade" id="addPortModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<form method="POST" action="{{ url_for('api.add_app_port', app_id=app.id) }}"> <div class="modal-header">
<h5 class="modal-title">Add Port</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="addPortForm" action="{{ url_for('api.add_app_port', app_id=app.id) }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="modal-header">
<h5 class="modal-title">Add Port</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <div class="mb-3">
<label class="form-label required">Port Number</label> <label class="form-label required">Port Number</label>
<input type="number" class="form-control" name="port_number" required min="1" max="65535"> <div class="input-group">
<input type="number" class="form-control" name="port_number" min="1" max="65535" required>
<button class="btn btn-outline-secondary" type="button" id="randomPortBtn">
<span class="ti ti-dice"></span>
</button>
</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label required">Protocol</label> <label class="form-label">Protocol</label>
<select class="form-select" name="protocol" required> <select class="form-select" name="protocol">
<option value="TCP">TCP</option> <option value="TCP">TCP</option>
<option value="UDP">UDP</option> <option value="UDP">UDP</option>
<option value="SCTP">SCTP</option> <option value="SCTP">SCTP</option>
@ -169,18 +168,34 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Description</label> <label class="form-label">Description</label>
<input type="text" class="form-control" name="description" placeholder="What is this port used for?"> <input type="text" class="form-control" name="description" placeholder="Optional description">
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> <button type="button" class="btn btn-link link-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Add Port</button> <button type="submit" class="btn btn-primary ms-auto">Add Port</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
<!-- Confirmation Modal for Port Deletion -->
<div class="modal modal-blur fade" id="deletePortModal" tabindex="-1">
<div class="modal-dialog modal-sm modal-dialog-centered">
<div class="modal-content">
<div class="modal-body">
<div class="modal-title">Are you sure?</div>
<div>This will permanently delete this port.</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link link-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">Delete Port</button>
</div>
</div>
</div>
</div>
<!-- Delete App Modal --> <!-- Delete App Modal -->
<div class="modal modal-blur fade" id="deleteAppModal" tabindex="-1"> <div class="modal modal-blur fade" id="deleteAppModal" tabindex="-1">
<div class="modal-dialog modal-sm modal-dialog-centered"> <div class="modal-dialog modal-sm modal-dialog-centered">
@ -200,24 +215,181 @@
</div> </div>
</div> </div>
<!-- Delete Port Modals --> <!-- Add some additional CSS for better markdown rendering -->
{% for port in app.ports %} <style>
<div class="modal modal-blur fade" id="deletePortModal{{ port.id }}" tabindex="-1"> .markdown-content {
<div class="modal-dialog modal-sm modal-dialog-centered"> line-height: 1.6;
<div class="modal-content"> padding: 1.5rem !important;
<div class="modal-body"> }
<div class="modal-title">Are you sure?</div>
<div>This will delete port {{ port.port_number }}/{{ port.protocol }}.</div> .markdown-content h1,
</div> .markdown-content h2,
<div class="modal-footer"> .markdown-content h3,
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> .markdown-content h4,
<form method="POST" action="{{ url_for('api.delete_port', port_id=port.id) }}"> .markdown-content h5,
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> .markdown-content h6 {
<button type="submit" class="btn btn-danger">Delete Port</button> margin-top: 1.5rem;
</form> margin-bottom: 1rem;
</div> font-weight: 600;
</div> }
</div>
</div> .markdown-content h1 {
{% endfor %} font-size: 1.8rem;
padding-bottom: 0.3rem;
border-bottom: 1px solid var(--tblr-border-color);
}
.markdown-content h2 {
font-size: 1.5rem;
padding-bottom: 0.3rem;
border-bottom: 1px solid var(--tblr-border-color);
}
.markdown-content p {
margin-bottom: 1rem;
}
.markdown-content ul,
.markdown-content ol {
margin-bottom: 1rem;
padding-left: 2rem;
}
.markdown-content blockquote {
padding: 0.5rem 1rem;
margin-bottom: 1rem;
border-left: 0.25rem solid var(--tblr-border-color);
color: var(--tblr-secondary);
}
.markdown-content pre {
padding: 1rem;
margin-bottom: 1rem;
background-color: var(--tblr-bg-surface);
border-radius: 0.25rem;
overflow-x: auto;
}
.markdown-content code {
padding: 0.2rem 0.4rem;
border-radius: 0.25rem;
background-color: var(--tblr-bg-surface);
font-size: 0.875em;
}
.markdown-content pre code {
padding: 0;
background-color: transparent;
}
.markdown-content img {
max-width: 100%;
border-radius: 0.25rem;
margin: 1rem 0;
}
.markdown-content table {
width: 100%;
margin-bottom: 1rem;
border-collapse: collapse;
}
.markdown-content table th,
.markdown-content table td {
padding: 0.5rem;
border: 1px solid var(--tblr-border-color);
}
.markdown-content table th {
background-color: var(--tblr-bg-surface);
font-weight: 600;
}
/* GitHub-style alerts styling */
.markdown-content blockquote:has(p:first-child:contains("[!NOTE]")),
.markdown-content blockquote:has(p:first-child:contains("[!TIP]")),
.markdown-content blockquote:has(p:first-child:contains("[!IMPORTANT]")),
.markdown-content blockquote:has(p:first-child:contains("[!WARNING]")),
.markdown-content blockquote:has(p:first-child:contains("[!CAUTION]")) {
padding: 1rem;
margin: 1rem 0;
border-left-width: 0.25rem;
border-radius: 0.25rem;
}
.markdown-content blockquote:has(p:first-child:contains("[!NOTE]")) {
background-color: rgba(0, 120, 215, 0.1);
border-left-color: #0078d7;
}
.markdown-content blockquote:has(p:first-child:contains("[!TIP]")) {
background-color: rgba(46, 160, 67, 0.1);
border-left-color: #2ea043;
}
.markdown-content blockquote:has(p:first-child:contains("[!IMPORTANT]")) {
background-color: rgba(162, 80, 214, 0.1);
border-left-color: #a250d6;
}
.markdown-content blockquote:has(p:first-child:contains("[!WARNING]")) {
background-color: rgba(245, 159, 0, 0.1);
border-left-color: #f59f00;
}
.markdown-content blockquote:has(p:first-child:contains("[!CAUTION]")) {
background-color: rgba(215, 58, 73, 0.1);
border-left-color: #d73a49;
}
</style>
<script>
// Store app ID for JavaScript use
const appId = {{ app.id }};
let portToDelete = null;
// Function to handle port delete confirmation
function confirmDeletePort(portId) {
portToDelete = portId;
const modal = new bootstrap.Modal(document.getElementById('deletePortModal'));
modal.show();
}
// Set up event listeners when DOM is ready
document.addEventListener('DOMContentLoaded', function () {
// Handle random port button click
const randomPortBtn = document.getElementById('randomPortBtn');
if (randomPortBtn) {
randomPortBtn.addEventListener('click', function () {
fetch('/api/ports/random')
.then(response => response.json())
.then(data => {
document.querySelector('#addPortForm input[name="port_number"]').value = data.port;
});
});
}
// Handle port deletion confirmation
const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
if (confirmDeleteBtn) {
confirmDeleteBtn.addEventListener('click', function () {
if (portToDelete) {
// Create a form and submit it programmatically
const form = document.createElement('form');
form.method = 'POST';
form.action = `/api/app/${appId}/port/${portToDelete}/delete`;
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrf_token';
csrfInput.value = '{{ csrf_token() }}';
form.appendChild(csrfInput);
document.body.appendChild(form);
form.submit();
}
});
}
});
</script>
{% endblock %} {% endblock %}

Binary file not shown.

BIN
instance/app.db Normal file

Binary file not shown.