This commit is contained in:
pika 2025-04-03 13:51:52 +02:00
parent 78ce15e82d
commit 0a31714a93
10 changed files with 159 additions and 149 deletions

View file

@ -20,20 +20,19 @@ RUN pip install --upgrade pip && \
COPY . . COPY . .
# Create the instance directory for SQLite # Create the instance directory for SQLite
RUN mkdir -p instance && \ # RUN mkdir -p instance && \
chmod 777 instance # chmod 777 instance
# Create a non-root user to run the app # Create a non-root user to run the app
RUN useradd -m appuser && \ # RUN useradd -m appuser && \
chown -R appuser:appuser /app # chown -R appuser:appuser /app
USER appuser # USER appuser
# Set environment variables # Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
SECRET_KEY="" \
FLASK_APP=wsgi.py FLASK_APP=wsgi.py
# Run gunicorn # Run gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--timeout", "120", "--workers", "4", "wsgi:app"] CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--timeout", "120", "--workers", "4", "run:app"]

View file

@ -298,7 +298,6 @@ def app_view(app_id):
@login_required @login_required
def app_edit(app_id): def app_edit(app_id):
"""Edit an existing application with comprehensive error handling""" """Edit an existing application 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()
@ -323,87 +322,57 @@ def app_edit(app_id):
# Check for port conflicts proactively # Check for port conflicts proactively
conflicts = [] conflicts = []
seen_ports = set() # To track ports already seen in this submission
for i, (port_number, protocol, _) in enumerate(port_data): for i, (port_number, protocol, _) in enumerate(port_data):
try: try:
clean_port = int(port_number) clean_port = int(port_number)
# Check if this port has already been seen in this submission
port_key = f"{clean_port}/{protocol}"
if port_key in seen_ports:
conflicts.append((clean_port, protocol, "Duplicate port in submission"))
continue
seen_ports.add(port_key)
# Check if the port is in use by another application
in_use, conflicting_app_name = is_port_in_use( in_use, conflicting_app_name = is_port_in_use(
clean_port, protocol, server_id, exclude_app_id=app_id clean_port, protocol, server_id, exclude_app_id=app_id
) )
if in_use: if in_use:
conflicts.append((clean_port, protocol, conflicting_app_name)) conflicts.append((clean_port, protocol, f"Port {clean_port}/{protocol} is already in use by application '{conflicting_app_name}'"))
except (ValueError, TypeError): except (ValueError, TypeError):
continue continue
if conflicts: if conflicts:
# Find the IDs of conflicting apps for linking for conflict in conflicts:
conflict_msgs = [] flash(f"Conflict: {conflict[0]}/{conflict[1]} - {conflict[2]}", "danger")
for port, protocol, conflict_app_name in conflicts: return render_template("dashboard/app_edit.html", app=app, servers=servers)
conflict_app = App.query.filter_by(name=conflict_app_name, server_id=server_id).first()
if conflict_app:
edit_url = url_for('dashboard.app_edit', app_id=conflict_app.id)
conflict_msgs.append(
f'Port {port}/{protocol} is in use by <a href="{edit_url}">{conflict_app_name}</a>'
)
else:
conflict_msgs.append(f'Port {port}/{protocol} is in use by {conflict_app_name}')
for msg in conflict_msgs:
flash(msg, "danger")
return render_template(
"dashboard/app_form.html",
title=f"Edit {app.name}",
edit_mode=True,
servers=servers,
app=app
)
# Replace local validation with shared function # Update application details
valid, error = validate_app_data(name, server_id, existing_app_id=app_id) app.name = name
app.server_id = server_id
app.documentation = documentation
app.url = url
if valid: # Only delete existing ports if new port data is provided
# Update application with URL if port_data:
app.name = name # Remove existing ports and add new ones
app.server_id = server_id Port.query.filter_by(app_id=app_id).delete()
app.documentation = documentation for port_number, protocol, description in port_data:
app.url = url new_port = Port(
app_id=app_id,
port_number=int(port_number),
protocol=protocol,
description=description
)
db.session.add(new_port)
# Update application db.session.commit()
from app.utils.app_utils import save_app flash("Application updated successfully", "success")
return redirect(url_for("dashboard.app_view", app_id=app_id))
success, updated_app, error = save_app( return render_template("dashboard/app_edit.html", app=app, servers=servers)
name, server_id, documentation, port_data, app_id, url
)
if success:
flash("Application updated successfully", "success")
return redirect(url_for("dashboard.app_view", app_id=app_id))
else:
flash(error, "danger")
# Extract app name from error and provide link if it's a conflict
if "already in use by application" in error:
app_name = error.split("'")[1] # Extract app name from error message
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'Would you like to edit the conflicting application? '
f'<a href="{edit_url}">Edit {app_name}</a>',
"info"
)
else:
flash(error, "danger")
# GET request - display the form
return render_template(
"dashboard/app_form.html",
title=f"Edit {app.name}",
edit_mode=True,
servers=servers,
app=app
)
@bp.route("/app/<int:app_id>/delete", methods=["POST"]) @bp.route("/app/<int:app_id>/delete", methods=["POST"])

View file

@ -4,34 +4,34 @@ import os
bp = Blueprint("static_assets", __name__) bp = Blueprint("static_assets", __name__)
@bp.route("/static/libs/tabler-icons/tabler-icons.min.css") # @bp.route("/static/libs/tabler-icons/tabler-icons.min.css")
def tabler_icons(): # def tabler_icons():
"""Serve tabler-icons CSS from node_modules or download if missing""" # """Serve tabler-icons CSS from node_modules or download if missing"""
icons_path = os.path.join(current_app.static_folder, "libs", "tabler-icons") # icons_path = os.path.join(current_app.static_folder, "libs", "tabler-icons")
#
# Create directory if it doesn't exist # # Create directory if it doesn't exist
if not os.path.exists(icons_path): # if not os.path.exists(icons_path):
os.makedirs(icons_path) # os.makedirs(icons_path)
#
css_file = os.path.join(icons_path, "tabler-icons.min.css") # css_file = os.path.join(icons_path, "tabler-icons.min.css")
#
# If file doesn't exist, download from CDN # # If file doesn't exist, download from CDN
if not os.path.exists(css_file): # if not os.path.exists(css_file):
import requests # import requests
#
try: # try:
cdn_url = "https://cdn.jsdelivr.net/npm/@tabler/icons@latest/iconfont/tabler-icons.min.css" # cdn_url = "https://cdn.jsdelivr.net/npm/@tabler/core@1.1.1/dist/css/tabler.min.css"
response = requests.get(cdn_url) # response = requests.get(cdn_url)
if response.status_code == 200: # if response.status_code == 200:
with open(css_file, "wb") as f: # with open(css_file, "wb") as f:
f.write(response.content) # f.write(response.content)
print(f"Downloaded tabler-icons.min.css from CDN") # print(f"Downloaded tabler-icons.min.css from CDN")
else: # else:
print(f"Failed to download tabler-icons CSS: {response.status_code}") # print(f"Failed to download tabler-icons CSS: {response.status_code}")
except Exception as e: # except Exception as e:
print(f"Error downloading tabler-icons CSS: {e}") # print(f"Error downloading tabler-icons CSS: {e}")
#
return send_from_directory(icons_path, "tabler-icons.min.css") # return send_from_directory(icons_path, "tabler-icons.min.css")
@bp.route("/static/css/tabler.min.css") @bp.route("/static/css/tabler.min.css")
@ -50,7 +50,7 @@ def tabler_css():
import requests import requests
try: try:
cdn_url = "https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/css/tabler.min.css" cdn_url = "https://cdn.jsdelivr.net/npm/@tabler/core@1.1.1/dist/css/tabler.min.css"
response = requests.get(cdn_url) response = requests.get(cdn_url)
if response.status_code == 200: if response.status_code == 200:
with open(css_file, "wb") as f: with open(css_file, "wb") as f:
@ -82,7 +82,7 @@ def favicon():
try: try:
# Using a simple placeholder favicon # Using a simple placeholder favicon
cdn_url = "https://www.google.com/favicon.ico" cdn_url = "https://www.svgrepo.com/show/529863/server-minimalistic.svg"
response = requests.get(cdn_url) response = requests.get(cdn_url)
if response.status_code == 200: if response.status_code == 200:
with open(favicon_file, "wb") as f: with open(favicon_file, "wb") as f:

View file

@ -119,8 +119,6 @@
<select name="protocols[]" class="form-select"> <select name="protocols[]" class="form-select">
<option value="TCP" {% if port.protocol=='TCP' %}selected{% endif %}>TCP</option> <option value="TCP" {% if port.protocol=='TCP' %}selected{% endif %}>TCP</option>
<option value="UDP" {% if port.protocol=='UDP' %}selected{% endif %}>UDP</option> <option value="UDP" {% if port.protocol=='UDP' %}selected{% endif %}>UDP</option>
<option value="SCTP" {% if port.protocol=='SCTP' %}selected{% endif %}>SCTP</option>
<option value="OTHER" {% if port.protocol=='OTHER' %}selected{% endif %}>OTHER</option>
</select> </select>
</td> </td>
<td> <td>
@ -144,8 +142,6 @@
<select name="protocols[]" class="form-select"> <select name="protocols[]" class="form-select">
<option value="TCP" selected>TCP</option> <option value="TCP" selected>TCP</option>
<option value="UDP">UDP</option> <option value="UDP">UDP</option>
<option value="SCTP">SCTP</option>
<option value="OTHER">OTHER</option>
</select> </select>
</td> </td>
<td> <td>
@ -251,9 +247,48 @@
</script> </script>
<script> <script>
// Make sure removePortRow is in the global scope
window.removePortRow = function (button) {
const tbody = document.querySelector('#ports-table tbody');
const row = button.closest('tr');
// Get current number of rows
const rowCount = tbody.querySelectorAll('tr').length;
// If this is the last row, clear its values instead of removing
if (rowCount <= 1) {
const inputs = row.querySelectorAll('input');
inputs.forEach(input => {
input.value = '';
});
// Reset protocol to TCP
const protocolSelect = row.querySelector('select[name="protocols[]"]');
if (protocolSelect) {
protocolSelect.value = 'TCP';
}
// Add a visual indicator that this row is empty
row.classList.add('table-secondary', 'opacity-50');
// Show a helping message
showNotification('Application saved with no ports. Use "Add Port" to add ports.', 'info');
} else {
// Remove the row if there are other rows
row.remove();
}
};
// Port management functions // Port management functions
function addPortRow(portNumber = '', protocol = 'TCP', description = '') { function addPortRow(portNumber = '', protocol = 'TCP', description = '') {
const tbody = document.querySelector('#ports-table tbody'); const tbody = document.querySelector('#ports-table tbody');
// Remove the empty row indicator if present
const emptyRows = tbody.querySelectorAll('tr.table-secondary.opacity-50');
if (emptyRows.length > 0) {
emptyRows.forEach(row => row.remove());
}
const newRow = document.createElement('tr'); const newRow = document.createElement('tr');
newRow.innerHTML = ` newRow.innerHTML = `
<td class="position-relative"> <td class="position-relative">
@ -265,8 +300,6 @@
<select name="protocols[]" class="form-select"> <select name="protocols[]" class="form-select">
<option value="TCP" ${protocol === 'TCP' ? 'selected' : ''}>TCP</option> <option value="TCP" ${protocol === 'TCP' ? 'selected' : ''}>TCP</option>
<option value="UDP" ${protocol === 'UDP' ? 'selected' : ''}>UDP</option> <option value="UDP" ${protocol === 'UDP' ? 'selected' : ''}>UDP</option>
<option value="SCTP" ${protocol === 'SCTP' ? 'selected' : ''}>SCTP</option>
<option value="OTHER" ${protocol === 'OTHER' ? 'selected' : ''}>OTHER</option>
</select> </select>
</td> </td>
<td> <td>
@ -285,10 +318,6 @@
setTimeout(setupPortValidation, 50); setTimeout(setupPortValidation, 50);
} }
function removePortRow(button) {
button.closest('tr').remove();
}
async function generateRandomPort() { async function generateRandomPort() {
try { try {
const serverId = document.querySelector('select[name="server_id"]').value; const serverId = document.querySelector('select[name="server_id"]').value;

View file

@ -156,7 +156,7 @@
<label class="form-label required">Port Number</label> <label class="form-label required">Port Number</label>
<div class="input-group"> <div class="input-group">
<input type="number" class="form-control" name="port_number" min="1" max="65535" required> <input type="number" class="form-control" name="port_number" min="1" max="65535" required>
<button class="btn btn-outline-secondary" type="button" id="randomPortBtn"> <button class="btn btn-outline-secondary" type="button" id="suggestRandomPort">
<span class="ti ti-dice"></span> <span class="ti ti-dice"></span>
</button> </button>
</div> </div>
@ -166,8 +166,6 @@
<select class="form-select" name="protocol"> <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="OTHER">OTHER</option>
</select> </select>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@ -350,56 +348,62 @@
{% block extra_js %} {% block extra_js %}
<script> <script>
// Store app ID for JavaScript use // Global variable to store the port ID to delete
const appId = {{ app.id }}; let portIdToDelete = null;
let portToDelete = null;
// IMPORTANT: Define confirmDeletePort outside the DOMContentLoaded event // Function to confirm port deletion
// so it's available in the global scope
function confirmDeletePort(portId) { function confirmDeletePort(portId) {
portToDelete = portId; portIdToDelete = portId;
const modal = new bootstrap.Modal(document.getElementById('deletePortModal')); // Show the delete modal
modal.show(); const deleteModal = new bootstrap.Modal(document.getElementById('deletePortModal'));
deleteModal.show();
} }
// Set up event listeners when DOM is ready // Set up the confirm button in the delete modal
document.addEventListener('DOMContentLoaded', function () { 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;
})
.catch(error => {
console.error('Error fetching random port:', error);
});
});
}
// Handle port deletion confirmation
const confirmDeleteBtn = document.getElementById('confirmDeleteBtn'); const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
if (confirmDeleteBtn) { if (confirmDeleteBtn) {
confirmDeleteBtn.addEventListener('click', function () { confirmDeleteBtn.addEventListener('click', function () {
if (portToDelete) { if (portIdToDelete) {
// Create a form and submit it programmatically // Create a form to submit the delete request
const form = document.createElement('form'); const form = document.createElement('form');
form.method = 'POST'; form.method = 'POST';
form.action = `/api/app/${appId}/port/${portToDelete}/delete`; form.action = `/api/app/{{ app.id }}/port/${portIdToDelete}/delete`;
// Add CSRF token
const csrfInput = document.createElement('input'); const csrfInput = document.createElement('input');
csrfInput.type = 'hidden'; csrfInput.type = 'hidden';
csrfInput.name = 'csrf_token'; csrfInput.name = 'csrf_token';
csrfInput.value = '{{ csrf_token() }}'; csrfInput.value = '{{ csrf_token() }}';
form.appendChild(csrfInput); form.appendChild(csrfInput);
// Append to body, submit, and remove
document.body.appendChild(form); document.body.appendChild(form);
form.submit(); form.submit();
document.body.removeChild(form);
} }
}); });
} }
// Set up the random port button
const randomPortBtn = document.getElementById('randomPortBtn');
if (randomPortBtn) {
randomPortBtn.addEventListener('click', function () {
fetch(`/api/server/{{ app.server.id }}/free-port`)
.then(response => response.json())
.then(data => {
if (data.success && data.port) {
document.querySelector('input[name="port_number"]').value = data.port;
} else {
alert(data.error || 'Could not generate a random port');
}
})
.catch(error => {
console.error('Error generating random port:', error);
alert('Error generating random port');
});
});
}
}); });
</script> </script>
{% endblock %} {% endblock %}

View file

@ -17,8 +17,8 @@
<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') }}"
onerror="this.onerror=null;this.href='https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/css/tabler.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') }}" <!-- <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';"> <!-- 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/github-markdown-reading-view.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/github-markdown-reading-view.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/github-markdown-source-view.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/github-markdown-source-view.css') }}">

View file

@ -105,6 +105,10 @@ def process_app_ports(app_id, port_data, server_id=None):
if app: if app:
server_id = app.server_id server_id = app.server_id
# If no port data is provided, that's valid (app with no ports)
if not port_data:
return True, None
# Track the port+protocol combinations we've seen to avoid duplicates # Track the port+protocol combinations we've seen to avoid duplicates
seen_ports = set() seen_ports = set()

View file

@ -7,7 +7,7 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
# image: homedocs:latest # image: homedocs:latest
ports: ports:
- "5001:8000" - "5000:8000"
volumes: volumes:
- ./instance:/app/instance # Persist SQLite database - ./instance:/app/instance # Persist SQLite database
# environment: # environment:

4
run.py
View file

@ -10,6 +10,7 @@ from datetime import datetime
import random import random
import string import string
import json import json
from flask_wtf.csrf import CSRFProtect
# Add the current directory to Python path # Add the current directory to Python path
current_dir = os.path.abspath(os.path.dirname(__file__)) current_dir = os.path.abspath(os.path.dirname(__file__))
@ -79,6 +80,9 @@ def register_routes(app):
print("Starting Flask app with SQLite database...") print("Starting Flask app with SQLite database...")
app = create_app("development") app = create_app("development")
# Set up CSRF protection
csrf = CSRFProtect(app)
@app.shell_context_processor @app.shell_context_processor
def make_shell_context(): def make_shell_context():

View file

@ -1,6 +1,7 @@
import os import os
import secrets import secrets
from app import create_app from app import create_app
from flask_wtf.csrf import CSRFProtect
# Generate a secret key if not provided # Generate a secret key if not provided
if not os.environ.get("SECRET_KEY"): if not os.environ.get("SECRET_KEY"):