1 Commits

Author SHA1 Message Date
Tyler 8f2d56c05a Better UI for both Desktop and Mobile 2025-05-14 15:31:43 -10:00
17 changed files with 527 additions and 467 deletions
+35 -58
View File
@@ -6,11 +6,11 @@ import html
import base64
import hashlib
import secrets
import datetime
import subprocess
import platform
from datetime import UTC
from datetime import datetime, timedelta
import sys
import psutil
# ===== Third-Party Imports =====
from flask import (
@@ -138,7 +138,7 @@ def log_admin_event(message: str):
try:
key = load_admin_key()
cipher = Fernet(key)
timestamp = datetime.datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S")
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
encrypted = cipher.encrypt(f"[{timestamp}] {message}".encode())
with open(ADMIN_LOG_FILE, 'ab') as f:
f.write(encrypted + b"\n")
@@ -149,12 +149,12 @@ def log_admin_event(message: str):
def cleanup_expired_files():
"""Remove files older than MAX_FILE_AGE_DAYS."""
try:
now = datetime.datetime.now(UTC)
now = datetime.now()
for fname in os.listdir(UPLOAD_FOLDER):
if fname.endswith(".enc") or fname.endswith(".json"):
path = os.path.join(UPLOAD_FOLDER, fname)
try:
file_time = datetime.datetime.fromtimestamp(os.path.getmtime(path), UTC)
file_time = datetime.datetime.fromtimestamp(os.path.getmtime(path), )
age = (now - file_time).days
if age > MAX_FILE_AGE_DAYS:
os.remove(path)
@@ -208,7 +208,7 @@ def handle_file_upload(request):
meta = {
'pickup_password': base64.urlsafe_b64encode(hashlib.sha256(pickup_password.encode()).digest()).decode(),
'original_name': filename,
'timestamp': datetime.datetime.now(UTC).isoformat()
'timestamp': datetime.datetime.now().isoformat()
}
with open(os.path.join(UPLOAD_FOLDER, f"{random_id}.json"), 'w') as f:
json.dump(meta, f)
@@ -408,48 +408,30 @@ def admin_page():
cleanup_expired_files()
routes = [rule.rule for rule in app.url_map.iter_rules() if rule.endpoint != 'static']
# Get uptime based on OS
if platform.system() == "Windows":
try:
# Windows uptime using PowerShell
ps_command = "(Get-CimInstance -ClassName Win32_OperatingSystem).LastBootUpTime"
uptime_output = subprocess.check_output(["powershell", "-Command", ps_command], shell=True).decode()
# Convert the PowerShell DateTime to Python datetime
boot_time = datetime.datetime.strptime(uptime_output.strip(), "%A, %B %d, %Y %I:%M:%S %p")
# Make boot_time timezone-aware (assuming local time)
boot_time = boot_time.replace(tzinfo=datetime.timezone.utc)
current_time = datetime.datetime.now(UTC)
uptime = current_time - boot_time
uptime_str = f"{uptime.days} days, {uptime.seconds // 3600} hours, {(uptime.seconds % 3600) // 60} minutes"
except Exception as e:
print(f"[ERROR] Failed to get Windows uptime: {str(e)}")
uptime_str = "Unavailable"
else:
try:
# Try reading from /proc/uptime first
with open('/proc/uptime', 'r') as f:
uptime_seconds = float(f.readline().split()[0])
days = int(uptime_seconds // 86400)
hours = int((uptime_seconds % 86400) // 3600)
minutes = int((uptime_seconds % 3600) // 60)
uptime_str = f"{days} days, {hours} hours, {minutes} minutes"
except Exception:
try:
# Fallback to uptime command if /proc/uptime fails
uptime_str = subprocess.check_output("uptime -p", shell=True).decode().strip()
except Exception as e:
print(f"[ERROR] Failed to get Linux uptime: {str(e)}")
uptime_str = "Unavailable"
now = datetime.now()
try:
boot_time = datetime.fromtimestamp(psutil.boot_time())
uptime = now - boot_time
days = uptime.days
hours, remainder = divmod(uptime.seconds, 3600)
minutes = remainder // 60
uptime_str = f"{days} days, {hours} hours, {minutes} minutes"
except Exception as e:
print(f"[ERROR] Uptime calculation failed: {e}")
uptime_str = "Unavailable"
server_info = {
"uptime": uptime_str,
"time": datetime.datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S"),
"python": platform.python_version(),
"debug": app.debug
"server_time": now.strftime("%Y-%m-%d %H:%M:%S"),
"python_version": platform.python_version(),
"debug_mode": app.debug
}
return render_template("admin.html", routes=routes, server_info=server_info)
@app.route("/restart-server", methods=["POST"])
def restart_server():
"""Restart the server."""
@@ -458,9 +440,7 @@ def restart_server():
try:
if platform.system() == "Windows":
# Get the current process ID
current_pid = os.getpid()
# Create a batch file to restart the server
restart_script = f"""
@echo off
timeout /t 2 /nobreak
@@ -470,33 +450,30 @@ def restart_server():
"""
with open("restart.bat", "w") as f:
f.write(restart_script)
# Start the restart script and exit
subprocess.Popen(["restart.bat"], shell=True)
return jsonify({"message": "Server restart initiated"}), 200
else:
# For Linux/Unix systems, use a Python-based restart
# Get the current Python interpreter and script path
current_pid = os.getpid()
python_path = sys.executable
script_path = os.path.abspath(__file__)
current_pid = os.getpid()
# Create a shell script to restart the server
restart_script = f"""#!/bin/bash
sleep 2
kill -9 {current_pid}
export PRODUCTION=true
{python_path} {script_path}
"""
# Create a safer and cleaner restart script
restart_script = """#!/bin/bash
sleep 2
PID=$1
kill "$PID"
while kill -0 "$PID" 2>/dev/null; do sleep 0.5; done
export PRODUCTION=true
exec "$2" "$3"
"""
# Write and make the script executable
with open("restart.sh", "w") as f:
f.write(restart_script)
os.chmod("restart.sh", 0o755)
# Start the restart script and exit
subprocess.Popen(["./restart.sh"], shell=True)
subprocess.Popen(["./restart.sh", str(current_pid), python_path, script_path])
return jsonify({"message": "Server restart initiated"}), 200
except Exception as e:
print(f"[ERROR] Failed to restart server: {str(e)}")
return jsonify({"error": f"Failed to restart server: {str(e)}"}), 500
+1
View File
@@ -4,6 +4,7 @@ flask==3.0.3
cryptography==42.0.5
waitress==2.1.2
werkzeug==3.0.1
psutil>=5.9.0,<6.0.0
# nginx - Only needed for Nginx integration, not installed via pip
# Run pip install -r requirements.txt
+7
View File
@@ -0,0 +1,7 @@
@echo off
timeout /t 2 /nobreak
taskkill /F /PID 15428
set PRODUCTION=true
start "" "python" "app.py"
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
sleep 2
# Save current process PID
PID=$1
# Gracefully stop the current server
kill "$PID"
# Wait until it exits
while kill -0 "$PID" 2>/dev/null; do
sleep 0.5
done
# Restart with the same interpreter and script
export PRODUCTION=true
exec "$2" "$3"
+53 -57
View File
@@ -1,7 +1,7 @@
/* ===== Global Reset ===== */
* {
box-sizing: border-box;
margin: 3px;
gap: 6px !important;
padding: 0;
}
@@ -21,21 +21,42 @@ body {
}
@media (max-width: 600px) {
#sitemap-section,
#password-change-section,
#server-update-section,
#server-status-section,
#server-logs-section,
#system-settings-section {
padding: 20px;
margin-bottom: 20px;
}
#sitemap-section li,
#server-status-section li {
font-size: 0.9em;
padding: 6px;
}
#logContainer {
font-size: 0.9em;
padding: 10px;
}
body {
font-size: 11px;
padding: 10px;
}
.button-group {
.button-group,
.admin-button-grid {
flex-direction: column;
gap: 12px;
align-items: stretch;
align-items: center;
}
.button-group button {
width: 100%;
min-width: unset;
max-width: 100%;
.button-group button,
.admin-button-grid button {
min-width: 75%;
max-width: 75%;
}
header {
@@ -48,7 +69,6 @@ body {
.logo-container {
flex-direction: column;
align-items: center;
gap: 12px;
}
.logo-container img {
@@ -77,10 +97,17 @@ body {
.admin-button-grid {
grid-template-columns: 1fr;
}
.status-list {
width: 100%;
max-width: 400px;
padding-left: 0;
list-style: none;
word-wrap: break-word;
overflow-wrap: break-word;
}
}
/* ===== Header ===== */
header {
display: flex;
@@ -99,7 +126,6 @@ header {
.logo-container {
display: flex;
align-items: center;
gap: 10px; /* optional: controls spacing between logo and text */
}
.logo-container img {
@@ -135,7 +161,6 @@ main {
width: 100%;
max-width: 800px;
padding: 0;
gap: 0;
}
/* ===== Card Styling ===== */
@@ -153,12 +178,22 @@ main {
display: flex !important;
flex-direction: column;
align-items: center;
gap: 0px;
max-width: 725px;
width: 100%;
}
.status-list {
width: 100%;
max-width: 400px;
padding-left: 0;
list-style: none;
word-wrap: break-word;
overflow-wrap: break-word;
}
/* ===== Inputs, Textareas, Selects ===== */
button,
select,
input,
@@ -182,6 +217,7 @@ input[type="file"] {
color: #28E060;
text-align: left;
transition: 0.3s;
min-height: 50px;
}
select {
@@ -193,10 +229,6 @@ textarea {
resize: none;
}
input[type="password"] {
min-height: 50px;
}
/* ===== File Input Customization ===== */
input[type="file"] {
border: 2px dashed #28E060;
@@ -247,7 +279,6 @@ select:focus {
display: flex;
flex-wrap: nowrap;
justify-content: center;
gap: 15px;
width: 100%;
}
@@ -287,7 +318,6 @@ button {
.admin-button-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--spacer); /* e.g., 20px between rows and columns */
justify-items: center;
width: 100%;
max-width: 640px;
@@ -308,7 +338,6 @@ button {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
}
.toggle-label {
@@ -376,7 +405,6 @@ button {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
width: 100%;
}
@@ -524,8 +552,8 @@ footer {
select,
#input-text,
#output-text {
width: 100%;
max-width: 90%;
width: 100% !important;
max-width: 90% !important;
}
}
@@ -556,7 +584,6 @@ footer {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
margin-top: 12px;
margin-bottom: 12px;
}
@@ -590,12 +617,6 @@ form {
align-items: center;
}
form input {
min-height: 50px;
width: 80%;
max-width: 500px;
text-align: left;
}
/* ===== Section Card Styling ===== */
section.card {
@@ -620,7 +641,6 @@ section.card {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
margin-bottom: 25px;
max-width: 725px;
width: 100%;
@@ -774,7 +794,7 @@ section.card {
#sitemap-section li,
#server-status-section li {
margin-bottom: 10px;
margin-bottom: 6px;
padding: 8px;
background-color: #2c2f33;
border-radius: 6px;
@@ -811,27 +831,3 @@ section.card {
border-radius: 12px;
box-shadow: 0 0 15px #28E060;
}
/* ===== Mobile Responsive Adjustments ===== */
@media (max-width: 768px) {
#sitemap-section,
#password-change-section,
#server-update-section,
#server-status-section,
#server-logs-section,
#system-settings-section {
padding: 20px;
margin-bottom: 20px;
}
#sitemap-section li,
#server-status-section li {
font-size: 0.9em;
padding: 6px;
}
#logContainer {
font-size: 0.9em;
padding: 10px;
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

+48
View File
@@ -298,6 +298,54 @@ function checkForPacman() {
window.exitGame();
}
}
function copyShareLink() {
const linkEl = document.getElementById("share-link");
const feedback = document.getElementById("shared-link-feedback");
if (!linkEl) return;
const linkText = linkEl.href || linkEl.textContent.trim();
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(linkText).then(() => {
showCopyFeedback(feedback);
}).catch(() => {
fallbackCopy(linkText, feedback);
});
} else {
fallbackCopy(linkText, feedback);
}
}
function fallbackCopy(text, feedbackEl) {
const tempInput = document.createElement("input");
tempInput.value = text;
document.body.appendChild(tempInput);
tempInput.select();
try {
document.execCommand("copy");
showCopyFeedback(feedbackEl);
} catch (err) {
alert("Copy failed. Please copy manually.");
}
document.body.removeChild(tempInput);
}
function showCopyFeedback(feedbackEl) {
if (!feedbackEl) return;
feedbackEl.style.display = "block";
feedbackEl.classList.add("show");
setTimeout(() => {
feedbackEl.classList.remove("show");
setTimeout(() => {
feedbackEl.style.display = "none";
}, 300);
}, 3000);
}
function startPacman() { }
function exitGame() { }
+3 -3
View File
@@ -32,14 +32,14 @@
<main>
<section class="card form-group" style="padding: 50px 30px;">
<h2 style="color: #ff0066; font-size: 2.5em;">404 - Not Found</h2>
<p class="mt-4" style="font-size: 1.2em; color: #cccccc;">
<p style="font-size: 1.2em; color: #cccccc;">
Whoops! That page doesn't seem to exist. Maybe it got encrypted?
</p>
<!-- Navigation -->
<div class="button-group mt-4">
<div class="button-group">
<a href="{{ url_for('index') }}">
<button type="button">⬅️ Return Home</button>
<button type="button">Return Home</button>
</a>
</div>
</section>
+3 -3
View File
@@ -31,16 +31,16 @@
<!-- Main Content -->
<main>
<section class="card form-group" style="padding: 50px 30px;">
<h2 style="color: #ff3300; font-size: 2.5em;">💥 500 - Server Error</h2>
<h2 style="color: #ff3300; font-size: 2.5em;">500 - Server Error</h2>
<p class="mt-4" style="font-size: 1.2em; color: #cccccc;">
Uh oh! The ghosts chomped the server wires. 🧟‍♂️👾
Uh oh! The ghosts chomped the server wires.
We're working on patching it up.
</p>
<!-- Navigation -->
<div class="button-group mt-4">
<a href="{{ url_for('index') }}">
<button type="button">⬅️ Return Home</button>
<button type="button">Return Home</button>
</a>
</div>
</section>
+9 -8
View File
@@ -53,12 +53,12 @@
<form action="{{ url_for('admin_logout') }}" method="GET" style="display: inline;">
<button type="submit">Log Out</button>
</form>
<button onclick="updateServer()">Pull Latest Changes</button>
<button onclick="updateServer()">Update Server</button>
<form action="{{ url_for('admin_settings') }}" method="GET" style="display: inline;">
<button type="submit">Manage Upload Settings</button>
<button type="submit">Settings</button>
</form>
<button onclick="resetAdmin()" class="danger-button">Reset Admin</button>
<button onclick="clearUploads()" class="danger-button">Clear Uploaded Files</button>
<button onclick="clearUploads()" class="danger-button">Clear PacShare</button>
</div>
@@ -88,14 +88,15 @@
<!-- Server Status Section -->
<section id="server-status-section" class="card form-group">
<h2>Server Status</h2>
<ul style="width: 400px;">
<li>Uptime: <code>0 days, 11 hours, 47 minutes</code></li>
<li>Server Time: <code>2025-05-14 14:32:18</code></li>
<li>Python Version: <code>3.13.3</code></li>
<li>Flask Debug Mode: <code>True</code></li>
<ul class="status-list">
<li>Uptime: <code>{{ server_info.uptime }}</code></li>
<li>Server Time: <code>{{ server_info.server_time }}</code></li>
<li>Python Version: <code>{{ server_info.python_version }}</code></li>
<li>Flask Debug Mode: <code>{{ server_info.debug_mode }}</code></li>
</ul>
</section>
<!-- Server Logs Section -->
<section id="server-logs-section" class="card form-group">
<h2>Server Logs</h2>
+11 -7
View File
@@ -18,16 +18,20 @@
</head>
<body class="dark">
<!-- Header -->
<header class="card">
<h1>PacCrypt Admin</h1>
<p>Administrator Login</p>
</header>
<header class="card logo-header">
<div class="logo-container">
<img src="{{ url_for('static', filename='img/PacCrypt.png') }}" alt="PacCrypt Logo" />
<div class="logo-text">
<h1>PACCRYPT</h1>
<p>Admin Login</p>
</div>
</div>
</header>
<!-- Main Content -->
<main>
<!-- Login Form Section -->
<section class="card form-group">
<h2>🔑 Admin Login</h2>
<h2>Admin Login</h2>
<!-- Flash Messages -->
{% with messages = get_flashed_messages() %}
@@ -41,7 +45,7 @@
<input type="text" name="username" placeholder="Username" required />
<input type="password" name="password" placeholder="Password" required />
<div class="button-group mt-3">
<button type="submit">🚪 Log In</button>
<button type="submit">Log In</button>
</div>
</form>
</section>
+13 -8
View File
@@ -16,16 +16,21 @@
</head>
<body class="dark">
<!-- Header -->
<header class="card">
<h1>PacCrypt Admin Settings</h1>
<p>Manage upload configuration</p>
</header>
<!-- Header -->
<header class="card logo-header">
<div class="logo-container">
<img src="{{ url_for('static', filename='img/PacCrypt.png') }}" alt="PacCrypt Logo" />
<div class="logo-text">
<h1>PACCRYPT</h1>
<p>Server Settings</p>
</div>
</div>
</header>
<!-- Main Content -->
<main>
<!-- Settings Form Section -->
<section class="card form-group">
<h2>⚙️ Upload Settings</h2>
<h2>Upload Settings</h2>
<!-- Flash Messages -->
{% with messages = get_flashed_messages() %}
@@ -51,9 +56,9 @@
<!-- Action Buttons -->
<div class="button-group mt-4">
<button type="submit">💾 Save Settings</button>
<button type="submit">Save Settings</button>
<a href="{{ url_for('admin_page') }}">
<button type="button">⬅️ Back to Admin Panel</button>
<button type="button">Back to Admin Panel</button>
</a>
</div>
</form>
+6 -7
View File
@@ -49,7 +49,7 @@
<canvas id="pacmanCanvas" width="800" height="600"></canvas>
</div>
<audio id="chomp-sound" src="{{ url_for('static', filename='audio/chomp.mp3') }}"></audio>
<div class="button-group">
<div class="button-group" style="margin-top: 6px;">
<button type="button" onclick="resetGame()">Restart Game</button>
<button type="button" onclick="exitGame()">Exit Game</button>
</div>
@@ -112,9 +112,8 @@
<!-- File Sharing Section -->
<section id="sharing-section" class="card form-group">
<h2>PacCrypt Share</h2>
<h3>Securely share encrypted files.</h3>
<p>Do not lose your passwords, data will be lost forever!</p>
<h2 style="margin-bottom: unset;">PacShare</h2>
<p style="margin-top: unset;">Securely share encrypted files.</p>
<!-- Flash Messages -->
{% with messages = get_flashed_messages() %}
@@ -126,8 +125,7 @@
{% if "pickup" in message %}
<div class="share-link-container">
<a id="share-link" href="{{ message.split(' at ')[1] }}" target="_blank">{{ message.split(" at ")[1] }}</a>
<!--- <span id="share-link">{{ message.split(" at ")[1] }}</span> --->
<button type="button" id="copy-share-btn">Copy Link</button>
<button type="button" onclick="copyShareLink()">Copy Link</button>
<div id="shared-link-feedback" class="copy-feedback">Link copied to clipboard!</div>
</div>
{% endif %}
@@ -138,6 +136,7 @@
{% endif %}
{% endwith %}
<!-- File Upload Form -->
<!-- Share Link Container (initially hidden) -->
<div class="share-link-container" id="share-link-container" style="display: none;">
@@ -153,7 +152,7 @@
<button type="submit">Upload and Generate Link</button>
</div>
</form>
<p style="color: #9c0000;">BOTH PASSWORDS ARE REQUIRED FOR PICKUP</p>
<script>
document.getElementById('upload-form').addEventListener('submit', async (e) => {
e.preventDefault();
+12 -7
View File
@@ -15,16 +15,21 @@
</head>
<body class="dark">
<!-- Header -->
<header class="card">
<h1>PacCrypt</h1>
<p>Secure File Pickup and Decryption</p>
</header>
<header class="card logo-header">
<div class="logo-container">
<img src="{{ url_for('static', filename='img/PacCrypt.png') }}" alt="PacCrypt Logo" />
<div class="logo-text">
<h1>PACCRYPT</h1>
<p>Encrypted File Pickup</p>
</div>
</div>
</header>
<!-- Main Content -->
<main>
<!-- File Pickup Section -->
<section id="pickup-section" class="card form-group">
<h2>🔐 File Pickup</h2>
<h2>File Pickup</h2>
<!-- Flash Messages -->
{% with messages = get_flashed_messages() %}
@@ -61,14 +66,14 @@
</div>
<div class="button-group">
<button type="submit">📥 Decrypt and Download</button>
<button type="submit">Decrypt and Download</button>
</div>
</form>
</section>
<!-- Security Notice Section -->
<section id="security-notice-section" class="card form-group">
<h2>🛡️ Security Notice</h2>
<h2>Security Notice</h2>
<p style="color: #00ff99; text-align: center;">
Make sure you're on the correct domain before entering any passwords.<br>
Your file will be permanently deleted after download.