From 7ec213fad0b007cba767337a38b9324b440117dc Mon Sep 17 00:00:00 2001 From: Tyler <68524461+TySP-Dev@users.noreply.github.com> Date: Thu, 1 May 2025 18:46:29 -1000 Subject: [PATCH] V .4.1 --- app.py | 452 +++++++++++++++++++++------------- requirements.txt | 1 + restart.bat | 5 + static/css/styles.css | 356 ++++++++++++++++++++------ static/js/encryption.js | 40 ++- static/js/fileops.js | 187 ++++++++------ static/js/main.js | 9 +- static/js/pacman.js | 146 ++++++----- static/js/ui.js | 185 +++++++++----- templates/403.html | 20 +- templates/404.html | 22 +- templates/500.html | 22 +- templates/admin.html | 256 +++++++++++++------ templates/admin_login.html | 38 +-- templates/admin_settings.html | 36 +-- templates/admin_setup.html | 35 ++- templates/index.html | 130 +++++++--- templates/pickup.html | 40 +-- 18 files changed, 1321 insertions(+), 659 deletions(-) create mode 100644 restart.bat diff --git a/app.py b/app.py index d54b6bf..0777b27 100644 --- a/app.py +++ b/app.py @@ -1,3 +1,4 @@ +# ===== Standard Library Imports ===== import os import io import json @@ -5,11 +6,13 @@ import html import base64 import hashlib import secrets -import shutil import datetime import subprocess import platform +from datetime import UTC +import sys +# ===== Third-Party Imports ===== from flask import ( Flask, render_template, request, jsonify, session, redirect, url_for, flash, send_file @@ -20,9 +23,11 @@ from cryptography.hazmat.primitives.hashes import SHA256 from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.fernet import Fernet +# ===== Application Configuration ===== app = Flask(__name__) app.secret_key = os.getenv("FLASK_SECRET", os.urandom(24)) +# ===== Constants ===== ADMIN_CRED_FILE = 'admin_creds.json' ADMIN_KEY_FILE = 'admin_key.key' ADMIN_LOG_FILE = 'admin_logs.enc' @@ -35,8 +40,9 @@ DEFAULT_SETTINGS = { "max_file_size_bytes": 25 * 1024 * 1024 * 1024 # 25GB } -# === Settings === +# ===== Settings Management ===== def load_settings(): + """Load application settings from file or create with defaults.""" if not os.path.exists(SETTINGS_FILE): with open(SETTINGS_FILE, 'w') as f: json.dump(DEFAULT_SETTINGS, f) @@ -51,20 +57,25 @@ MAX_FILE_SIZE_BYTES = settings["max_file_size_bytes"] if not os.path.exists(UPLOAD_FOLDER): os.makedirs(UPLOAD_FOLDER) -# === Crypto === +# ===== Cryptographic Functions ===== def derive_key(password: str, salt: bytes) -> bytes: + """Derive a cryptographic key from password using PBKDF2.""" return PBKDF2HMAC(algorithm=SHA256(), length=32, salt=salt, iterations=200_000).derive(password.encode()) def hash_password(password: str, salt: bytes) -> str: + """Hash a password with salt for secure storage.""" return base64.urlsafe_b64encode(derive_key(password, salt)).decode() def simple_encode(text: str) -> str: + """Basic Caesar cipher encryption.""" return ''.join(ALPHABET[(ALPHABET.index(c) + 3) % 26] if c in ALPHABET else c for c in text.lower()) def simple_decode(text: str) -> str: + """Basic Caesar cipher decryption.""" return ''.join(ALPHABET[(ALPHABET.index(c) - 3) % 26] if c in ALPHABET else c for c in text.lower()) def advanced_encrypt(plaintext: str, password: str) -> str: + """Encrypt text using AES-GCM with password-derived key.""" salt = os.urandom(16) key = derive_key(password, salt) nonce = os.urandom(12) @@ -72,6 +83,7 @@ def advanced_encrypt(plaintext: str, password: str) -> str: return base64.urlsafe_b64encode(salt + nonce + ct).decode() def advanced_decrypt(token_b64: str, password: str) -> str: + """Decrypt text using AES-GCM with password-derived key.""" try: data = base64.urlsafe_b64decode(token_b64.encode()) salt, nonce, ct = data[:16], data[16:28], data[28:] @@ -80,8 +92,9 @@ def advanced_decrypt(token_b64: str, password: str) -> str: except Exception: return "[Error] Invalid password or corrupted data!" -# === Admin Auth === +# ===== Admin Authentication ===== def load_admin_key(): + """Load or generate admin encryption key.""" if not os.path.exists(ADMIN_KEY_FILE): with open(ADMIN_KEY_FILE, 'wb') as f: f.write(Fernet.generate_key()) @@ -89,6 +102,7 @@ def load_admin_key(): return f.read() def encrypt_creds(username, password): + """Encrypt and store admin credentials.""" key = load_admin_key() cipher = Fernet(key) salt = os.urandom(16) @@ -98,6 +112,7 @@ def encrypt_creds(username, password): f.write(cipher.encrypt(data)) def check_creds(username, password): + """Verify admin credentials.""" try: key = load_admin_key() cipher = Fernet(key) @@ -111,82 +126,104 @@ def check_creds(username, password): return False def log_admin_event(message: str): + """Log admin actions securely.""" try: key = load_admin_key() cipher = Fernet(key) - timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + timestamp = datetime.datetime.now(UTC).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") except Exception as e: print("[ERROR] Failed to write admin log:", e) -# === Text Encryption Route === +# ===== File Management ===== +def cleanup_expired_files(): + """Remove files older than MAX_FILE_AGE_DAYS.""" + now = datetime.datetime.now(UTC) + 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) + age = (now - file_time).days + if age > MAX_FILE_AGE_DAYS: + os.remove(path) + print(f"[INFO] Deleted expired file: {fname}") + except Exception as e: + print(f"[ERROR] Could not check/delete file {fname}: {e}") + +# ===== Route Handlers ===== @app.route("/", methods=["GET", "POST"]) def index(): + """Main application route handling encryption/decryption and file uploads.""" if request.method == 'POST': - if 'file' in request.files: # <-- Handling file upload - file = request.files['file'] - enc_password = request.form.get('enc_password') - pickup_password = request.form.get('pickup_password') - - if not file or not enc_password or not pickup_password: - flash('Missing fields') - return redirect(url_for('index')) - - if file.content_length and file.content_length > MAX_FILE_SIZE_BYTES: - flash(f"File too large! Limit: {MAX_FILE_SIZE_BYTES / (1024**3):.2f} GB") - return redirect(url_for('index')) - - filename = secure_filename(file.filename) - temp_path = os.path.join(UPLOAD_FOLDER, filename) - file.save(temp_path) - - with open(temp_path, 'rb') as f: - data = f.read() - - salt = os.urandom(16) - key = derive_key(enc_password, salt) - nonce = os.urandom(12) - ct = AESGCM(key).encrypt(nonce, data, None) - - random_id = secrets.token_urlsafe(24) - - with open(os.path.join(UPLOAD_FOLDER, f"{random_id}.enc"), 'wb') as f: - f.write(salt + nonce + ct) - os.remove(temp_path) - - meta = { - 'pickup_password': base64.urlsafe_b64encode(hashlib.sha256(pickup_password.encode()).digest()).decode(), - 'original_name': filename, - 'timestamp': datetime.datetime.utcnow().isoformat() - } - with open(os.path.join(UPLOAD_FOLDER, f"{random_id}.json"), 'w') as f: - json.dump(meta, f) - - pickup_url = request.host_url.rstrip('/') + url_for('pickup_file', file_id=random_id) - flash(pickup_url) - return redirect(url_for('index')) - - else: # <-- Handling encryption/decryption - data = request.get_json() - encryption_type = data.get("encryption-type", "basic") - operation = data.get("operation", "") - message = data.get("message", "") - password = data.get("password", "") - - if encryption_type == "basic": - result = simple_encode(message) if operation == "encrypt" else simple_decode(message) - else: - result = advanced_encrypt(message, password) if operation == "encrypt" else advanced_decrypt(message, password) - - return jsonify(result=html.escape(result)) - + if 'file' in request.files: + return handle_file_upload(request) + else: + return handle_text_operation(request) return render_template("index.html", result="", password="", encryption_type="advanced", settings=settings) -# === File Pickup Route === +def handle_file_upload(request): + """Process file upload and encryption.""" + file = request.files['file'] + enc_password = request.form.get('enc_password') + pickup_password = request.form.get('pickup_password') + + if not file or not enc_password or not pickup_password: + return jsonify({"error": "Missing fields"}), 400 + + if file.content_length and file.content_length > MAX_FILE_SIZE_BYTES: + return jsonify({"error": f"File too large! Limit: {MAX_FILE_SIZE_BYTES / (1024**3):.2f} GB"}), 400 + + filename = secure_filename(file.filename) + temp_path = os.path.join(UPLOAD_FOLDER, filename) + file.save(temp_path) + + with open(temp_path, 'rb') as f: + data = f.read() + + salt = os.urandom(16) + key = derive_key(enc_password, salt) + nonce = os.urandom(12) + ct = AESGCM(key).encrypt(nonce, data, None) + + random_id = secrets.token_urlsafe(24) + + with open(os.path.join(UPLOAD_FOLDER, f"{random_id}.enc"), 'wb') as f: + f.write(salt + nonce + ct) + os.remove(temp_path) + + meta = { + 'pickup_password': base64.urlsafe_b64encode(hashlib.sha256(pickup_password.encode()).digest()).decode(), + 'original_name': filename, + 'timestamp': datetime.datetime.now(UTC).isoformat() + } + with open(os.path.join(UPLOAD_FOLDER, f"{random_id}.json"), 'w') as f: + json.dump(meta, f) + + pickup_url = request.host_url.rstrip('/') + url_for('pickup_file', file_id=random_id) + return jsonify({"success": True, "pickup_url": pickup_url}) + +def handle_text_operation(request): + """Process text encryption/decryption operations.""" + data = request.get_json() + encryption_type = data.get("encryption-type", "basic") + operation = data.get("operation", "") + message = data.get("message", "") + password = data.get("password", "") + + if encryption_type == "basic": + result = simple_encode(message) if operation == "encrypt" else simple_decode(message) + else: + result = advanced_encrypt(message, password) if operation == "encrypt" else advanced_decrypt(message, password) + + return jsonify(result=html.escape(result)) + +# ===== File Pickup Route ===== @app.route("/pickup/", methods=["GET", "POST"]) def pickup_file(file_id): + """Handle file pickup and decryption.""" meta_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.json") enc_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.enc") @@ -195,59 +232,61 @@ def pickup_file(file_id): return redirect(url_for('index')) if request.method == 'POST': - pickup_password = request.form.get('pickup_password') - enc_password = request.form.get('enc_password') - - if not pickup_password or not enc_password: - flash("Missing fields") - return redirect(request.url) - - with open(meta_path, 'r') as f: - meta = json.load(f) - - expected_hash = base64.urlsafe_b64encode(hashlib.sha256(pickup_password.encode()).digest()).decode() - if expected_hash != meta['pickup_password']: - flash("Incorrect pickup password") - return redirect(request.url) - - with open(enc_path, 'rb') as f: - enc_data = f.read() - salt, nonce, ct = enc_data[:16], enc_data[16:28], enc_data[28:] - key = derive_key(enc_password, salt) - - try: - decrypted = AESGCM(key).decrypt(nonce, ct, None) - except Exception: - flash("Decryption failed") - return redirect(request.url) - - os.remove(meta_path) - os.remove(enc_path) - log_admin_event(f"File {file_id} downloaded and deleted.") - - return send_file(io.BytesIO(decrypted), as_attachment=True, download_name=meta['original_name']) - + return handle_file_pickup(request, meta_path, enc_path, file_id) return render_template("pickup.html", file_id=file_id) -def cleanup_expired_files(): - now = datetime.datetime.utcnow() +def handle_file_pickup(request, meta_path, enc_path, file_id): + """Process file pickup and decryption.""" + pickup_password = request.form.get('pickup_password') + enc_password = request.form.get('enc_password') - 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.utcfromtimestamp(os.path.getmtime(path)) - age = (now - file_time).days - if age > MAX_FILE_AGE_DAYS: - os.remove(path) - print(f"[INFO] Deleted expired file: {fname}") - except Exception as e: - print(f"[ERROR] Could not check/delete file {fname}: {e}") + if not pickup_password or not enc_password: + flash("Missing fields") + return redirect(request.url) + with open(meta_path, 'r') as f: + meta = json.load(f) -# === Admin Log Viewer === + expected_hash = base64.urlsafe_b64encode(hashlib.sha256(pickup_password.encode()).digest()).decode() + if expected_hash != meta['pickup_password']: + flash("Incorrect pickup password") + return redirect(request.url) + + with open(enc_path, 'rb') as f: + enc_data = f.read() + salt, nonce, ct = enc_data[:16], enc_data[16:28], enc_data[28:] + key = derive_key(enc_password, salt) + + try: + decrypted = AESGCM(key).decrypt(nonce, ct, None) + except Exception: + flash("Decryption failed") + return redirect(request.url) + + os.remove(meta_path) + os.remove(enc_path) + log_admin_event(f"File {file_id} downloaded and deleted.") + + response = send_file( + io.BytesIO(decrypted), + as_attachment=True, + download_name=meta['original_name'], + mimetype='application/octet-stream' + ) + + # Add headers for better mobile compatibility + response.headers['Content-Disposition'] = f'attachment; filename="{meta["original_name"]}"' + response.headers['Content-Type'] = 'application/octet-stream' + response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' + response.headers['Pragma'] = 'no-cache' + response.headers['Expires'] = '0' + + return response + +# ===== Admin Routes ===== @app.route("/admin-logs") def admin_logs(): + """View admin activity logs.""" if not session.get("admin_logged_in"): return redirect(url_for("admin_login")) @@ -270,47 +309,50 @@ def admin_logs(): return jsonify(logs=logs) -# === Admin Settings Editor === @app.route("/admin-settings", methods=["GET", "POST"]) def admin_settings(): + """Manage application settings.""" if not session.get("admin_logged_in"): return redirect(url_for("admin_login")) current_settings = load_settings() if request.method == 'POST': - upload_folder = request.form.get('upload_folder', current_settings.get('upload_folder', 'uploads')) - max_file_age_days = int(request.form.get('max_file_age_days', current_settings.get('max_file_age_days', 14))) - max_file_size_gb = float(request.form.get('max_file_size_gb', current_settings.get('max_file_size_bytes', 25 * 1024 * 1024 * 1024) / (1024 * 1024 * 1024))) - max_file_size_bytes = int(max_file_size_gb * 1024 * 1024 * 1024) - - updated_settings = { - "upload_folder": upload_folder, - "max_file_age_days": max_file_age_days, - "max_file_size_bytes": max_file_size_bytes - } - - with open(SETTINGS_FILE, 'w') as f: - json.dump(updated_settings, f) - - flash("Settings updated successfully!") - - global settings, UPLOAD_FOLDER, MAX_FILE_AGE_DAYS, MAX_FILE_SIZE_BYTES - settings = load_settings() - UPLOAD_FOLDER = settings.get('upload_folder', 'uploads') - MAX_FILE_AGE_DAYS = settings.get('max_file_age_days', 14) - MAX_FILE_SIZE_BYTES = settings.get('max_file_size_bytes', 25 * 1024 * 1024 * 1024) - - if not os.path.exists(UPLOAD_FOLDER): - os.makedirs(UPLOAD_FOLDER) - - return redirect(url_for("admin_settings")) - + return handle_settings_update(request, current_settings) return render_template("admin_settings.html", settings=current_settings) -# === Admin Setup === +def handle_settings_update(request, current_settings): + """Process settings update request.""" + upload_folder = request.form.get('upload_folder', current_settings.get('upload_folder', 'uploads')) + max_file_age_days = int(request.form.get('max_file_age_days', current_settings.get('max_file_age_days', 14))) + max_file_size_gb = float(request.form.get('max_file_size_gb', current_settings.get('max_file_size_bytes', 25 * 1024 * 1024 * 1024) / (1024 * 1024 * 1024))) + max_file_size_bytes = int(max_file_size_gb * 1024 * 1024 * 1024) + + updated_settings = { + "upload_folder": upload_folder, + "max_file_age_days": max_file_age_days, + "max_file_size_bytes": max_file_size_bytes + } + + with open(SETTINGS_FILE, 'w') as f: + json.dump(updated_settings, f) + + flash("Settings updated successfully!") + + global settings, UPLOAD_FOLDER, MAX_FILE_AGE_DAYS, MAX_FILE_SIZE_BYTES + settings = load_settings() + UPLOAD_FOLDER = settings.get('upload_folder', 'uploads') + MAX_FILE_AGE_DAYS = settings.get('max_file_age_days', 14) + MAX_FILE_SIZE_BYTES = settings.get('max_file_size_bytes', 25 * 1024 * 1024 * 1024) + + if not os.path.exists(UPLOAD_FOLDER): + os.makedirs(UPLOAD_FOLDER) + + return redirect(url_for("admin_settings")) + @app.route("/admin-setup", methods=["GET", "POST"]) def admin_setup(): + """Initial admin account setup.""" if os.path.exists(ADMIN_CRED_FILE): return redirect(url_for("admin_login")) if request.method == "POST": @@ -323,9 +365,9 @@ def admin_setup(): flash("Both fields required") return render_template("admin_setup.html") -# === Admin Login === @app.route("/admin-login", methods=["GET", "POST"]) def admin_login(): + """Admin login handler.""" if request.method == "POST": u = request.form.get("username") p = request.form.get("password") @@ -338,15 +380,15 @@ def admin_login(): flash("Incorrect credentials") return render_template("admin_login.html") -# === Admin Logout === @app.route("/admin-logout") def admin_logout(): + """Admin logout handler.""" session.pop("admin_logged_in", None) return redirect(url_for("index")) -# === Admin Page === @app.route("/adminpage") def admin_page(): + """Admin dashboard.""" if not session.get("admin_logged_in"): if not os.path.exists(ADMIN_CRED_FILE): return redirect(url_for("admin_setup")) @@ -355,32 +397,88 @@ def admin_page(): cleanup_expired_files() routes = [rule.rule for rule in app.url_map.iter_rules() if rule.endpoint != 'static'] - try: - uptime = subprocess.check_output("uptime -p", shell=True).decode().strip() - except Exception: - uptime = "Unavailable" + # 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: + # Linux uptime using uptime command + 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" server_info = { - "uptime": uptime, - "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "uptime": uptime_str, + "time": datetime.datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S"), "python": platform.python_version(), "debug": app.debug } return render_template("admin.html", routes=routes, server_info=server_info) -# === Restart Server === -@app.route("/restart-server") +@app.route("/restart-server", methods=["POST"]) def restart_server(): + """Restart the server.""" if not session.get("admin_logged_in"): - return redirect(url_for("admin_login")) - subprocess.Popen(["sudo", "systemctl", "restart", "paccrypt.service"]) - flash("Restart triggered") - return redirect(url_for("admin_page")) + return jsonify({"error": "Unauthorized"}), 401 + + 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 + start "" "python" "app.py" + """ + 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 + python_path = sys.executable + script_path = os.path.abspath(__file__) + + # Create a shell script to restart the server + restart_script = f"""#!/bin/bash + sleep 2 + {python_path} {script_path} + """ + + # 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) + 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 -# === Reset Admin Credentials === @app.route("/admin-reset", methods=["POST"]) def admin_reset(): + """Reset admin credentials.""" if not session.get("admin_logged_in"): return redirect(url_for("admin_login")) try: @@ -395,9 +493,9 @@ def admin_reset(): print("[ERROR] admin_reset failed:", e) return redirect(url_for("admin_setup")) -# === Change Admin Password === @app.route("/admin-change-password", methods=["POST"]) def admin_change_password(): + """Change admin password.""" if not session.get("admin_logged_in"): return redirect(url_for("admin_login")) @@ -432,6 +530,7 @@ def admin_change_password(): @app.route("/admin-clear-uploads", methods=["POST"]) def admin_clear_uploads(): + """Clear all uploaded files.""" if not session.get("admin_logged_in"): return redirect(url_for("admin_login")) @@ -449,31 +548,34 @@ def admin_clear_uploads(): @app.route("/admin-update-server", methods=["POST"]) def admin_update_server(): + """Update server from GitHub repository.""" if not session.get("admin_logged_in"): - return redirect(url_for("admin_login")) + return jsonify({"error": "Unauthorized"}), 401 try: - repo_dir = os.path.abspath(os.path.dirname(__file__)) # Dynamically get current project directory - repo_url = "https://github.com/TySP-Dev/PacCrypt.git" - - # Ensure directory is a git repo + repo_dir = os.path.abspath(os.path.dirname(__file__)) + + # Check if we're in a git repository if not os.path.exists(os.path.join(repo_dir, ".git")): - return redirect(url_for('500.html')) + return jsonify({"error": "Not a git repository"}), 400 - # Pull latest changes + # Execute git commands subprocess.run(["git", "fetch"], cwd=repo_dir, check=True) subprocess.run(["git", "reset", "--hard", "origin/main"], cwd=repo_dir, check=True) - subprocess.run(["git", "pull", repo_url, "main"], cwd=repo_dir, check=True) + subprocess.run(["git", "pull"], cwd=repo_dir, check=True) - flash("Server updated from GitHub!", "clear-feedback") + return jsonify({"message": "Server updated successfully from GitHub!"}), 200 + except subprocess.CalledProcessError as e: + print(f"[ERROR] Git operation failed: {str(e)}") + return jsonify({"error": f"Git operation failed: {str(e)}"}), 500 except Exception as e: - flash(f"Update failed: {str(e)}", "clear-feedback") - - return redirect(url_for("admin_page")) - + print(f"[ERROR] Update failed: {str(e)}") + return jsonify({"error": f"Update failed: {str(e)}"}), 500 +# ===== Sitemap and Robots ===== @app.route("/sitemap", methods=["GET"]) def sitemap(): + """Generate sitemap.xml.""" sitemap_xml = ''' https://paccrypt.unnaturalll.dev/ @@ -483,9 +585,9 @@ def sitemap(): ''' return sitemap_xml, 200, {'Content-Type': 'application/xml'} - @app.route("/robots.txt") def robots_txt(): + """Generate robots.txt.""" lines = [ "User-agent: *", "Disallow: /adminpage", @@ -501,10 +603,7 @@ def robots_txt(): ] return "\n".join(lines), 200, {"Content-Type": "text/plain"} - - - -# === Error Handlers === +# ===== Error Handlers ===== @app.errorhandler(404) def page_not_found(e): return render_template('404.html'), 404 @@ -526,10 +625,9 @@ def handle_file_not_found(e): if os.getenv("PRODUCTION", "false").lower() == "true": return render_template('500.html'), 500 else: - raise e # re-raise for debugging in development + raise e - -# === Server Mode Execution === +# ===== Application Entry Point ===== if __name__ == "__main__": PRODUCTION = os.getenv("PRODUCTION", "false").lower() == "true" if PRODUCTION: diff --git a/requirements.txt b/requirements.txt index f1d587b..466165a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ flask==3.0.3 cryptography==42.0.5 waitress==2.1.2 +werkzeug==3.0.1 # nginx - Only needed for Nginx integration, not installed via pip # Run pip install -r requirements.txt \ No newline at end of file diff --git a/restart.bat b/restart.bat new file mode 100644 index 0000000..a684cbe --- /dev/null +++ b/restart.bat @@ -0,0 +1,5 @@ + + @echo off + timeout /t 2 /nobreak + start "" "python" "app.py" + \ No newline at end of file diff --git a/static/css/styles.css b/static/css/styles.css index 8ce1e2e..dea835f 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -1,7 +1,7 @@ /* ===== Global Reset ===== */ * { box-sizing: border-box; - margin: 0; + margin: 3px; padding: 0; } @@ -27,7 +27,7 @@ header { box-shadow: 0 0 15px rgba(0, 255, 153, 0.4); width: 100%; max-width: 800px; - margin-bottom: 30px; + margin-bottom: 40px !important; } header h1 { @@ -48,8 +48,8 @@ main { align-items: center; width: 100%; max-width: 800px; - padding: 20px; - gap: 30px; + padding: 0; + gap: 0; } /* ===== Card Styling ===== */ @@ -68,6 +68,7 @@ main { flex-direction: column; align-items: center; gap: 0px; + max-width: 725px; width: 100%; } @@ -79,14 +80,17 @@ input[type="file"] { width: 80%; max-width: 500px; padding: 12px 20px; - border: 2px solid #00ff99; + border: 1px solid #00ff99; border-radius: 8px; background-color: #2c2f33; color: #00ff99; font-size: 1em; - text-align: center; + text-align: left; transition: 0.3s; - margin:10px auto; +} + +select { + text-align: center; } textarea { @@ -122,7 +126,7 @@ input:focus, textarea:focus, select:focus { outline: none; - box-shadow: 0 0 8px rgba(0, 255, 153, 0.8); + box-shadow: 0 0 10px rgba(0, 255, 153, 0.8); } /* ===== Textareas Specific Widths ===== */ @@ -136,10 +140,9 @@ select:focus { /* ===== Button Group Styling ===== */ .button-group { display: flex; - flex-wrap: wrap; + flex-wrap: nowrap; justify-content: center; gap: 15px; - margin: 10px auto; width: 100%; } @@ -152,23 +155,34 @@ button { font-size: 1em; cursor: pointer; transition: 0.3s; - width: 100%; - max-width: 200px; - /* margin: 10px auto; */ + width: auto; + min-width: 225px; + max-width: 300px; } button:hover { background-color: #00ff99; color: #121212; + box-shadow: 0 0 10px rgba(0, 255, 153, 0.4); } +.danger-button { + background-color: #5f3131; + box-shadow: 0 0 20px rgba(185, 0, 0, 0.4); +} + +.danger-button:hover { + background-color: #ff0000; + color: #121212; + box-shadow: 0 0 40px rgb(255, 0, 0); +} + /* ===== Toggle Switch Styling ===== */ .toggle-container { display: flex; align-items: center; justify-content: center; gap: 12px; - margin-top: 10px; width: 100%; } @@ -207,15 +221,15 @@ button { /* The circle knob */ .slider::before { content: ""; - height: 26px; - width: 26px; + height: 22px; + width: 22px; background-color: #00ff99; border-radius: 50%; transition: .4s; - transform: translateX(4px); + transform: translateX(2px); position: absolute; - left: 0px; - bottom: 2.5px; + left: auto; + bottom: auto; } input:checked + .slider::before { @@ -295,9 +309,9 @@ footer { color: #00ff99; border-radius: 12px; box-shadow: 0 0 15px rgba(0, 255, 153, 0.4); - margin-top: 30px; width: 100%; max-width: 800px; + margin-top: 40px; } footer a { @@ -309,39 +323,70 @@ footer { color: #ff0066; } -/* ===== Responsive Tweaks ===== */ +/* ===== Responsive Design ===== */ @media (max-width: 600px) { - input, textarea, select, #input-text, #output-text { + input, + textarea, + select, + #input-text, + #output-text { width: 100%; max-width: 90%; } } /* ===== Copy Feedback Message ===== */ -.copy-feedback { - background-color: #2a2a2a; - border: 1px solid #00ff99; +.copy-feedback, #shared-link-feedback { + background-color: #2c2f33; padding: 6px 12px; margin-top: 6px; border-radius: 6px; color: #00ff99; font-size: 0.9em; + display: none; opacity: 0; - transition: opacity 0.3s ease; text-align: center; max-width: 300px; margin-left: auto; margin-right: auto; + transition: opacity 0.3s ease; } - .copy-feedback.show { - opacity: 1; - } - -.hidden { - display: none !important; +.copy-feedback.show, #shared-link-feedback.show { + display: block; + opacity: 1; } +.share-link-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + margin-top: 12px; + margin-bottom: 12px; +} + +#share-link { + display: block; + background-color: #2c2f33; + padding: 8px 16px; + border-radius: 6px; + color: #00ff99; + font-size: 0.9em; + text-align: center; + max-width: 720px; + width: 100%; + word-break: break-all; + text-decoration: none; + transition: all 0.3s ease; +} + +#share-link:hover { + color: #00cc77; + background-color: #36393f; +} + +/* ===== Form Styling ===== */ form { width: 100%; display: flex; @@ -349,65 +394,39 @@ form { align-items: center; } - form input, - form button { - width: 80%; - max-width: 500px; - margin-bottom: 12px; - text-align: center; - } +form input { + width: 80%; + max-width: 500px; + text-align: left; +} +/* ===== Section Card Styling ===== */ section.card { display: flex; flex-direction: column; align-items: center; } -.copy-feedback.show { - display: block; - width: fit-content; - margin-top: 10px; - padding: 6px 12px; - background-color: #2a2a2a; - color: #00ff99; - border: 1px solid #00ff99; - border-radius: 6px; -} - -#logContainer { - white-space: pre-wrap; /* Wrap long lines */ - word-wrap: break-word; /* Break long words if needed */ - overflow-wrap: anywhere; /* Ensures long strings don't overflow */ - background: black; - color: lime; - padding: 10px; - border-radius: 8px; - max-height: 400px; - overflow-y: auto; - width: 100%; - box-sizing: border-box; -} - +/* ===== Pacman Game Styling ===== */ #pacmanCanvas { background-color: black; display: block; - margin: auto; border: 2px solid #00ff99; border-radius: 12px; - align-items: center; - justify-content: center; + max-width: 700px; + width: 100%; + aspect-ratio: 4/3; + object-fit: contain; } + #pacman-section { display: flex; flex-direction: column; align-items: center; - justify-content: center; gap: 20px; - padding: 20px; - display: block; - margin: auto; - border: 2px solid #00ff99; - border-radius: 12px; + margin-bottom: 25px; + max-width: 725px; + width: 100%; } .pacman-wrapper { @@ -415,10 +434,197 @@ section.card { justify-content: center; align-items: center; width: 100%; + padding: 0; + margin: 0; } - -/* ===== Utility: Hidden Class ===== */ +/* ===== Utility Classes ===== */ .hidden { display: none !important; } + +/* ===== Section Spacing ===== */ +#password-generator-section { + margin-bottom: 25px; +} + +#encoding-section { + margin-bottom: 25px; +} + +/* ===== File Input Section ===== */ +#encoding-section #file-section { + display: none; +} + +#encoding-section #file-section:not(.hidden) { + display: flex; +} + +/* Ensure PacCrypt sharing file uploader is always visible */ +#sharing-section #file-section { + display: flex; +} + +/* Mobile-friendly download button */ +.download-btn { + width: 100%; + padding: 12px; + font-size: 16px; + cursor: pointer; + background-color: var(--primary-color); + color: white; + border: none; + border-radius: 4px; + transition: background-color 0.3s; +} + +.download-btn:hover { + background-color: var(--primary-hover); +} + +/* Mobile form adjustments */ +.pickup-form { + max-width: 100%; + margin: 0 auto; +} + +.pickup-form input[type="password"] { + width: 100%; + padding: 12px; + margin-bottom: 10px; + font-size: 16px; + border: 1px solid var(--border-color); + border-radius: 4px; + background-color: var(--input-bg); + color: var(--text-color); +} + +/* Mobile-specific styles */ +@media (max-width: 768px) { + .download-btn { + padding: 15px; + font-size: 18px; + } + + .pickup-form input[type="password"] { + padding: 15px; + font-size: 18px; + } +} + +/* ===== Admin Section Styling ===== */ +#sitemap-section, +#password-change-section, +#server-update-section, +#server-status-section, +#server-logs-section, +#system-settings-section { + margin-bottom: 25px; + padding: 25px; + background-color: #1e1e1e; + border-radius: 12px; + box-shadow: 0 0 15px rgba(0, 255, 153, 0.4); +} + +.sitemap-header { + display: flex; + justify-content: space-between; + align-items: center; + margin: 15px 0; +} + +.sitemap-header h3 { + color: #00ff99; + margin: 0; +} + +.collapse-btn { + background: none; + border: none; + color: #00ff99; + font-size: 1.2em; + cursor: pointer; + padding: 5px 10px; + transition: transform 0.3s ease; +} + +.collapse-btn:hover { + transform: scale(1.1); +} + +.sitemap-content { + transition: all 0.3s ease; + margin-bottom: 15px; +} + +#sitemap-section ul, +#server-status-section ul { + list-style: none; + padding-left: 0; + margin-top: 15px; +} + +#sitemap-section li, +#server-status-section li { + margin-bottom: 10px; + padding: 8px; + background-color: #2c2f33; + border-radius: 6px; + color: #00ff99; +} + +#server-logs-section button { + margin-bottom: 15px; + width: 100%; + max-width: 300px; +} + +#logLoader { + color: #00ff99; + text-align: center; + padding: 10px; +} + +#logContainer { + background-color: #2c2f33; + color: #00ff99; + padding: 15px; + border-radius: 8px; + max-height: 400px; + overflow-y: auto; + font-family: monospace; + white-space: pre-wrap; +} + +#system-settings-section { + margin-bottom: unset !important; + padding: 25px; + background-color: #1e1e1e; + border-radius: 12px; + box-shadow: 0 0 15px rgba(0, 255, 153, 0.4); +} + +/* ===== 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; + } +} diff --git a/static/js/encryption.js b/static/js/encryption.js index 73d088c..1b9835e 100644 --- a/static/js/encryption.js +++ b/static/js/encryption.js @@ -1,10 +1,21 @@ -// encryption.js +/** + * Encryption module. + * Handles cryptographic operations using Web Crypto API. + * Implements AES-GCM encryption with PBKDF2 key derivation. + */ +// ===== Constants ===== +const SALT_LENGTH = 16; +const IV_LENGTH = 12; +const PBKDF2_ITERATIONS = 200_000; +const KEY_LENGTH = 256; + +// ===== Key Derivation ===== /** * Derives an AES-GCM key from a password using PBKDF2. * @param {string} password - User-supplied password. * @param {Uint8Array} salt - Randomly generated salt. - * @returns {Promise} + * @returns {Promise} - Derived cryptographic key. */ export async function deriveKey(password, salt) { const encoder = new TextEncoder(); @@ -20,16 +31,17 @@ export async function deriveKey(password, salt) { { name: 'PBKDF2', salt, - iterations: 200_000, + iterations: PBKDF2_ITERATIONS, hash: 'SHA-256' }, keyMaterial, - { name: 'AES-GCM', length: 256 }, + { name: 'AES-GCM', length: KEY_LENGTH }, false, ['encrypt', 'decrypt'] ); } +// ===== Encryption ===== /** * Encrypts a message using AES-GCM with a derived key. * @param {string} message - Plaintext message to encrypt. @@ -38,12 +50,16 @@ export async function deriveKey(password, salt) { */ export async function encryptAdvanced(message, password) { const encoder = new TextEncoder(); - const salt = crypto.getRandomValues(new Uint8Array(16)); - const iv = crypto.getRandomValues(new Uint8Array(12)); + const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH)); + const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)); const key = await deriveKey(password, salt); const encoded = encoder.encode(message); - const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded); + const ciphertext = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv }, + key, + encoded + ); const output = new Uint8Array(salt.length + iv.length + ciphertext.byteLength); output.set(salt); @@ -53,6 +69,7 @@ export async function encryptAdvanced(message, password) { return btoa(String.fromCharCode(...output)); } +// ===== Decryption ===== /** * Decrypts an AES-GCM encrypted string. * @param {string} encryptedData - Base64-encoded ciphertext. @@ -64,9 +81,9 @@ export async function decryptAdvanced(encryptedData, password) { atob(encryptedData).split('').map(c => c.charCodeAt(0)) ); - const salt = encrypted.slice(0, 16); - const iv = encrypted.slice(16, 28); - const ciphertext = encrypted.slice(28); + const salt = encrypted.slice(0, SALT_LENGTH); + const iv = encrypted.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH); + const ciphertext = encrypted.slice(SALT_LENGTH + IV_LENGTH); const key = await deriveKey(password, salt); const decrypted = await crypto.subtle.decrypt( @@ -78,8 +95,9 @@ export async function decryptAdvanced(encryptedData, password) { return new TextDecoder().decode(decrypted); } +// ===== Module Initialization ===== /** - * Optional init logging for module diagnostics. + * Initializes the encryption module and logs its status. */ export function setupEncryption() { console.log('[Encryption] Module loaded'); diff --git a/static/js/fileops.js b/static/js/fileops.js index c90c6b9..4047dd1 100644 --- a/static/js/fileops.js +++ b/static/js/fileops.js @@ -1,92 +1,119 @@ -// fileops.js - -import { encryptAdvanced, decryptAdvanced } from './encryption.js'; - /** - * Encrypts the selected file and triggers download of the encrypted version. - * @param {HTMLInputElement} fileInput - The input element of type 'file'. - * @param {string} password - Password for encryption. + * File operations module. + * Handles file encryption and decryption operations. */ -export function encryptFile(fileInput, password) { - if (!fileInput.files.length) { - alert("Please select a file!"); - return; - } +// ===== Constants ===== +const CHUNK_SIZE = 1024 * 1024; // 1MB chunks + +// ===== Public Interface ===== +export async function encryptFile(fileInput, password) { const file = fileInput.files[0]; - const reader = new FileReader(); + if (!file) return; - reader.onload = async (e) => { - const rawBytes = new Uint8Array(e.target.result); - const base64 = btoa(String.fromCharCode(...rawBytes)); - const encrypted = await encryptAdvanced(base64, password); - downloadFile(encrypted, file.name + ".enc"); - }; - - reader.readAsArrayBuffer(file); + try { + const encryptedChunks = await processFile(file, password, true); + downloadEncryptedFile(encryptedChunks, file.name); + } catch (error) { + alert("Error encrypting file: " + error.message); + } } -/** - * Decrypts the selected encrypted file and triggers download of the original. - * @param {HTMLInputElement} fileInput - The input element of type 'file'. - * @param {string} password - Password for decryption. - */ -export function decryptFile(fileInput, password) { - if (!fileInput.files.length) { - alert("Please select a file!"); - return; +export async function decryptFile(fileInput, password) { + const file = fileInput.files[0]; + if (!file) return; + + try { + const decryptedChunks = await processFile(file, password, false); + downloadDecryptedFile(decryptedChunks, file.name); + } catch (error) { + alert("Error decrypting file: " + error.message); + } +} + +// ===== File Processing ===== +async function processFile(file, password, isEncrypt) { + const chunks = []; + const totalChunks = Math.ceil(file.size / CHUNK_SIZE); + let processedChunks = 0; + + for (let start = 0; start < file.size; start += CHUNK_SIZE) { + const chunk = file.slice(start, start + CHUNK_SIZE); + const arrayBuffer = await chunk.arrayBuffer(); + const uint8Array = new Uint8Array(arrayBuffer); + + const processedChunk = await processChunk(uint8Array, password, isEncrypt); + chunks.push(processedChunk); + + processedChunks++; + updateProgress(processedChunks, totalChunks); } - const file = fileInput.files[0]; - const reader = new FileReader(); + return chunks; +} - reader.onload = async (e) => { - try { - const encryptedText = e.target.result; - const base64Decrypted = await decryptAdvanced(encryptedText, password); - const byteArray = new Uint8Array( - [...atob(base64Decrypted)].map(c => c.charCodeAt(0)) - ); - downloadFileBinary(byteArray, file.name.replace(/\.enc$/, '')); - } catch (err) { - console.error("[Decryption Error]", err); - alert("Decryption failed: wrong password or corrupted file."); +async function processChunk(data, password, isEncrypt) { + const payload = { + "encryption-type": "advanced", + operation: isEncrypt ? "encrypt" : "decrypt", + message: Array.from(data).join(','), + password: password + }; + + const response = await fetch("/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + return new Uint8Array(result.result.split(',').map(Number)); +} + +// ===== File Download ===== +function downloadEncryptedFile(chunks, originalName) { + const blob = new Blob(chunks, { type: 'application/octet-stream' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = originalName + '.encrypted'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +function downloadDecryptedFile(chunks, originalName) { + const blob = new Blob(chunks, { type: 'application/octet-stream' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = originalName.replace('.encrypted', ''); + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +// ===== Progress Tracking ===== +function updateProgress(processed, total) { + const progressBar = document.getElementById("file-progress"); + const progressText = document.getElementById("file-progress-text"); + + if (progressBar && progressText) { + const percent = Math.round((processed / total) * 100); + progressBar.style.width = percent + "%"; + progressText.textContent = `Processing: ${percent}%`; + + if (processed === total) { + setTimeout(() => { + progressBar.style.width = "0%"; + progressText.textContent = ""; + }, 1000); } - }; - - reader.readAsText(file); -} - -/** - * Downloads a text-based file (encrypted string). - * @param {string} content - The file content to download. - * @param {string} filename - Desired name for the downloaded file. - */ -function downloadFile(content, filename) { - const blob = new Blob([content], { type: "application/octet-stream" }); - const url = URL.createObjectURL(blob); - - const a = document.createElement("a"); - a.href = url; - a.download = filename; - a.click(); - - URL.revokeObjectURL(url); -} - -/** - * Downloads a binary file (Uint8Array). - * @param {Uint8Array} byteArray - The binary content. - * @param {string} filename - Desired name for the downloaded file. - */ -function downloadFileBinary(byteArray, filename) { - const blob = new Blob([byteArray], { type: "application/octet-stream" }); - const url = URL.createObjectURL(blob); - - const a = document.createElement("a"); - a.href = url; - a.download = filename; - a.click(); - - URL.revokeObjectURL(url); + } } diff --git a/static/js/main.js b/static/js/main.js index 4af3926..61cfb31 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1,11 +1,12 @@ -// main.js +/** + * Main application entry point. + * Initializes UI and game components when the DOM is loaded. + */ import { setupUI } from './ui.js'; import { setupGame } from './pacman.js'; -/** - * Initialize UI and game once the DOM is fully loaded. - */ +// Initialize application when DOM is fully loaded window.addEventListener("DOMContentLoaded", () => { setupUI(); setupGame(); diff --git a/static/js/pacman.js b/static/js/pacman.js index 42f8b72..9a25542 100644 --- a/static/js/pacman.js +++ b/static/js/pacman.js @@ -1,47 +1,28 @@ -// pacman.js +/** + * Pacman game module. + * Handles game logic, rendering, and user interaction. + */ +// ===== Game Constants ===== +const PACMAN_SPEED = 40; +const ENEMY_SPEED = 20; +const CELL_SIZE = 40; +const DOT_SIZE = 5; + +// ===== Game State ===== +let canvas, ctx, pacman, enemy, walls, dots, score; +let cols, rows, randSeed, gameInterval; + +// ===== Public Interface ===== export function setupGame() { console.log('[PacMan] Game module loaded.'); - window.startPacman = startPacman; window.exitGame = exitGame; } -// ====== Game Constants & State ====== -let canvas, ctx, pacman, enemy, walls, dots, score; -let pacmanSpeed = 40, - enemySpeed = 20, - cellSize = 40, - dotSize = 5, - cols, rows, randSeed, gameInterval; - -// ====== Game Initialization ====== - export function startPacman() { - canvas = document.getElementById("pacmanCanvas"); - ctx = canvas.getContext("2d"); - - cols = Math.floor(canvas.width / cellSize); - rows = Math.floor(canvas.height / cellSize); - walls = []; - dots = []; - score = 0; - - clearInterval(gameInterval); - const seedSource = document.getElementById("password")?.value || "pacman"; - randSeed = [...seedSource].reduce((s, c) => s + c.charCodeAt(0), 0); - - - generateWalls(); - generateDots(); - - pacman = spawn(); - do { enemy = spawn(); } while (enemy.x === pacman.x && enemy.y === pacman.y); - - pacman.dx = pacman.dy = 0; - document.addEventListener("keydown", movePacman); - - gameInterval = setInterval(gameLoop, 150); + initializeGame(); + setupGameLoop(); } export function stopPacman() { @@ -61,8 +42,39 @@ export function exitGame() { document.getElementById("encoding-section").style.display = "block"; } -// ====== Game Setup Helpers ====== +// ===== Game Initialization ===== +function initializeGame() { + canvas = document.getElementById("pacmanCanvas"); + ctx = canvas.getContext("2d"); + cols = Math.floor(canvas.width / CELL_SIZE); + rows = Math.floor(canvas.height / CELL_SIZE); + walls = []; + dots = []; + score = 0; + + clearInterval(gameInterval); + + // Get seed from generated password or use default + const passwordField = document.getElementById("generated-password"); + const seedSource = passwordField?.value || "pacman"; + randSeed = [...seedSource].reduce((s, c) => s + c.charCodeAt(0), 0); + + generateWalls(); + generateDots(); + + pacman = spawn(); + do { enemy = spawn(); } while (enemy.x === pacman.x && enemy.y === pacman.y); + + pacman.dx = pacman.dy = 0; + document.addEventListener("keydown", movePacman); +} + +function setupGameLoop() { + gameInterval = setInterval(gameLoop, 150); +} + +// ===== Game Setup Helpers ===== function spawn() { const options = []; for (let c = 1; c < cols - 1; c++) { @@ -80,9 +92,9 @@ function spawn() { } const s = options[Math.floor(rand() * options.length)]; return { - x: s.c * cellSize + cellSize / 2, - y: s.r * cellSize + cellSize / 2, - size: cellSize / 2 - 5, + x: s.c * CELL_SIZE + CELL_SIZE / 2, + y: s.r * CELL_SIZE + CELL_SIZE / 2, + size: CELL_SIZE / 2 - 5, dx: 0, dy: 0 }; @@ -94,6 +106,7 @@ function rand() { } function generateWalls() { + // First pass: generate initial walls for (let c = 0; c < cols; c++) { for (let r = 0; r < rows; r++) { if (c === 0 || r === 0 || c === cols - 1 || r === rows - 1 || rand() < 0.2) { @@ -101,6 +114,25 @@ function generateWalls() { } } } + + // Second pass: check for enclosed spaces + for (let c = 1; c < cols - 1; c++) { + for (let r = 1; r < rows - 1; r++) { + // Skip if already a wall + if (walls.some(w => w.c === c && w.r === r)) continue; + + // Check all four sides + const hasWallAbove = walls.some(w => w.c === c && w.r === r - 1); + const hasWallBelow = walls.some(w => w.c === c && w.r === r + 1); + const hasWallLeft = walls.some(w => w.c === c - 1 && w.r === r); + const hasWallRight = walls.some(w => w.c === c + 1 && w.r === r); + + // If all sides are walls, make this spot a wall too + if (hasWallAbove && hasWallBelow && hasWallLeft && hasWallRight) { + walls.push({ c, r }); + } + } + } } function generateDots() { @@ -120,8 +152,7 @@ function generateDots() { } } -// ====== Game Loop & Drawing ====== - +// ===== Game Loop & Rendering ===== function gameLoop() { ctx.clearRect(0, 0, canvas.width, canvas.height); drawWalls(); @@ -137,7 +168,7 @@ function gameLoop() { function drawWalls() { ctx.fillStyle = "blue"; walls.forEach(w => { - ctx.fillRect(w.c * cellSize, w.r * cellSize, cellSize, cellSize); + ctx.fillRect(w.c * CELL_SIZE, w.r * CELL_SIZE, CELL_SIZE, CELL_SIZE); }); } @@ -151,7 +182,10 @@ function drawChar(ch, color) { function drawScore() { ctx.fillStyle = "white"; ctx.font = "20px Poppins"; - ctx.fillText("Score: " + score, 10, 25); + ctx.textAlign = "left"; + // Add padding to prevent clipping + const padding = 10; + ctx.fillText("Score: " + score, padding, 25); } function checkGameOver() { @@ -167,17 +201,16 @@ function checkGameOver() { } } -// ====== Movement Logic ====== - +// ===== Movement Logic ===== function movePacman(e) { const k = e.key; if (!["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(k)) return; e.preventDefault(); - if (k === "ArrowUp") { pacman.dx = 0; pacman.dy = -pacmanSpeed; } - if (k === "ArrowDown") { pacman.dx = 0; pacman.dy = pacmanSpeed; } - if (k === "ArrowLeft") { pacman.dx = -pacmanSpeed; pacman.dy = 0; } - if (k === "ArrowRight") { pacman.dx = pacmanSpeed; pacman.dy = 0; } + if (k === "ArrowUp") { pacman.dx = 0; pacman.dy = -PACMAN_SPEED; } + if (k === "ArrowDown") { pacman.dx = 0; pacman.dy = PACMAN_SPEED; } + if (k === "ArrowLeft") { pacman.dx = -PACMAN_SPEED; pacman.dy = 0; } + if (k === "ArrowRight") { pacman.dx = PACMAN_SPEED; pacman.dy = 0; } } function moveChar(ch) { @@ -191,7 +224,7 @@ function moveChar(ch) { function moveEnemy() { const options = []; - const moves = [[enemySpeed, 0], [-enemySpeed, 0], [0, enemySpeed], [0, -enemySpeed]]; + const moves = [[ENEMY_SPEED, 0], [-ENEMY_SPEED, 0], [0, ENEMY_SPEED], [0, -ENEMY_SPEED]]; moves.forEach(([dx, dy]) => { const nx = enemy.x + dx; @@ -225,8 +258,8 @@ function willCollide(x, y, size) { const top = y - size, bottom = y + size; return walls.some(w => { - const wx1 = w.c * cellSize, wy1 = w.r * cellSize; - const wx2 = wx1 + cellSize, wy2 = wy1 + cellSize; + const wx1 = w.c * CELL_SIZE, wy1 = w.r * CELL_SIZE; + const wx2 = wx1 + CELL_SIZE, wy2 = wy1 + CELL_SIZE; return right > wx1 && left < wx2 && bottom > wy1 && top < wy2; }); } @@ -235,8 +268,8 @@ function eatDots() { const chompSound = document.getElementById("chomp-sound"); dots = dots.filter(d => { - const dx = d.c * cellSize + cellSize / 2; - const dy = d.r * cellSize + cellSize / 2; + const dx = d.c * CELL_SIZE + CELL_SIZE / 2; + const dy = d.r * CELL_SIZE + CELL_SIZE / 2; if (Math.abs(pacman.x - dx) < pacman.size && Math.abs(pacman.y - dy) < pacman.size) { score++; @@ -253,10 +286,11 @@ function eatDots() { ctx.fillStyle = "white"; dots.forEach(d => { ctx.beginPath(); - ctx.arc(d.c * cellSize + cellSize / 2, d.r * cellSize + cellSize / 2, dotSize, 0, Math.PI * 2); + ctx.arc(d.c * CELL_SIZE + CELL_SIZE / 2, d.r * CELL_SIZE + CELL_SIZE / 2, DOT_SIZE, 0, Math.PI * 2); ctx.fill(); }); } +// ===== Global Functions ===== window.resetGame = resetGame; window.exitGame = exitGame; diff --git a/static/js/ui.js b/static/js/ui.js index 7e0c2cf..b39ad6c 100644 --- a/static/js/ui.js +++ b/static/js/ui.js @@ -1,72 +1,99 @@ -ο»Ώ// ui.js +ο»Ώ/** + * UI management module. + * Handles user interface interactions and form handling. + */ + import { encryptFile, decryptFile } from './fileops.js'; -/** - * Initialize all UI functionality after DOM is loaded - */ +// ===== UI Initialization ===== export function setupUI() { - toggleEncryptionOptions(); - toggleInputMode(); + // Set initial state of remove button to hidden + const removeBtn = document.getElementById("remove-file-btn"); + if (removeBtn) { + removeBtn.style.display = "none"; + } + + initializeEventListeners(); +} - const encryptionTypeEl = document.getElementById("encryption-type"); - const inputTextEl = document.getElementById("input-text"); - const formEl = document.getElementById("crypto-form"); - const removeFileBtn = document.getElementById("remove-file-btn"); - const clearAllBtn = document.getElementById("clear-all-btn"); - const generateBtn = document.getElementById("generate-btn"); - const copyPasswordBtn = document.getElementById("copy-btn"); - const copyOutputBtn = document.getElementById("copy-output-btn"); - const toggleSwitch = document.getElementById("operation-toggle"); - const copyShareBtn = document.getElementById("copy-share-btn"); - const shareLink = document.getElementById("share-link"); +// ===== Event Listeners ===== +function initializeEventListeners() { + const elements = { + encryptionType: document.getElementById("encryption-type"), + inputText: document.getElementById("input-text"), + form: document.getElementById("crypto-form"), + removeFileBtn: document.getElementById("remove-file-btn"), + clearAllBtn: document.getElementById("clear-all-btn"), + generateBtn: document.getElementById("generate-btn"), + copyPasswordBtn: document.getElementById("copy-btn"), + copyOutputBtn: document.getElementById("copy-output-btn"), + toggleSwitch: document.getElementById("operation-toggle"), + copyShareBtn: document.getElementById("copy-share-btn"), + shareLink: document.getElementById("share-link") + }; - if ( - encryptionTypeEl && inputTextEl && formEl && removeFileBtn && - clearAllBtn && generateBtn && copyPasswordBtn && toggleSwitch - ) { - encryptionTypeEl.addEventListener("change", toggleEncryptionOptions); - inputTextEl.addEventListener("input", () => { - toggleInputMode(); - checkForPacman(); - }); - formEl.addEventListener("submit", handleSubmit); - removeFileBtn.addEventListener("click", removeFile); - clearAllBtn.addEventListener("click", clearAll); - generateBtn.addEventListener("click", generateRandomPassword); - copyPasswordBtn.addEventListener("click", () => copyToClipboard("generated-password", "password-copy-feedback")); - copyOutputBtn?.addEventListener("click", () => copyToClipboard("output-text", "output-copy-feedback")); - toggleSwitch.addEventListener("change", updateToggleLabels); - - const copySharedLinkBtn = document.getElementById("copy-shared-link"); - const sharedLinkEl = document.getElementById("shared-link"); - - if (copySharedLinkBtn && sharedLinkEl) { - copySharedLinkBtn.addEventListener("click", () => { - navigator.clipboard.writeText(sharedLinkEl.textContent.trim()).then(() => { - const feedback = document.getElementById("shared-link-feedback"); - if (feedback) { - feedback.classList.remove("hidden"); - feedback.classList.add("show"); - - setTimeout(() => { - feedback.classList.remove("show"); - feedback.classList.add("hidden"); - }, 3000); - } - }); - }); - - sharedLinkEl.scrollIntoView({ behavior: "smooth" }); - } + if (validateElements(elements)) { + setupElementListeners(elements); } } +function validateElements(elements) { + return elements.encryptionType && elements.inputText && elements.form && + elements.removeFileBtn && elements.clearAllBtn && elements.generateBtn && + elements.copyPasswordBtn && elements.toggleSwitch; +} +function setupElementListeners(elements) { + elements.encryptionType.addEventListener("change", toggleEncryptionOptions); + elements.inputText.addEventListener("input", handleInputChange); + elements.form.addEventListener("submit", handleSubmit); + elements.removeFileBtn.addEventListener("click", removeFile); + elements.clearAllBtn.addEventListener("click", clearAll); + elements.generateBtn.addEventListener("click", generateRandomPassword); + elements.copyPasswordBtn.addEventListener("click", () => copyToClipboard("generated-password", "password-copy-feedback")); + elements.copyOutputBtn?.addEventListener("click", () => copyToClipboard("output-text", "output-copy-feedback")); + elements.toggleSwitch.addEventListener("change", updateToggleLabels); + // Add file input change listener + const fileInput = document.getElementById("file-input"); + if (fileInput) { + fileInput.addEventListener("change", () => { + const removeBtn = document.getElementById("remove-file-btn"); + if (removeBtn) { + removeBtn.style.display = fileInput.files.length > 0 ? "inline-block" : "none"; + } + }); + } + setupShareLinkListeners(elements); +} + +function setupShareLinkListeners(elements) { + if (elements.copyShareBtn && elements.shareLink) { + elements.copyShareBtn.addEventListener("click", () => { + const linkText = elements.shareLink.textContent.trim(); + navigator.clipboard.writeText(linkText).then(() => { + const feedback = document.getElementById("shared-link-feedback"); + if (feedback) { + feedback.style.display = "block"; + feedback.classList.add("show"); + setTimeout(() => { + feedback.classList.remove("show"); + setTimeout(() => { + feedback.style.display = "none"; + }, 300); + }, 3000); + } + }); + }); + } +} + +// ===== UI State Management ===== function toggleEncryptionOptions() { const type = document.getElementById("encryption-type").value.trim().toLowerCase(); const passwordInputWrapper = document.getElementById("password-input"); + const fileSection = document.querySelector("#encoding-section #file-section"); const isAdvanced = type.includes("advanced"); if (passwordInputWrapper) { @@ -77,11 +104,18 @@ function toggleEncryptionOptions() { } } + if (fileSection) { + if (isAdvanced) { + fileSection.classList.remove("hidden"); + } else { + fileSection.classList.add("hidden"); + } + } + updateToggleLabels(); toggleInputMode(); } - function updateToggleLabels() { const type = document.getElementById("encryption-type")?.value; const leftLabel = document.getElementById("toggle-left-label"); @@ -112,6 +146,7 @@ function toggleInputMode() { removeBtn.style.display = fileSelected ? "inline-block" : "none"; } +// ===== Form Handling ===== async function handleSubmit(event) { event.preventDefault(); @@ -133,6 +168,10 @@ async function handleSubmit(event) { : decryptFile(fileInput, password); } + await handleTextOperation(encryptionType, operation, password); +} + +async function handleTextOperation(encryptionType, operation, password) { const payload = { "encryption-type": encryptionType, operation: operation, @@ -153,6 +192,7 @@ async function handleSubmit(event) { } } +// ===== Utility Functions ===== function removeFile() { const fileInput = document.getElementById("file-input"); if (fileInput) fileInput.value = ""; @@ -168,19 +208,30 @@ function generateRandomPassword() { charset.charAt(Math.floor(Math.random() * charset.length)) ).join(""); const passwordField = document.getElementById("generated-password"); - if (passwordField) passwordField.value = password; + if (passwordField) { + passwordField.value = password; + // Check if we should start Pacman + checkForPacman(); + } } function copyToClipboard(elementId, feedbackId) { const el = document.getElementById(elementId); const feedback = document.getElementById(feedbackId); - if (!el || !feedback) return; - navigator.clipboard.writeText(el.textContent || el.value || "").then(() => { - feedback.classList.add("show"); - setTimeout(() => { - feedback.classList.remove("show"); - }, 3000); + if (!el || !el.value) return; + + navigator.clipboard.writeText(el.value).then(() => { + if (feedback) { + feedback.style.display = "block"; + feedback.classList.add("show"); + setTimeout(() => { + feedback.classList.remove("show"); + setTimeout(() => { + feedback.style.display = "none"; + }, 300); // Wait for fade-out animation to complete + }, 3000); + } }); } @@ -196,6 +247,11 @@ function clearAll() { document.getElementById("encoding-section")?.style.setProperty("display", "block"); } +function handleInputChange() { + toggleInputMode(); + checkForPacman(); +} + function checkForPacman() { const val = document.getElementById("input-text").value.trim().toLowerCase(); const pacSection = document.getElementById("pacman-section"); @@ -210,6 +266,7 @@ function checkForPacman() { } } - function startPacman() { } function exitGame() { } + + diff --git a/templates/403.html b/templates/403.html index 7a34dee..4b3b163 100644 --- a/templates/403.html +++ b/templates/403.html @@ -3,20 +3,27 @@ - 403 - PacCrypt + + 403 Forbidden - PacCrypt + + + + + - -
+ +

PacCrypt

-

Secure Encoding, Encryption and Password Generation

+

Encrypt and share your text or files securely

+

🚫 403 - Forbidden

@@ -24,6 +31,7 @@ Looks like this area is locked behind a secret ghost door! πŸ›‘οΈπŸ‘»

+
@@ -36,10 +44,8 @@ - diff --git a/templates/404.html b/templates/404.html index f22a761..e71c1e0 100644 --- a/templates/404.html +++ b/templates/404.html @@ -3,27 +3,35 @@ - 404 - PacCrypt + + 404 Not Found - PacCrypt + + + + + - -
+ +

PacCrypt

-

Secure Encoding, Encryption and Password Generation

+

Encrypt and share your text or files securely

+

❓ 404 - Not Found

- Whoops! That page doesn’t seem to exist. Maybe it got encrypted? πŸ§©πŸ” + Whoops! That page doesn't seem to exist. Maybe it got encrypted? πŸ§©πŸ”

+
@@ -36,10 +44,8 @@ - diff --git a/templates/500.html b/templates/500.html index 7455a9c..2982b96 100644 --- a/templates/500.html +++ b/templates/500.html @@ -3,28 +3,36 @@ - 500 - PacCrypt + + 500 Server Error - PacCrypt + + + + + - -
+ +

PacCrypt

-

Secure Encoding, Encryption and Password Generation

+

Encrypt and share your text or files securely

+

πŸ’₯ 500 - Server Error

Uh oh! The ghosts chomped the server wires. πŸ§Ÿβ€β™‚οΈπŸ‘Ύ - We’re working on patching it up. + We're working on patching it up.

+
@@ -37,10 +45,8 @@ - diff --git a/templates/admin.html b/templates/admin.html index 9f6eb9c..a64373b 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -1,97 +1,93 @@ ο»Ώ - - + + + Admin Panel - PacCrypt - - - + + + + + + + + + - -
+ +

PacCrypt Admin Panel

Site Overview & Controls

+
+ +
+

πŸ’Ύ Server Management

+ +
+ +
+ + - -
-

πŸ” Site Map

-
    - {% for route in routes %} -
  • πŸ”— {{ route }}
  • - {% endfor %} -
- -
- - - + +
+
-
- -
+ +
+ + + + +
-
- -
- - {% with messages = get_flashed_messages(with_categories=true, category_filter=['clear-feedback']) %} - {% for category, message in messages %} -
{{ message }}
- {% endfor %} - {% endwith %} + +
+ + +
+ +
- -
+ +

πŸ”‘ Change Admin Password

+ {% with messages = get_flashed_messages(with_categories=true, category_filter=['password-feedback']) %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endwith %} - - +
- - + +
- -
-

πŸ“¦ Update Server from GitHub

-
- -
- - {% with update_msgs = get_flashed_messages(category_filter=["update"]) %} - {% if update_msgs %} -
- {{ update_msgs[0] | safe }} -
- {% endif %} - {% endwith %} -
- - -
+ +

πŸ“Š Server Status

  • πŸ•’ Uptime: {{ server_info.uptime }}
  • @@ -101,35 +97,22 @@
- -
+ +

πŸ“œ Server Logs

- +
- -
-

🧩 System Configuration

-

You can manage upload storage, limits, and expiration policies here:

- - - -
- -
-

© 2025 UnNaturalll-Dev. All rights reserved.

- GitHub Logo + GitHub Logo - Sitemap Png + Sitemap Png
@@ -151,5 +134,124 @@ } + diff --git a/templates/admin_login.html b/templates/admin_login.html index d6c902e..6cc6e57 100644 --- a/templates/admin_login.html +++ b/templates/admin_login.html @@ -1,34 +1,45 @@ ο»Ώ - - - Admin Login - PacCrypt - - - - + + + + Admin Login - PacCrypt + + + + + + + + + + - -
+ +

PacCrypt Admin

Administrator Login

+
+

πŸ”‘ Admin Login

+ {% with messages = get_flashed_messages() %} {% if messages %}

{{ messages[0] }}

{% endif %} {% endwith %} +
- - + +
@@ -36,13 +47,12 @@
+

© 2025 UnNaturalll-Dev. All rights reserved.

- GitHub Logo + GitHub Logo
- diff --git a/templates/admin_settings.html b/templates/admin_settings.html index 0d39c5e..92aa2f7 100644 --- a/templates/admin_settings.html +++ b/templates/admin_settings.html @@ -1,24 +1,32 @@ ο»Ώ - - + + + Admin Settings - PacCrypt - - - + + + + + + + - -
+ +

PacCrypt Admin Settings

-

Manage upload configuration securely

+

Manage upload configuration

+
+

βš™οΈ Upload Settings

+ {% with messages = get_flashed_messages() %} {% if messages %}
    @@ -29,16 +37,18 @@ {% endif %} {% endwith %} + - + - + - + +
    @@ -53,10 +63,8 @@ - diff --git a/templates/admin_setup.html b/templates/admin_setup.html index 6fb0f7f..eeacdf5 100644 --- a/templates/admin_setup.html +++ b/templates/admin_setup.html @@ -1,34 +1,45 @@ ο»Ώ - - + + + Admin Setup - PacCrypt - - - + + + + + + + + + - -
    + +

    PacCrypt Admin

    -

    Secure Admin Setup

    +

    Admin Setup

    +
    +

    πŸ›‘οΈ Create Admin Account

    + {% with messages = get_flashed_messages() %} {% if messages %}

    {{ messages[0] }}

    {% endif %} {% endwith %} + - - + +
    @@ -40,10 +51,8 @@

    © 2025 UnNaturalll-Dev. All rights reserved.

    - GitHub Logo + GitHub Logo
    - diff --git a/templates/index.html b/templates/index.html index 50a8a16..d545ede 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3,31 +3,38 @@ - PacCrypt + + PacCrypt - Encrypt and share your text or files securely + + + + + + - -
    + +

    PacCrypt

    Encrypt and share your text or files securely

    +
    - - -
    + +

    πŸ”‘ Password Generator

    - +
    -
    Copied to clipboard!
    +
    Password copied to clipboard!
    @@ -43,12 +50,11 @@
- - -
+ +

πŸ” Encrypt & Decrypt

- +
+
Encrypt
+
+
- +
+ -
- - -
- - +
+
-
Copied to clipboard!
+ + + +
+ +
+
Text copied to clipboard!
- -
-

πŸ“€ PacCrypt Sharing

-

Securely share a file with encryption and a pickup password.

+ +
+

πŸ“€ PacCrypt Share

+

Securely share encrypted files.

+

Do not lose your passwords, data will be lost forever!

+ {% with messages = get_flashed_messages() %} {% if messages %}
    @@ -104,10 +118,12 @@
  • {{ message | safe }} {% if "pickup" in message %} -
    - {{ message.split(" at ")[1] }} - - + {% endif %}
  • {% endfor %} @@ -116,31 +132,75 @@ {% endif %} {% endwith %} -
    + + + + - + -
    +
    + + +

    Files expire after {{ settings.max_file_age_days }} days.
    Max file size: {{ settings.max_file_size_bytes // (1024 * 1024 * 1024) }} GB.

-

© 2025 UnNaturalll-Dev. All rights reserved.

- GitHub Logo + GitHub Logo
- diff --git a/templates/pickup.html b/templates/pickup.html index 6115f0c..9bb3f27 100644 --- a/templates/pickup.html +++ b/templates/pickup.html @@ -1,24 +1,32 @@ ο»Ώ - - + + + Pickup File - PacCrypt - - - + + + + + + + - -
+ +

PacCrypt Pickup

-

Enter passwords to retrieve your file securely

+

Enter passwords to retrieve your file

+
+

πŸ” Decrypt and Download

+ {% with messages = get_flashed_messages() %} {% if messages %}
    @@ -29,16 +37,18 @@ {% endif %} {% endwith %} -
    - - + + + +
    - +
-
+ +

Link ID: {{ file_id }}

@@ -47,10 +57,8 @@ -