wip
This commit is contained in:
parent
78ce15e82d
commit
0a31714a93
10 changed files with 159 additions and 149 deletions
13
Dockerfile
13
Dockerfile
|
@ -20,20 +20,19 @@ RUN pip install --upgrade pip && \
|
|||
COPY . .
|
||||
|
||||
# Create the instance directory for SQLite
|
||||
RUN mkdir -p instance && \
|
||||
chmod 777 instance
|
||||
# RUN mkdir -p instance && \
|
||||
# chmod 777 instance
|
||||
|
||||
# Create a non-root user to run the app
|
||||
RUN useradd -m appuser && \
|
||||
chown -R appuser:appuser /app
|
||||
# RUN useradd -m appuser && \
|
||||
# chown -R appuser:appuser /app
|
||||
|
||||
USER appuser
|
||||
# USER appuser
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
SECRET_KEY="" \
|
||||
FLASK_APP=wsgi.py
|
||||
|
||||
# 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"]
|
||||
|
|
|
@ -298,7 +298,6 @@ def app_view(app_id):
|
|||
@login_required
|
||||
def app_edit(app_id):
|
||||
"""Edit an existing application with comprehensive error handling"""
|
||||
# Get the application and all servers
|
||||
app = App.query.get_or_404(app_id)
|
||||
servers = Server.query.all()
|
||||
|
||||
|
@ -323,87 +322,57 @@ def app_edit(app_id):
|
|||
|
||||
# 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)
|
||||
|
||||
# Check if the port is in use by another application
|
||||
in_use, conflicting_app_name = is_port_in_use(
|
||||
clean_port, protocol, server_id, exclude_app_id=app_id
|
||||
)
|
||||
|
||||
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):
|
||||
continue
|
||||
|
||||
if conflicts:
|
||||
# Find the IDs of conflicting apps for linking
|
||||
conflict_msgs = []
|
||||
for port, protocol, conflict_app_name in conflicts:
|
||||
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 conflict in conflicts:
|
||||
flash(f"Conflict: {conflict[0]}/{conflict[1]} - {conflict[2]}", "danger")
|
||||
return render_template("dashboard/app_edit.html", app=app, servers=servers)
|
||||
|
||||
for msg in conflict_msgs:
|
||||
flash(msg, "danger")
|
||||
# Update application details
|
||||
app.name = name
|
||||
app.server_id = server_id
|
||||
app.documentation = documentation
|
||||
app.url = url
|
||||
|
||||
return render_template(
|
||||
"dashboard/app_form.html",
|
||||
title=f"Edit {app.name}",
|
||||
edit_mode=True,
|
||||
servers=servers,
|
||||
app=app
|
||||
)
|
||||
# Only delete existing ports if new port data is provided
|
||||
if port_data:
|
||||
# Remove existing ports and add new ones
|
||||
Port.query.filter_by(app_id=app_id).delete()
|
||||
for port_number, protocol, description in port_data:
|
||||
new_port = Port(
|
||||
app_id=app_id,
|
||||
port_number=int(port_number),
|
||||
protocol=protocol,
|
||||
description=description
|
||||
)
|
||||
db.session.add(new_port)
|
||||
|
||||
# Replace local validation with shared function
|
||||
valid, error = validate_app_data(name, server_id, existing_app_id=app_id)
|
||||
db.session.commit()
|
||||
flash("Application updated successfully", "success")
|
||||
return redirect(url_for("dashboard.app_view", app_id=app_id))
|
||||
|
||||
if valid:
|
||||
# Update application with URL
|
||||
app.name = name
|
||||
app.server_id = server_id
|
||||
app.documentation = documentation
|
||||
app.url = url
|
||||
|
||||
# Update application
|
||||
from app.utils.app_utils import save_app
|
||||
|
||||
success, updated_app, error = save_app(
|
||||
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
|
||||
)
|
||||
return render_template("dashboard/app_edit.html", app=app, servers=servers)
|
||||
|
||||
|
||||
@bp.route("/app/<int:app_id>/delete", methods=["POST"])
|
||||
|
|
|
@ -4,34 +4,34 @@ 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/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/core@1.1.1/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-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")
|
||||
|
@ -50,7 +50,7 @@ def tabler_css():
|
|||
import requests
|
||||
|
||||
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)
|
||||
if response.status_code == 200:
|
||||
with open(css_file, "wb") as f:
|
||||
|
@ -82,7 +82,7 @@ def favicon():
|
|||
|
||||
try:
|
||||
# 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)
|
||||
if response.status_code == 200:
|
||||
with open(favicon_file, "wb") as f:
|
||||
|
|
|
@ -119,8 +119,6 @@
|
|||
<select name="protocols[]" class="form-select">
|
||||
<option value="TCP" {% if port.protocol=='TCP' %}selected{% endif %}>TCP</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>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -144,8 +142,6 @@
|
|||
<select name="protocols[]" class="form-select">
|
||||
<option value="TCP" selected>TCP</option>
|
||||
<option value="UDP">UDP</option>
|
||||
<option value="SCTP">SCTP</option>
|
||||
<option value="OTHER">OTHER</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -251,9 +247,48 @@
|
|||
</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
|
||||
function addPortRow(portNumber = '', protocol = 'TCP', description = '') {
|
||||
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');
|
||||
newRow.innerHTML = `
|
||||
<td class="position-relative">
|
||||
|
@ -265,8 +300,6 @@
|
|||
<select name="protocols[]" class="form-select">
|
||||
<option value="TCP" ${protocol === 'TCP' ? 'selected' : ''}>TCP</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>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -285,10 +318,6 @@
|
|||
setTimeout(setupPortValidation, 50);
|
||||
}
|
||||
|
||||
function removePortRow(button) {
|
||||
button.closest('tr').remove();
|
||||
}
|
||||
|
||||
async function generateRandomPort() {
|
||||
try {
|
||||
const serverId = document.querySelector('select[name="server_id"]').value;
|
||||
|
|
|
@ -156,7 +156,7 @@
|
|||
<label class="form-label required">Port Number</label>
|
||||
<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">
|
||||
<button class="btn btn-outline-secondary" type="button" id="suggestRandomPort">
|
||||
<span class="ti ti-dice"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -166,8 +166,6 @@
|
|||
<select class="form-select" name="protocol">
|
||||
<option value="TCP">TCP</option>
|
||||
<option value="UDP">UDP</option>
|
||||
<option value="SCTP">SCTP</option>
|
||||
<option value="OTHER">OTHER</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
|
@ -350,56 +348,62 @@
|
|||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Store app ID for JavaScript use
|
||||
const appId = {{ app.id }};
|
||||
let portToDelete = null;
|
||||
// Global variable to store the port ID to delete
|
||||
let portIdToDelete = null;
|
||||
|
||||
// IMPORTANT: Define confirmDeletePort outside the DOMContentLoaded event
|
||||
// so it's available in the global scope
|
||||
// Function to confirm port deletion
|
||||
function confirmDeletePort(portId) {
|
||||
portToDelete = portId;
|
||||
const modal = new bootstrap.Modal(document.getElementById('deletePortModal'));
|
||||
modal.show();
|
||||
portIdToDelete = portId;
|
||||
// Show the delete modal
|
||||
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 () {
|
||||
// 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');
|
||||
if (confirmDeleteBtn) {
|
||||
confirmDeleteBtn.addEventListener('click', function () {
|
||||
if (portToDelete) {
|
||||
// Create a form and submit it programmatically
|
||||
if (portIdToDelete) {
|
||||
// Create a form to submit the delete request
|
||||
const form = document.createElement('form');
|
||||
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');
|
||||
csrfInput.type = 'hidden';
|
||||
csrfInput.name = 'csrf_token';
|
||||
csrfInput.value = '{{ csrf_token() }}';
|
||||
|
||||
form.appendChild(csrfInput);
|
||||
|
||||
// Append to body, submit, and remove
|
||||
document.body.appendChild(form);
|
||||
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>
|
||||
{% endblock %}
|
|
@ -17,8 +17,8 @@
|
|||
<link rel="stylesheet" href="{{ url_for('static', filename='css/app.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';">
|
||||
<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='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/github-markdown-reading-view.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/github-markdown-source-view.css') }}">
|
||||
|
|
|
@ -105,6 +105,10 @@ def process_app_ports(app_id, port_data, server_id=None):
|
|||
if app:
|
||||
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
|
||||
seen_ports = set()
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ services:
|
|||
dockerfile: Dockerfile
|
||||
# image: homedocs:latest
|
||||
ports:
|
||||
- "5001:8000"
|
||||
- "5000:8000"
|
||||
volumes:
|
||||
- ./instance:/app/instance # Persist SQLite database
|
||||
# environment:
|
||||
|
|
4
run.py
4
run.py
|
@ -10,6 +10,7 @@ from datetime import datetime
|
|||
import random
|
||||
import string
|
||||
import json
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
|
||||
# Add the current directory to Python path
|
||||
current_dir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
@ -79,6 +80,9 @@ def register_routes(app):
|
|||
print("Starting Flask app with SQLite database...")
|
||||
app = create_app("development")
|
||||
|
||||
# Set up CSRF protection
|
||||
csrf = CSRFProtect(app)
|
||||
|
||||
|
||||
@app.shell_context_processor
|
||||
def make_shell_context():
|
||||
|
|
1
wsgi.py
1
wsgi.py
|
@ -1,6 +1,7 @@
|
|||
import os
|
||||
import secrets
|
||||
from app import create_app
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
|
||||
# Generate a secret key if not provided
|
||||
if not os.environ.get("SECRET_KEY"):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue