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 0000000..84ed908 Binary files /dev/null and b/static/img/Github_logo.png differ diff --git a/static/img/sitemap.png b/static/img/sitemap.png new file mode 100644 index 0000000..095fc38 Binary files /dev/null and b/static/img/sitemap.png differ diff --git a/static/js/encryption.js b/static/js/encryption.js new file mode 100644 index 0000000..73d088c --- /dev/null +++ b/static/js/encryption.js @@ -0,0 +1,86 @@ +// encryption.js + +/** + * Derives an AES-GCM key from a password using PBKDF2. + * @param {string} password - User-supplied password. + * @param {Uint8Array} salt - Randomly generated salt. + * @returns {Promise} + */ +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 }}

+
+
+ + + + + +