From 6ad2b65aba61c3da5527c1ad81e73d11b32f61f3 Mon Sep 17 00:00:00 2001 From: Tyler <68524461+TySP-Dev@users.noreply.github.com> Date: Tue, 29 Apr 2025 16:38:33 -1000 Subject: [PATCH] Lots of new features See release for more info --- README.md | 177 ++++++------ app.py | 525 ++++++++++++++++++++++++++++++---- settings.json | 1 + static/css/styles.css | 278 ++++++++++++------ static/img/Github_logo.png | Bin 0 -> 28074 bytes static/img/sitemap.png | Bin 0 -> 11825 bytes static/js/encryption.js | 86 ++++++ static/js/fileops.js | 92 ++++++ static/js/main.js | 12 + static/js/pacman.js | 262 +++++++++++++++++ static/js/ui.js | 215 ++++++++++++++ templates/403.html | 63 ++-- templates/404.html | 61 ++-- templates/500.html | 64 +++-- templates/admin.html | 155 ++++++++++ templates/admin_login.html | 48 ++++ templates/admin_settings.html | 62 ++++ templates/admin_setup.html | 49 ++++ templates/index.html | 270 +++++++++-------- templates/pickup.html | 56 ++++ 20 files changed, 2034 insertions(+), 442 deletions(-) create mode 100644 settings.json create mode 100644 static/img/Github_logo.png create mode 100644 static/img/sitemap.png create mode 100644 static/js/encryption.js create mode 100644 static/js/fileops.js create mode 100644 static/js/main.js create mode 100644 static/js/pacman.js create mode 100644 static/js/ui.js create mode 100644 templates/admin.html create mode 100644 templates/admin_login.html create mode 100644 templates/admin_settings.html create mode 100644 templates/admin_setup.html create mode 100644 templates/pickup.html diff --git a/README.md b/README.md index 7ce1f36..4a98056 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,29 @@ # PacCrypt WebApp -**PacCrypt** is a web-based platform that allows you to securely encrypt/decrypt text and files, generate passwords, and even enjoy a hidden Pac-Man game! -Built using Python (Flask), JavaScript, and AES-GCM encryption. +**PacCrypt** is a secure, feature-rich web app for encrypting and decrypting text and files โ€” built with Flask, JavaScript, and AES-GCM encryption. +Now with an admin control panel, GitHub updater, and a built-in Pac-Man easter egg! ๐Ÿ•น๏ธ -Official Website: [paccrypt.unnaturalll.dev](http://paccrypt.unnaturalll.dev) +Live demo: [paccrypt.unnaturalll.dev](http://paccrypt.unnaturalll.dev) --- ## โœจ Features -- ๐Ÿ”’ **Basic and Advanced Encryption** (Text and Files) -- ๐Ÿ”‘ **Password Generator** -- ๐Ÿ„น๏ธ **Pac-Man Easter Egg** (Type `pacman` to unlock!) -- ๐Ÿ“ฑ **Responsive Design** (Mobile Friendly) -- โšก **One-Click Start Scripts** (Dev and Production modes) -- ๐ŸŽจ **Modern Animated UI** (Dark Mode + Green Neon Theme) +- ๐Ÿ”’ Basic and Advanced Encryption for Text & Files +- ๐Ÿ“ Secure File Uploads with Pickup Passwords +- ๐Ÿ”‘ Random Password Generator +- ๐ŸŽฎ Hidden Pac-Man Game โ€” type `pacman` to play +- ๐Ÿง  Smart UI: Auto-switches input sections, toggles encryption labels +- ๐Ÿ“‹ Clipboard Copy Feedback with styled status boxes +- ๐Ÿงพ Admin Panel: + - Site map with live route list + - Server restart & GitHub update button + - Secure admin credential management + - Server logs & upload cleanup +- ๐Ÿงฉ System Settings Page for upload config +- ๐Ÿ“œ Custom 403, 404, and 500 Error Pages +- ๐Ÿค– robots.txt and /sitemap for crawlers +- ๐Ÿ“ฑ Mobile-Responsive UI --- @@ -22,90 +31,88 @@ Official Website: [paccrypt.unnaturalll.dev](http://paccrypt.unnaturalll.dev) ### ๐Ÿ“‹ Prerequisites -- **Python 3.7+** -- **Flask 3+** -- **Cryptography 42+** -- **Waitress 2.1+** -- **Nginx** (Recommended for production) +- Python 3.7+ +- Flask 3+ +- Cryptography 42+ +- Waitress 2.1+ +- Git (for update feature) +- Nginx (recommended) --- ### โšก Quick Setup -1. Clone the repository: +```bash +git clone https://github.com/TySP-Dev/PacCrypt.git +cd paccrypt-webapp-final +python -m venv venv +source venv/bin/activate # or venv\Scripts\activate on Windows +pip install -r requirements.txt +``` - ```bash - git clone https://github.com/TySP-Dev/PacCrypt.git - cd paccrypt-webapp-final - ``` +Then run: -2. Create and activate a virtual environment: +- Development Mode: + ```bash + ./start_dev.sh # or start_dev.bat + ``` - ```bash - python -m venv venv - source venv/bin/activate # Windows: venv\Scripts\activate - ``` +- Production Mode: + ```bash + ./start_prod.sh # or start_prod.bat + ``` -3. Install required Python packages: - - ```bash - pip install -r requirements.txt - ``` - -4. Start the app: - - **Windows**: - ```bash - start_dev.bat # For Development - start_prod.bat # For Production - ``` - - **Linux / Mac**: - ```bash - chmod +x start_dev.sh start_prod.sh - ./start_dev.sh # For Development - ./start_prod.sh # For Production - ``` - -5. Access the app at: - [http://127.0.0.1:5000](http://127.0.0.1:5000) +Visit [http://127.0.0.1:5000](http://127.0.0.1:5000) --- -## ๐Ÿš€ Usage Guide +## ๐Ÿงญ Navigation & Usage -### ๐Ÿ”’ Text Encryption/Decryption +### ๐Ÿ” Encrypt & Decrypt -- Select **Encryption Type** (Basic or Advanced) -- Enter text -- Provide password (Advanced only) -- Choose **Encrypt** or **Decrypt** -- Click **Submit** +- Choose between Basic Cipher or Advanced AES +- Type your message or upload a file +- Enter password (if AES) +- Select mode using toggle (Encrypt/Decrypt) +- Hit Execute -### ๐Ÿ“ File Encryption/Decryption +### ๐Ÿ“ค Share Files -- Select **Advanced** encryption -- Upload a file -- Provide password -- Choose **Encrypt** or **Decrypt** -- Click **Submit** +- Upload a file with two passwords: + - Encryption password + - Pickup password +- Get a shareable URL and click ๐Ÿ“‹ Copy Link -### ๐Ÿ”‘ Password Generator +### ๐Ÿ”‘ Generate Passwords -- Click **Generate** to create a secure password -- Click **Copy** to save it to clipboard +- Click Generate +- Then hit ๐Ÿ“‹ Copy -### ๐ŸŽฎ Pac-Man Easter Egg +### ๐ŸŽฎ Pac-Man Game -- Type **`pacman`** into the input box to unlock the hidden Pac-Man game! +- Type `pacman` in the input box +- Game appears with Restart/Exit controls +- Classic arrow key controls ๐Ÿ•น๏ธ --- -## ๐Ÿ›ก๏ธ Hosting with Nginx (optional) +## ๐Ÿ› ๏ธ Admin Panel -Recommended for secure public deployment. +Visit `/adminpage` after setting up credentials at `/admin-setup`. -Example minimal Nginx config: +Features: +- ๐Ÿ”„ Restart server +- ๐Ÿ”ƒ Update from GitHub (git pull) +- ๐Ÿงฝ Clear uploads +- ๐Ÿ” Change admin password +- ๐Ÿ“ View logs +- โš™๏ธ Adjust upload settings + +--- + +## ๐Ÿ›ก๏ธ Deployment Tips + +Minimal Nginx config: ```nginx server { @@ -121,52 +128,50 @@ server { } ``` -> Tip: Set up SSL with Let's Encrypt for HTTPS security! ๐Ÿ” +Use Let's Encrypt to add SSL/TLS support. --- -## ๐Ÿ“‚ Project Structure +## ๐Ÿ—‚๏ธ Project Structure ``` paccrypt-webapp-final/ โ”œโ”€โ”€ app.py โ”œโ”€โ”€ requirements.txt +โ”œโ”€โ”€ README.md โ”œโ”€โ”€ templates/ โ”‚ โ”œโ”€โ”€ index.html โ”‚ โ”œโ”€โ”€ 404.html โ”‚ โ””โ”€โ”€ 403.html โ”‚ โ””โ”€โ”€ 500.html +โ”‚ โ””โ”€โ”€ admin.html +โ”‚ โ””โ”€โ”€ admin_login.html +โ”‚ โ””โ”€โ”€ admin_settings.html +โ”‚ โ””โ”€โ”€ admin_setup.html +โ”‚ โ””โ”€โ”€ pickup.html โ”œโ”€โ”€ static/ โ”‚ โ”œโ”€โ”€ css/ โ”‚ โ”‚ โ””โ”€โ”€ styles.css โ”‚ โ”œโ”€โ”€ js/ -โ”‚ โ”‚ โ””โ”€โ”€ script.js +โ”‚ โ”‚ โ””โ”€โ”€ ui.js +โ”‚ โ”‚ โ””โ”€โ”€ pacman.js +โ”‚ โ”‚ โ””โ”€โ”€ main.js +โ”‚ โ”‚ โ””โ”€โ”€ fileops.js +โ”‚ โ”‚ โ””โ”€โ”€ encryption.js โ”‚ โ”œโ”€โ”€ img/ โ”‚ โ”‚ โ””โ”€โ”€ PacCrypt.png +โ”‚ โ”‚ โ””โ”€โ”€ Github_logo.png +โ”‚ โ”‚ โ””โ”€โ”€ sitemap.png โ”‚ โ””โ”€โ”€ audio/ โ”‚ โ””โ”€โ”€ chomp.mp3 โ”œโ”€โ”€ start_dev.bat โ”œโ”€โ”€ start_prod.bat โ”œโ”€โ”€ start_dev.sh โ”œโ”€โ”€ start_prod.sh -โ”œโ”€โ”€ README.md ``` --- -## ๐Ÿค Contributing - -Contributions are welcome! - -- Add new features -- Fix bugs -- Improve performance -- Expand the Pac-Man Easter Egg ๐ŸŽฎ - ---- - ## ๐Ÿ“„ License -This project is licensed under the **MIT License**. - ---- \ No newline at end of file +MIT ยฉ [TySP-Dev](https://github.com/TySP-Dev) diff --git a/app.py b/app.py index b91ccf4..9c4f81e 100644 --- a/app.py +++ b/app.py @@ -1,90 +1,498 @@ -## DEV DEV DEV - import os -from flask import Flask, render_template, request, jsonify +import io +import json import html import base64 +import hashlib +import secrets +import shutil +import datetime +import subprocess +import platform + +from flask import ( + Flask, render_template, request, jsonify, session, + redirect, url_for, flash, send_file +) +from werkzeug.utils import secure_filename from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives.hashes import SHA256 from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.fernet import Fernet app = Flask(__name__) +app.secret_key = os.getenv("FLASK_SECRET", os.urandom(24)) -# ====== Your App Code ====== - +ADMIN_CRED_FILE = 'admin_creds.json' +ADMIN_KEY_FILE = 'admin_key.key' +ADMIN_LOG_FILE = 'admin_logs.enc' +SETTINGS_FILE = 'settings.json' ALPHABET = list('abcdefghijklmnopqrstuvwxyz') +DEFAULT_SETTINGS = { + "upload_folder": "uploads", + "max_file_age_days": 14, + "max_file_size_bytes": 25 * 1024 * 1024 * 1024 # 25GB +} + +# === Settings === +def load_settings(): + if not os.path.exists(SETTINGS_FILE): + with open(SETTINGS_FILE, 'w') as f: + json.dump(DEFAULT_SETTINGS, f) + with open(SETTINGS_FILE, 'r') as f: + return json.load(f) + +settings = load_settings() +UPLOAD_FOLDER = settings["upload_folder"] +MAX_FILE_AGE_DAYS = settings["max_file_age_days"] +MAX_FILE_SIZE_BYTES = settings["max_file_size_bytes"] + +if not os.path.exists(UPLOAD_FOLDER): + os.makedirs(UPLOAD_FOLDER) + +# === Crypto === +def derive_key(password: str, salt: bytes) -> bytes: + return PBKDF2HMAC(algorithm=SHA256(), length=32, salt=salt, iterations=200_000).derive(password.encode()) + +def hash_password(password: str, salt: bytes) -> str: + return base64.urlsafe_b64encode(derive_key(password, salt)).decode() + def simple_encode(text: str) -> str: - return ''.join( - ALPHABET[(ALPHABET.index(c) + 3) % 26] if c in ALPHABET else c - for c in text.lower() - ) + 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: - return ''.join( - ALPHABET[(ALPHABET.index(c) - 3) % 26] if c in ALPHABET else c - for c in text.lower() - ) - -def derive_key(password: str, salt: bytes) -> bytes: - kdf = PBKDF2HMAC( - algorithm=SHA256(), - length=32, - salt=salt, - iterations=200_000, - ) - return kdf.derive(password.encode()) + 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: salt = os.urandom(16) key = derive_key(password, salt) - - aesgcm = AESGCM(key) nonce = os.urandom(12) - - ct = aesgcm.encrypt(nonce, plaintext.encode(), None) - encrypted = salt + nonce + ct - return base64.urlsafe_b64encode(encrypted).decode() + ct = AESGCM(key).encrypt(nonce, plaintext.encode(), None) + return base64.urlsafe_b64encode(salt + nonce + ct).decode() def advanced_decrypt(token_b64: str, password: str) -> str: try: data = base64.urlsafe_b64decode(token_b64.encode()) salt, nonce, ct = data[:16], data[16:28], data[28:] key = derive_key(password, salt) - aesgcm = AESGCM(key) - pt = aesgcm.decrypt(nonce, ct, None) - return pt.decode() + return AESGCM(key).decrypt(nonce, ct, None).decode() except Exception: return "[Error] Invalid password or corrupted data!" +# === Admin Auth === +def load_admin_key(): + if not os.path.exists(ADMIN_KEY_FILE): + with open(ADMIN_KEY_FILE, 'wb') as f: + f.write(Fernet.generate_key()) + with open(ADMIN_KEY_FILE, 'rb') as f: + return f.read() + +def encrypt_creds(username, password): + key = load_admin_key() + cipher = Fernet(key) + salt = os.urandom(16) + hashed_pw = hash_password(password, salt) + data = json.dumps({"u": username, "p": hashed_pw, "s": base64.b64encode(salt).decode()}).encode() + with open(ADMIN_CRED_FILE, 'wb') as f: + f.write(cipher.encrypt(data)) + +def check_creds(username, password): + try: + key = load_admin_key() + cipher = Fernet(key) + with open(ADMIN_CRED_FILE, 'rb') as f: + decrypted = cipher.decrypt(f.read()) + creds = json.loads(decrypted) + salt = base64.b64decode(creds["s"]) + return creds["u"] == username and creds["p"] == hash_password(password, salt) + except Exception as e: + print("[ERROR] check_creds failed:", e) + return False + +def log_admin_event(message: str): + try: + key = load_admin_key() + cipher = Fernet(key) + timestamp = datetime.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") + except Exception as e: + print("[ERROR] Failed to write admin log:", e) + +# === Text Encryption Route === @app.route("/", methods=["GET", "POST"]) def index(): if request.method == 'POST': - data = request.get_json() - encryption_type = data.get("encryption-type", "basic") - operation = data.get("operation", "") - message = data.get("message", "") - password = data.get("password", "") - file_password = data.get("file-password", "") + 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') - final_password = file_password if file_password else password + if not file or not enc_password or not pickup_password: + flash('Missing fields') + return redirect(url_for('index')) - if encryption_type == "basic": - result = simple_encode(message) if operation == "encrypt" else simple_decode(message) + 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)) + + return render_template("index.html", result="", password="", encryption_type="advanced", settings=settings) + +# === File Pickup Route === +@app.route("/pickup/", methods=["GET", "POST"]) +def pickup_file(file_id): + meta_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.json") + enc_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.enc") + + if not os.path.exists(meta_path) or not os.path.exists(enc_path): + flash("File not found or expired") + 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 render_template("pickup.html", file_id=file_id) + +def cleanup_expired_files(): + now = datetime.datetime.utcnow() + + 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}") + + +# === Admin Log Viewer === +@app.route("/admin-logs") +def admin_logs(): + if not session.get("admin_logged_in"): + return redirect(url_for("admin_login")) + + logs = [] + try: + key = load_admin_key() + cipher = Fernet(key) + if os.path.exists(ADMIN_LOG_FILE): + with open(ADMIN_LOG_FILE, 'rb') as f: + lines = f.readlines() + for line in lines[-100:]: + if line.strip(): + try: + decrypted = cipher.decrypt(line.strip()) + logs.append(decrypted.decode()) + except Exception: + logs.append("[Error] Corrupted log entry.") + except Exception as e: + logs.append(f"[Error loading logs] {str(e)}") + + return jsonify(logs=logs) + +# === Admin Settings Editor === +@app.route("/admin-settings", methods=["GET", "POST"]) +def admin_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 render_template("admin_settings.html", settings=current_settings) + +# === Admin Setup === +@app.route("/admin-setup", methods=["GET", "POST"]) +def admin_setup(): + if os.path.exists(ADMIN_CRED_FILE): + return redirect(url_for("admin_login")) + if request.method == "POST": + u = request.form.get("username") + p = request.form.get("password") + if u and p: + encrypt_creds(u, p) + session["admin_logged_in"] = True + return redirect(url_for("admin_page")) + flash("Both fields required") + return render_template("admin_setup.html") + +# === Admin Login === +@app.route("/admin-login", methods=["GET", "POST"]) +def admin_login(): + if request.method == "POST": + u = request.form.get("username") + p = request.form.get("password") + if check_creds(u, p): + session["admin_logged_in"] = True + log_admin_event("Admin login successful.") + return redirect(url_for("admin_page")) else: - result = advanced_encrypt(message, final_password) if operation == "encrypt" else advanced_decrypt(message, final_password) + log_admin_event("Admin login failed.") + flash("Incorrect credentials") + return render_template("admin_login.html") - return jsonify(result=html.escape(result)) +# === Admin Logout === +@app.route("/admin-logout") +def admin_logout(): + session.pop("admin_logged_in", None) + return redirect(url_for("index")) - return render_template( - "index.html", - result="", - password="", - encryption_type="advanced" - ) +# === Admin Page === +@app.route("/adminpage") +def admin_page(): + if not session.get("admin_logged_in"): + if not os.path.exists(ADMIN_CRED_FILE): + return redirect(url_for("admin_setup")) + return redirect(url_for("admin_login")) -# ====== Smart Server Startup ====== + 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" + + server_info = { + "uptime": uptime, + "time": datetime.datetime.now().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") +def restart_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")) + +# === Reset Admin Credentials === +@app.route("/admin-reset", methods=["POST"]) +def admin_reset(): + if not session.get("admin_logged_in"): + return redirect(url_for("admin_login")) + try: + if os.path.exists(ADMIN_CRED_FILE): + os.remove(ADMIN_CRED_FILE) + if os.path.exists(ADMIN_KEY_FILE): + os.remove(ADMIN_KEY_FILE) + session.pop("admin_logged_in", None) + flash("Admin credentials reset. Please create new credentials.") + except Exception as e: + flash("Failed to reset admin credentials.") + 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(): + if not session.get("admin_logged_in"): + return redirect(url_for("admin_login")) + + current = request.form.get("current_password") + new = request.form.get("new_password") + + try: + key = load_admin_key() + cipher = Fernet(key) + with open(ADMIN_CRED_FILE, 'rb') as file: + decrypted = cipher.decrypt(file.read()) + creds = json.loads(decrypted) + + salt = base64.b64decode(creds["s"]) + if hash_password(current, salt) != creds["p"]: + flash("Current password is incorrect") + return redirect(url_for("admin_page")) + + creds["p"] = hash_password(new, salt) + encrypted = cipher.encrypt(json.dumps(creds).encode()) + with open(ADMIN_CRED_FILE, 'wb') as file: + file.write(encrypted) + + log_admin_event("Admin password changed.") + flash("Password updated successfully", "password-feedback") + return redirect(url_for("admin_page")) + + except Exception as e: + flash("Failed to update password") + print("[ERROR] Password change failed:", e) + return redirect(url_for("admin_page")) + +@app.route("/admin-clear-uploads", methods=["POST"]) +def admin_clear_uploads(): + if not session.get("admin_logged_in"): + return redirect(url_for("admin_login")) + + deleted = 0 + for filename in os.listdir(UPLOAD_FOLDER): + if filename.endswith(".enc") or filename.endswith(".json"): + try: + os.remove(os.path.join(UPLOAD_FOLDER, filename)) + deleted += 1 + except Exception as e: + print("[ERROR] Failed to delete:", filename, e) + + flash(f"Cleared {deleted} uploaded file(s).", "clear-feedback") + return redirect(url_for("admin_page")) + +@app.route("/admin-update-server", methods=["POST"]) +def admin_update_server(): + if not session.get("admin_logged_in"): + return redirect(url_for("admin_login")) + + try: + output = subprocess.check_output(["git", "pull", "origin", "main"], cwd=os.getcwd()).decode() + flash("โœ… Server updated successfully!
" + html.escape(output) + "
", "update") + except subprocess.CalledProcessError as e: + flash("โŒ Failed to update server:
" + html.escape(e.output.decode()) + "
", "update") + except Exception as ex: + flash("โŒ An error occurred: " + str(ex), "update") + + return redirect(url_for("admin_page")) + +@app.route("/sitemap") +def sitemap(): + output = ["

PacCrypt Sitemap

", "") + return "\n".join(output) + +@app.route("/robots.txt") +def robots_txt(): + lines = [ + "User-agent: *", + "Disallow: /adminpage", + "Disallow: /admin-login", + "Disallow: /admin-setup", + "Disallow: /admin-reset", + "Disallow: /admin-settings", + "Disallow: /restart-server", + "Disallow: /pickup", + "Disallow: /admin-change-password", + "Allow: /", + f"Sitemap: {url_for('sitemap', _external=True)}" + ] + return "\n".join(lines), 200, {"Content-Type": "text/plain"} + + + + +# === Error Handlers === @app.errorhandler(404) def page_not_found(e): return render_template('404.html'), 404 @@ -97,16 +505,25 @@ def server_error(e): def forbidden(e): return render_template('403.html'), 403 +@app.errorhandler(405) +def method_not_allowed(e): + return render_template('403.html'), 403 + +@app.errorhandler(FileNotFoundError) +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 + + +# === Server Mode Execution === if __name__ == "__main__": PRODUCTION = os.getenv("PRODUCTION", "false").lower() == "true" - if PRODUCTION: from waitress import serve print("[INFO] Running in PRODUCTION mode with Waitress.") serve(app, host="0.0.0.0", port=5000) else: print("[INFO] Running in DEVELOPMENT mode with Flask server.") - app.run(debug=True, host="0.0.0.0", port=5000) - - -## DEV DEV DEV + app.run(debug=True, host="0.0.0.0", port=5000) \ No newline at end of file diff --git a/settings.json b/settings.json new file mode 100644 index 0000000..0642050 --- /dev/null +++ b/settings.json @@ -0,0 +1 @@ +{"upload_folder": "uploads", "max_file_age_days": 14, "max_file_size_bytes": 26843545600} \ No newline at end of file diff --git a/static/css/styles.css b/static/css/styles.css index 9b36441..8ce1e2e 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -38,7 +38,6 @@ header { header p { font-size: 1.2em; - color: #00ff99; } /* ===== Main Layout ===== */ @@ -53,26 +52,26 @@ main { gap: 30px; } -/* ===== Section Card Styling ===== */ +/* ===== Card Styling ===== */ .card { background-color: #1e1e1e; padding: 25px; width: 100%; - max-width: 800px; border-radius: 12px; box-shadow: 0 0 15px rgba(0, 255, 153, 0.4); text-align: center; } -/* ===== Uniform Form Inputs ===== */ +/* ===== Form Group Styling ===== */ .form-group { - display: flex; + display: flex !important; flex-direction: column; align-items: center; - gap: 20px; + gap: 0px; width: 100%; } +/* ===== Inputs, Textareas, Selects ===== */ input, textarea, select, @@ -85,7 +84,9 @@ input[type="file"] { background-color: #2c2f33; color: #00ff99; font-size: 1em; + text-align: center; transition: 0.3s; + margin:10px auto; } textarea { @@ -93,15 +94,13 @@ textarea { resize: none; } -input[type="password"], -#password { +input[type="password"] { min-height: 50px; } input[type="file"] { border: 2px dashed #00ff99; cursor: pointer; - text-align: center; } input[type="file"]::file-selector-button { @@ -118,6 +117,7 @@ input[type="file"] { background-color: #00cc77; } +/* ===== Focus Effects ===== */ input:focus, textarea:focus, select:focus { @@ -125,29 +125,27 @@ select:focus { box-shadow: 0 0 8px rgba(0, 255, 153, 0.8); } -/* ===== Match input and output sizes ===== */ +/* ===== Textareas Specific Widths ===== */ #input-text, #output-text { width: 80%; max-width: 500px; height: 140px; - box-sizing: border-box; - resize: none; } -/* ===== Buttons ===== */ +/* ===== Button Group Styling ===== */ .button-group { display: flex; flex-wrap: wrap; justify-content: center; gap: 15px; - margin-top: 15px; + margin: 10px auto; width: 100%; } button { padding: 10px 20px; - border: 2px solid #00ff99; + border: 0px solid #00ff99; border-radius: 8px; background-color: #2c2f33; color: #00ff99; @@ -156,6 +154,7 @@ button { transition: 0.3s; width: 100%; max-width: 200px; + /* margin: 10px auto; */ } button:hover { @@ -163,69 +162,86 @@ button { color: #121212; } -/* ===== Toggle Buttons (Encode/Decode, Encrypt/Decrypt) ===== */ -.radio-group { +/* ===== Toggle Switch Styling ===== */ +.toggle-container { display: flex; + align-items: center; justify-content: center; - gap: 8px; - margin-top: 8px; + gap: 12px; + margin-top: 10px; width: 100%; } -.radio-button { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 1px 1px; - border: 2px solid #00ff99; - border-radius: 8px; - background-color: #2c2f33; - color: #00ff99; - cursor: pointer; - transition: 0.3s; +/* Make sure the switch aligns well */ +.switch { position: relative; - box-shadow: none; + display: flex; + align-items: center; /* <-- Ensures vertical centering */ + justify-content: center; + width: 70px; + height: 34px; } - .radio-button:hover { - background-color: #00ff99; - color: #121212; + /* Hide the checkbox */ + .switch input { + opacity: 0; + width: 0; + height: 0; } - /* Hide the actual radio input */ - .radio-button input { - display: none; - } - - /* When selected, make the ENTIRE BUTTON glow */ - .radio-button input:checked + span { - background-color: #2c2f33; - color: #00ff99; - box-shadow: 0 0 15px rgba(0, 255, 153, 0.7); - border-radius: 8px; - padding: 8px 18px; - display: inline-flex; - align-items: center; - justify-content: center; - } - - -/* ===== Remove File Button ===== */ -#remove-file-btn { - display: none; - margin-top: 8px; - padding: 8px 16px; - border: 2px solid #ff5555; +/* The slider */ +.slider { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; background-color: #2c2f33; - color: #ff5555; - border-radius: 8px; - cursor: pointer; - transition: 0.3s; + border: 2px solid #00ff99; + border-radius: 34px; + transition: .4s; + display: flex; + align-items: center; } - #remove-file-btn:hover { - background-color: #ff5555; - color: #2c2f33; + /* The circle knob */ + .slider::before { + content: ""; + height: 26px; + width: 26px; + background-color: #00ff99; + border-radius: 50%; + transition: .4s; + transform: translateX(4px); + position: absolute; + left: 0px; + bottom: 2.5px; + } + +input:checked + .slider::before { + transform: translateX(36px); +} + +/* Toggle Labels */ +.labels { + position: relative; + width: 100px; + display: flex; + justify-content: space-between; + font-size: 0.9em; + color: #00ff99; + margin-top: 5px; +} + + .labels::before, + .labels::after { + content: attr(data-on); + width: 50%; + text-align: center; + } + + .labels::after { + content: attr(data-off); } /* ===== Toast Notifications ===== */ @@ -244,11 +260,11 @@ button { display: flex; align-items: center; justify-content: center; - animation: fadein 0.5s, fadeout 0.5s 2.5s; } .toast.show { visibility: visible; + animation: fadein 0.5s, fadeout 0.5s 2.5s; } @keyframes fadein { @@ -271,21 +287,6 @@ button { } } -/* ===== Pacman Canvas ===== */ -.pacman-wrapper { - display: flex; - justify-content: center; - margin-bottom: 18px; -} - -#pacmanCanvas { - background-color: black; - border: 2px solid #00ff99; - border-radius: 10px; - width: 800px; - height: 600px; -} - /* ===== Footer ===== */ footer { text-align: center; @@ -310,15 +311,114 @@ footer { /* ===== Responsive Tweaks ===== */ @media (max-width: 600px) { - input, - textarea, - select, - #input-text, - #output-text, - #password-field, - #password, - #file-password { + input, textarea, select, #input-text, #output-text { width: 100%; max-width: 90%; } } + +/* ===== Copy Feedback Message ===== */ +.copy-feedback { + background-color: #2a2a2a; + border: 1px solid #00ff99; + padding: 6px 12px; + margin-top: 6px; + border-radius: 6px; + color: #00ff99; + font-size: 0.9em; + opacity: 0; + transition: opacity 0.3s ease; + text-align: center; + max-width: 300px; + margin-left: auto; + margin-right: auto; +} + + .copy-feedback.show { + opacity: 1; + } + +.hidden { + display: none !important; +} + +form { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; +} + + form input, + form button { + width: 80%; + max-width: 500px; + margin-bottom: 12px; + text-align: center; + } + +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; +} + +#pacmanCanvas { + background-color: black; + display: block; + margin: auto; + border: 2px solid #00ff99; + border-radius: 12px; + align-items: center; + justify-content: center; +} +#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; +} + +.pacman-wrapper { + display: flex; + justify-content: center; + align-items: center; + width: 100%; +} + + +/* ===== Utility: Hidden Class ===== */ +.hidden { + display: none !important; +} diff --git a/static/img/Github_logo.png b/static/img/Github_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..84ed9088ebe99ed459b2486cf15cdf97f4ccc32f GIT binary patch literal 28074 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 z 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 + */ +export async function deriveKey(password, salt) { + const encoder = new TextEncoder(); + const keyMaterial = await crypto.subtle.importKey( + 'raw', + encoder.encode(password), + { name: 'PBKDF2' }, + false, + ['deriveKey'] + ); + + return crypto.subtle.deriveKey( + { + name: 'PBKDF2', + salt, + iterations: 200_000, + hash: 'SHA-256' + }, + keyMaterial, + { name: 'AES-GCM', length: 256 }, + false, + ['encrypt', 'decrypt'] + ); +} + +/** + * Encrypts a message using AES-GCM with a derived key. + * @param {string} message - Plaintext message to encrypt. + * @param {string} password - User password for key derivation. + * @returns {Promise} - Base64-encoded encrypted string. + */ +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 key = await deriveKey(password, salt); + const encoded = encoder.encode(message); + + 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); + output.set(iv, salt.length); + output.set(new Uint8Array(ciphertext), salt.length + iv.length); + + return btoa(String.fromCharCode(...output)); +} + +/** + * Decrypts an AES-GCM encrypted string. + * @param {string} encryptedData - Base64-encoded ciphertext. + * @param {string} password - Password used to derive the decryption key. + * @returns {Promise} - Decrypted plaintext. + */ +export async function decryptAdvanced(encryptedData, password) { + const encrypted = new Uint8Array( + 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 key = await deriveKey(password, salt); + + const decrypted = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv }, + key, + ciphertext + ); + + return new TextDecoder().decode(decrypted); +} + +/** + * Optional init logging for module diagnostics. + */ +export function setupEncryption() { + console.log('[Encryption] Module loaded'); +} diff --git a/static/js/fileops.js b/static/js/fileops.js new file mode 100644 index 0000000..c90c6b9 --- /dev/null +++ b/static/js/fileops.js @@ -0,0 +1,92 @@ +// 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. + */ +export function encryptFile(fileInput, password) { + if (!fileInput.files.length) { + alert("Please select a file!"); + return; + } + + const file = fileInput.files[0]; + const reader = new FileReader(); + + 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); +} + +/** + * 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; + } + + const file = fileInput.files[0]; + const reader = new FileReader(); + + 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."); + } + }; + + 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 new file mode 100644 index 0000000..4af3926 --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,12 @@ +// main.js + +import { setupUI } from './ui.js'; +import { setupGame } from './pacman.js'; + +/** + * Initialize UI and game once the DOM is fully loaded. + */ +window.addEventListener("DOMContentLoaded", () => { + setupUI(); + setupGame(); +}); diff --git a/static/js/pacman.js b/static/js/pacman.js new file mode 100644 index 0000000..42f8b72 --- /dev/null +++ b/static/js/pacman.js @@ -0,0 +1,262 @@ +// pacman.js + +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); +} + +export function stopPacman() { + clearInterval(gameInterval); + if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height); +} + +export function resetGame() { + stopPacman(); + startPacman(); +} + +export function exitGame() { + stopPacman(); + document.getElementById("input-text").value = ""; + document.getElementById("pacman-section").style.display = "none"; + document.getElementById("encoding-section").style.display = "block"; +} + +// ====== Game Setup Helpers ====== + +function spawn() { + const options = []; + for (let c = 1; c < cols - 1; c++) { + for (let r = 1; r < rows - 1; r++) { + if (!walls.some(w => w.c === c && w.r === r)) { + const neighbors = [ + { c: c + 1, r }, { c: c - 1, r }, + { c, r: r + 1 }, { c, r: r - 1 } + ]; + if (neighbors.some(n => !walls.some(w => w.c === n.c && w.r === n.r))) { + options.push({ c, r }); + } + } + } + } + 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, + dx: 0, + dy: 0 + }; +} + +function rand() { + const x = Math.sin(randSeed++) * 10000; + return x - Math.floor(x); +} + +function generateWalls() { + 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) { + walls.push({ c, r }); + } + } + } +} + +function generateDots() { + dots = []; + for (let c = 1; c < cols - 1; c++) { + for (let r = 1; r < rows - 1; r++) { + if (walls.some(w => w.c === c && w.r === r)) continue; + + const isEnclosed = + walls.some(w => w.c === c + 1 && w.r === r) && + walls.some(w => w.c === c - 1 && w.r === r) && + walls.some(w => w.c === c && w.r === r + 1) && + walls.some(w => w.c === c && w.r === r - 1); + + if (!isEnclosed) dots.push({ c, r }); + } + } +} + +// ====== Game Loop & Drawing ====== + +function gameLoop() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + drawWalls(); + moveChar(pacman); + moveEnemy(); + drawChar(pacman, "yellow"); + drawChar(enemy, "red"); + eatDots(); + drawScore(); + checkGameOver(); +} + +function drawWalls() { + ctx.fillStyle = "blue"; + walls.forEach(w => { + ctx.fillRect(w.c * cellSize, w.r * cellSize, cellSize, cellSize); + }); +} + +function drawChar(ch, color) { + ctx.beginPath(); + ctx.arc(ch.x, ch.y, ch.size, 0, Math.PI * 2); + ctx.fillStyle = color; + ctx.fill(); +} + +function drawScore() { + ctx.fillStyle = "white"; + ctx.font = "20px Poppins"; + ctx.fillText("Score: " + score, 10, 25); +} + +function checkGameOver() { + if ( + Math.abs(pacman.x - enemy.x) < pacman.size && + Math.abs(pacman.y - enemy.y) < pacman.size + ) { + ctx.fillStyle = "#00ff99"; + ctx.font = "40px Poppins"; + ctx.textAlign = "center"; + ctx.fillText("Game Over!", canvas.width / 2, canvas.height / 2); + clearInterval(gameInterval); + } +} + +// ====== 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; } +} + +function moveChar(ch) { + const nx = ch.x + ch.dx; + const ny = ch.y + ch.dy; + if (!willCollide(nx, ny, ch.size)) { + ch.x = nx; + ch.y = ny; + } +} + +function moveEnemy() { + const options = []; + const moves = [[enemySpeed, 0], [-enemySpeed, 0], [0, enemySpeed], [0, -enemySpeed]]; + + moves.forEach(([dx, dy]) => { + const nx = enemy.x + dx; + const ny = enemy.y + dy; + if (!willCollide(nx, ny, enemy.size)) options.push({ dx, dy }); + }); + + if (!options.length) return; + + let best = options[0]; + let bestDist = dist(enemy.x + best.dx, enemy.y + best.dy, pacman.x, pacman.y); + + for (const opt of options) { + const d = dist(enemy.x + opt.dx, enemy.y + opt.dy, pacman.x, pacman.y); + if (d < bestDist) { + best = opt; + bestDist = d; + } + } + + enemy.x += best.dx; + enemy.y += best.dy; +} + +function dist(x1, y1, x2, y2) { + return Math.abs(x1 - x2) + Math.abs(y1 - y2); +} + +function willCollide(x, y, size) { + const left = x - size, right = x + 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; + return right > wx1 && left < wx2 && bottom > wy1 && top < wy2; + }); +} + +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; + + if (Math.abs(pacman.x - dx) < pacman.size && Math.abs(pacman.y - dy) < pacman.size) { + score++; + if (chompSound) { + chompSound.currentTime = 0; + chompSound.volume = 0.4; + chompSound.play(); + } + return false; + } + return true; + }); + + 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.fill(); + }); +} + +window.resetGame = resetGame; +window.exitGame = exitGame; diff --git a/static/js/ui.js b/static/js/ui.js new file mode 100644 index 0000000..7e0c2cf --- /dev/null +++ b/static/js/ui.js @@ -0,0 +1,215 @@ +๏ปฟ// ui.js +import { encryptFile, decryptFile } from './fileops.js'; + +/** + * Initialize all UI functionality after DOM is loaded + */ +export function setupUI() { + toggleEncryptionOptions(); + toggleInputMode(); + + 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"); + + 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" }); + } + } +} + + + + +function toggleEncryptionOptions() { + const type = document.getElementById("encryption-type").value.trim().toLowerCase(); + const passwordInputWrapper = document.getElementById("password-input"); + const isAdvanced = type.includes("advanced"); + + if (passwordInputWrapper) { + if (isAdvanced) { + passwordInputWrapper.classList.remove("hidden"); + } else { + passwordInputWrapper.classList.add("hidden"); + } + } + + updateToggleLabels(); + toggleInputMode(); +} + + +function updateToggleLabels() { + const type = document.getElementById("encryption-type")?.value; + const leftLabel = document.getElementById("toggle-left-label"); + const rightLabel = document.getElementById("toggle-right-label"); + + if (!type || !leftLabel || !rightLabel) return; + + const isAdvanced = type.toLowerCase().includes("advanced"); + leftLabel.textContent = isAdvanced ? "Encrypt" : "Encode"; + rightLabel.textContent = isAdvanced ? "Decrypt" : "Decode"; +} + +function toggleInputMode() { + const fileInput = document.getElementById("file-input"); + const textValue = document.getElementById("input-text")?.value.trim(); + const isAdvanced = document.getElementById("encryption-type")?.value === "advanced"; + + const textSection = document.getElementById("text-section"); + const fileSection = document.getElementById("file-section"); + const removeBtn = document.getElementById("remove-file-btn"); + + if (!fileInput || !textSection || !fileSection || !removeBtn) return; + + const fileSelected = fileInput.files.length > 0; + + textSection.style.display = fileSelected ? "none" : "flex"; + fileSection.style.display = (isAdvanced && !textValue) ? "flex" : "none"; + removeBtn.style.display = fileSelected ? "inline-block" : "none"; +} + +async function handleSubmit(event) { + event.preventDefault(); + + const encryptionType = document.getElementById("encryption-type")?.value; + const password = document.getElementById("password")?.value; + const fileInput = document.getElementById("file-input"); + const isDecrypt = document.getElementById("operation-toggle").checked; + const operation = isDecrypt ? "decrypt" : "encrypt"; + + if (!encryptionType || !fileInput) return; + + if (encryptionType === "advanced" && !password) { + return alert("Password is required for advanced encryption."); + } + + if (fileInput.files.length > 0) { + return (operation === "encrypt") + ? encryptFile(fileInput, password) + : decryptFile(fileInput, password); + } + + const payload = { + "encryption-type": encryptionType, + operation: operation, + message: document.getElementById("input-text")?.value, + password: password + }; + + try { + const response = await fetch("/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) + }); + const data = await response.json(); + document.getElementById("output-text").value = data.result; + } catch (err) { + alert("Error processing request: " + err.message); + } +} + +function removeFile() { + const fileInput = document.getElementById("file-input"); + if (fileInput) fileInput.value = ""; + const removeBtn = document.getElementById("remove-file-btn"); + if (removeBtn) removeBtn.style.display = 'none'; + toggleInputMode(); +} + +function generateRandomPassword() { + const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+[]{}|;:,.<>?/~"; + const length = 30; + const password = Array.from({ length }, () => + charset.charAt(Math.floor(Math.random() * charset.length)) + ).join(""); + const passwordField = document.getElementById("generated-password"); + if (passwordField) passwordField.value = password; +} + +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); + }); +} + +function clearAll() { + const fields = ["input-text", "output-text", "file-input", "password"]; + fields.forEach(id => { + const el = document.getElementById(id); + if (el) el.value = ""; + }); + removeFile(); + toggleInputMode(); + document.getElementById("pacman-section")?.style.setProperty("display", "none"); + document.getElementById("encoding-section")?.style.setProperty("display", "block"); +} + +function checkForPacman() { + const val = document.getElementById("input-text").value.trim().toLowerCase(); + const pacSection = document.getElementById("pacman-section"); + const encSection = document.getElementById("encoding-section"); + + if (val.includes("pacman") && pacSection.style.display !== "block") { + pacSection.style.display = "block"; + encSection.style.display = "none"; + window.startPacman(); + } else if (pacSection.style.display === "block" && !val.includes("pacman")) { + window.exitGame(); + } +} + + +function startPacman() { } +function exitGame() { } diff --git a/templates/403.html b/templates/403.html index 7b5ca2b..7a34dee 100644 --- a/templates/403.html +++ b/templates/403.html @@ -1,42 +1,45 @@ - - - 403 - PacCrypt - - - + + + 403 - PacCrypt + + + + + -
-

PacCrypt

-

Secure Encoding, Encryption and Password Generation

-
+
+

PacCrypt

+

Secure Encoding, Encryption and Password Generation

+
-
-
-

403 - Forbidden

-

- Looks like this area is locked behind a secret ghost door! ๐Ÿ›ก๏ธ๐Ÿ‘ป -

+
+
+

๐Ÿšซ 403 - Forbidden

+

+ Looks like this area is locked behind a secret ghost door! ๐Ÿ›ก๏ธ๐Ÿ‘ป +

-
+
+ + +
+

© 2025 UnNaturalll-Dev. All rights reserved.

+ + GitHub Logo - -
-
- -
-

© 2025 UnNaturalll-Dev. All rights reserved.

- - GitHub Logo - -
+ diff --git a/templates/404.html b/templates/404.html index 2eb2f41..f22a761 100644 --- a/templates/404.html +++ b/templates/404.html @@ -1,42 +1,45 @@ - - - 404 - PacCrypt - - - + + + 404 - PacCrypt + + + + + -
-

PacCrypt

-

Secure Encoding, Encryption and Password Generation

-
+
+

PacCrypt

+

Secure Encoding, Encryption and Password Generation

+
-
+
+
+

โ“ 404 - Not Found

+

+ Whoops! That page doesnโ€™t seem to exist. Maybe it got encrypted? ๐Ÿงฉ๐Ÿ” +

-
-

404 - Page Not Found

-

Oops! The page you're looking for doesn't exist.

+ +
+
- - - -
- -
-

© 2025 UnNaturalll-Dev. All rights reserved.

- - GitHub Logo - -
+ diff --git a/templates/500.html b/templates/500.html index a2dc67c..7455a9c 100644 --- a/templates/500.html +++ b/templates/500.html @@ -1,42 +1,46 @@ - - - 500 - PacCrypt - - - + + + 500 - PacCrypt + + + + + -
-

PacCrypt

-

Secure Encoding, Encryption and Password Generation

-
+
+

PacCrypt

+

Secure Encoding, Encryption and Password Generation

+
-
-
-

500 - Server Error

-

- Uh oh! The ghosts chomped the server. ๐ŸงŸโ€โ™‚๏ธ -

+
+
+

๐Ÿ’ฅ 500 - Server Error

+

+ Uh oh! The ghosts chomped the server wires. ๐ŸงŸโ€โ™‚๏ธ๐Ÿ‘พ + Weโ€™re working on patching it up. +

-
+
+ + +
+

© 2025 UnNaturalll-Dev. All rights reserved.

+ + GitHub Logo - -
-
- -
-

© 2025 UnNaturalll-Dev. All rights reserved.

- - GitHub Logo - -
+ diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..9f6eb9c --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,155 @@ +๏ปฟ + + + + + Admin Panel - PacCrypt + + + + + + + +
+

PacCrypt Admin Panel

+

Site Overview & Controls

+
+ +
+ + +
+

๐Ÿ” 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 }}
  • +
  • ๐Ÿ“… Server Time: {{ server_info.time }}
  • +
  • ๐Ÿ Python Version: {{ server_info.python }}
  • +
  • โš™๏ธ Flask Debug Mode: {{ server_info.debug }}
  • +
+
+ + +
+

๐Ÿ“œ Server Logs

+ + + +
+ + +
+

๐Ÿงฉ System Configuration

+

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

+ + + +
+ +
+ + +
+

© 2025 UnNaturalll-Dev. All rights reserved.

+ + GitHub Logo + + + Sitemap Png + +
+ + + + + + diff --git a/templates/admin_login.html b/templates/admin_login.html new file mode 100644 index 0000000..d6c902e --- /dev/null +++ b/templates/admin_login.html @@ -0,0 +1,48 @@ +๏ปฟ + + + + + Admin Login - PacCrypt + + + + + + + +
+

PacCrypt Admin

+

Administrator Login

+
+ +
+
+

๐Ÿ”‘ Admin Login

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

{{ messages[0] }}

+ {% endif %} + {% endwith %} + +
+ + +
+ +
+
+
+
+ +
+

© 2025 UnNaturalll-Dev. All rights reserved.

+ + GitHub Logo + +
+ + + diff --git a/templates/admin_settings.html b/templates/admin_settings.html new file mode 100644 index 0000000..0d39c5e --- /dev/null +++ b/templates/admin_settings.html @@ -0,0 +1,62 @@ +๏ปฟ + + + + + Admin Settings - PacCrypt + + + + + + +
+

PacCrypt Admin Settings

+

Manage upload configuration securely

+
+ +
+
+

โš™๏ธ Upload Settings

+ + {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} + +
+ + + + + + + + + +
+ + + + +
+
+
+
+ + +
+

© 2025 UnNaturalll-Dev. All rights reserved.

+ + GitHub Logo + +
+ + + diff --git a/templates/admin_setup.html b/templates/admin_setup.html new file mode 100644 index 0000000..6fb0f7f --- /dev/null +++ b/templates/admin_setup.html @@ -0,0 +1,49 @@ +๏ปฟ + + + + + Admin Setup - PacCrypt + + + + + + + +
+

PacCrypt Admin

+

Secure Admin Setup

+
+ +
+
+

๐Ÿ›ก๏ธ Create Admin Account

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

{{ messages[0] }}

+ {% endif %} + {% endwith %} + +
+ + +
+ +
+
+
+
+ + +
+

© 2025 UnNaturalll-Dev. All rights reserved.

+ + GitHub Logo + +
+ + + diff --git a/templates/index.html b/templates/index.html index b733739..50a8a16 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,124 +1,146 @@ - - - - - - PacCrypt - - - - - - - - - - - - - -
-

PacCrypt

-

Secure Encoding, Encryption and Password Generation

-
- -
- - -
-

Password Generator

-
- -
- - -
-
Copied to Clipboard!
-
-
- - - - - -
-

Text Encoder / Decoder & File Encryption

-
- - - - - - -
- - -
- - -
- -
- - -
- -
- - - - - -
- -
-
- - -
- - - -
- - -
- - -
Copied to Clipboard!
-
- -
- - -
-

© 2025 UnNaturalll-Dev. All rights reserved.

- - GitHub Logo - -
- - - + + + + + + PacCrypt + + + + + + + +
+

PacCrypt

+

Encrypt and share your text or files securely

+
+ +
+ + +
+

๐Ÿ”‘ Password Generator

+
+ +
+ + +
+
Copied to clipboard!
+
+
+ + + + + + +
+

๐Ÿ” Encrypt & Decrypt

+
+ +
+ + +
+ +
+ Encrypt + + Decrypt +
+ +
+ +
+ +
+ +
+ + + +
+ + +
+ + +
+ +
+
Copied to clipboard!
+
+
+ + +
+

๐Ÿ“ค PacCrypt Sharing

+

Securely share a file with encryption and a pickup password.

+ + {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • + {{ message | safe }} + {% if "pickup" in message %} +
    + {{ message.split(" at ")[1] }} + + + {% endif %} +
  • + {% endfor %} +
+ + {% 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 + +
+ + + diff --git a/templates/pickup.html b/templates/pickup.html new file mode 100644 index 0000000..6115f0c --- /dev/null +++ b/templates/pickup.html @@ -0,0 +1,56 @@ +๏ปฟ + + + + + Pickup File - PacCrypt + + + + + + +
+

PacCrypt Pickup

+

Enter passwords to retrieve your file securely

+
+ +
+
+

๐Ÿ” Decrypt and Download

+ + {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} + +
+ + +
+ +
+
+
+ +
+

Link ID: {{ file_id }}

+
+
+ + + + + +