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 app.core.models import Subnet, Server, App, Port
from app.core.extensions import db
@ -197,15 +197,6 @@ 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():
data = request.json
md_content = data.get("markdown", "")
html = markdown.markdown(md_content)
return jsonify({"html": html})
@bp.route("/ports/suggest", methods=["GET"])
def suggest_ports():
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)
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
existing_port = Port.query.filter_by(app_id=app_id, port_number=clean_port).first()
if existing_port:
return jsonify({
"success": False,
"error": f"Port {clean_port} already exists for this application"
}), 400
error_msg = f"Port {clean_port} already exists for this application"
flash(error_msg, "warning")
return redirect(url_for("dashboard.app_view", app_id=app_id)) if not request.is_xhr else jsonify({"success": False, "error": error_msg}), 400
# Create new port
new_port = Port(
@ -316,10 +307,17 @@ def add_app_port(app_id):
db.session.add(new_port)
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({
"success": True,
"message": f"Port {clean_port}/{protocol} added successfully",
"message": success_msg,
"port": {
"id": new_port.id,
"number": new_port.port_number,
@ -330,7 +328,8 @@ def add_app_port(app_id):
except Exception as e:
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"])
@ -357,25 +356,26 @@ def get_app_ports(app_id):
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
def delete_port(port_id):
"""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
def delete_app_port(app_id, port_id):
"""Delete a port from an application"""
app = App.query.get_or_404(app_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:
db.session.delete(port)
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:
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"])

Binary file not shown.

View file

@ -66,34 +66,6 @@
<small class="form-hint">Select the server where this application runs</small>
</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="d-flex justify-content-between align-items-center mb-2">
<label class="form-label mb-0">Application Ports</label>
@ -112,16 +84,16 @@
<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>
<th>Port</th>
<th>Protocol</th>
<th>Description</th>
<th width="40"></th>
</tr>
</thead>
<tbody>
{% if app and app.ports %}
{% for port in app.ports %}
<tr data-port-id="{{ port.id }}">
<tr>
<td>
<input type="number" name="port_numbers[]" class="form-control" min="1" max="65535"
value="{{ port.port_number }}" required>
@ -146,13 +118,22 @@
</tr>
{% endfor %}
{% 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>
<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">
<button type="submit" class="btn btn-primary">Save Application</button>
{% if edit_mode %}
@ -175,11 +156,45 @@
const appId = null;
{% endif %}
// Setup markdown preview
setupMarkdownPreview();
// Initialize everything once the DOM is loaded
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
setupPortHandlers();
if (addPortBtn) {
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>
@ -244,13 +259,5 @@
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>
{% endblock %}

View file

@ -25,9 +25,9 @@
</div>
</div>
<div class="row mt-3">
<div class="row g-3">
<div class="col-md-4">
<!-- Basic Information -->
<!-- Basic Information card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Basic Information</h3>
@ -35,44 +35,38 @@
<div class="card-body">
<div class="mb-3">
<div class="form-label">Server</div>
<div>
<a href="{{ url_for('dashboard.server_view', server_id=server.id) }}">
{{ server.hostname }}
</a>
({{ server.ip_address }})
</div>
<div><a href="{{ url_for('dashboard.server_view', server_id=app.server.id) }}">{{ app.server.name }}</a> ({{
app.server.ip_address }})</div>
</div>
<div class="mb-3">
<div class="form-label">Created</div>
<div>{{ app.created_at.strftime('%Y-%m-%d %H:%M') }}</div>
<div>{{ app.created_at }}</div>
</div>
<div class="mb-3">
<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>
<!-- Ports -->
<!-- Ports card -->
<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>
<div class="card-actions">
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addPortModal">
<span class="ti ti-plus me-1"></span> Add Port
</button>
</div>
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addPortModal">
<span class="ti ti-plus"></span> Add Port
</button>
</div>
<div class="card-body">
<div class="card-body p-0">
{% if app.ports %}
<div class="table-responsive">
<table class="table table-vcenter">
<table class="table table-vcenter card-table">
<thead>
<tr>
<th>Port</th>
<th>Protocol</th>
<th>Description</th>
<th class="w-1"></th>
<th>PORT</th>
<th>PROTOCOL</th>
<th>DESCRIPTION</th>
<th width="40"></th>
</tr>
</thead>
<tbody>
@ -80,12 +74,12 @@
<tr>
<td>{{ port.port_number }}</td>
<td>{{ port.protocol }}</td>
<td>{{ port.description or 'No description' }}</td>
<td>{{ port.description }}</td>
<td>
<a href="#" data-bs-toggle="modal" data-bs-target="#deletePortModal{{ port.id }}"
class="btn btn-sm btn-ghost-danger">
<button type="button" class="btn btn-sm btn-ghost-danger"
onclick="confirmDeletePort({{ port.id }})">
<span class="ti ti-trash"></span>
</a>
</button>
</td>
</tr>
{% endfor %}
@ -93,7 +87,7 @@
</table>
</div>
{% else %}
<div class="empty">
<div class="empty p-4">
<div class="empty-icon">
<span class="ti ti-plug"></span>
</div>
@ -113,14 +107,14 @@
</div>
<div class="col-md-8">
<!-- Documentation section - ENHANCED -->
<div class="card">
<!-- Documentation section -->
<div class="card h-100">
<div class="card-header">
<h3 class="card-title">Documentation</h3>
</div>
<div class="card-body markdown-content">
<div class="card-body markdown-content p-4">
{% if app.documentation %}
{{ app.documentation|markdown }}
{{ app.documentation|markdown|safe }}
{% else %}
<div class="empty">
<div class="empty-icon">
@ -144,23 +138,28 @@
</div>
<!-- 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-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() }}">
<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="mb-3">
<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 class="mb-3">
<label class="form-label required">Protocol</label>
<select class="form-select" name="protocol" required>
<label class="form-label">Protocol</label>
<select class="form-select" name="protocol">
<option value="TCP">TCP</option>
<option value="UDP">UDP</option>
<option value="SCTP">SCTP</option>
@ -169,18 +168,34 @@
</div>
<div class="mb-3">
<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 class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Add Port</button>
<button type="button" class="btn btn-link link-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary ms-auto">Add Port</button>
</div>
</form>
</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 -->
<div class="modal modal-blur fade" id="deleteAppModal" tabindex="-1">
<div class="modal-dialog modal-sm modal-dialog-centered">
@ -200,24 +215,181 @@
</div>
</div>
<!-- Delete Port Modals -->
{% for port in app.ports %}
<div class="modal modal-blur fade" id="deletePortModal{{ port.id }}" 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 delete port {{ port.port_number }}/{{ port.protocol }}.</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<form method="POST" action="{{ url_for('api.delete_port', port_id=port.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-danger">Delete Port</button>
</form>
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
<!-- Add some additional CSS for better markdown rendering -->
<style>
.markdown-content {
line-height: 1.6;
padding: 1.5rem !important;
}
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
margin-top: 1.5rem;
margin-bottom: 1rem;
font-weight: 600;
}
.markdown-content h1 {
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 %}

Binary file not shown.

BIN
instance/app.db Normal file

Binary file not shown.