13 Commits

Author SHA1 Message Date
Tyler bb8690b74f UI Changes and better phone experience. 2025-05-14 05:00:12 -10:00
Tyler ed11ccd2a1 Update README.md 2025-05-04 13:30:40 -10:00
Tyler db00538aee Small changed
Updated README and Logo Change
2025-05-04 13:29:08 -10:00
Tyler 05a9ada8d9 Update README.md 2025-05-01 21:58:23 -10:00
Tyler 61193320d4 Actually working swipe controls for pacman game 2025-05-01 21:49:06 -10:00
Tyler 1c1fed1dd5 reverting swipe controls 2025-05-01 21:23:13 -10:00
Tyler 1edd1c858c Delete static/js/script.js 2025-05-01 21:20:15 -10:00
Tyler 1d55d4f4ce Swipe controls? 2025-05-01 21:14:34 -10:00
Tyler 7aefd5aff8 small fixes 2025-05-01 20:59:46 -10:00
Tyler 271b4cdc91 Delete restart.bat 2025-05-01 20:59:16 -10:00
Tyler 90dcb7ecb8 small things i forgot on my last push 2025-05-01 19:01:13 -10:00
Tyler 7ec213fad0 V .4.1 2025-05-01 18:46:29 -10:00
Tyler 766386501b Small fixes 2025-04-29 17:43:11 -10:00
20 changed files with 2753 additions and 2011 deletions
+113 -22
View File
@@ -1,9 +1,9 @@
# PacCrypt WebApp # PacCrypt
**PacCrypt** is a secure, feature-rich web app for encrypting and decrypting text and files — built with 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! 🕹️ Now with an admin control panel, GitHub updater, and a built-in Pac-Man easter egg! 🕹️
Live demo: [paccrypt.unnaturalll.dev](http://paccrypt.unnaturalll.dev) Officially Hosted Here: [paccrypt.unnaturalll.dev](http://paccrypt.unnaturalll.dev)
--- ---
@@ -35,8 +35,9 @@ Live demo: [paccrypt.unnaturalll.dev](http://paccrypt.unnaturalll.dev)
- Flask 3+ - Flask 3+
- Cryptography 42+ - Cryptography 42+
- Waitress 2.1+ - Waitress 2.1+
- Git (for update feature) - Git (For update feature)
- Nginx (recommended) - Nginx (Recommended)
- Cockpit (Recommended if hosted on **Linux**)
--- ---
@@ -54,45 +55,49 @@ Then run:
- Development Mode: - Development Mode:
```bash ```bash
./start_dev.sh # or start_dev.bat ./start_dev.sh #<-- start_dev.bat (Windows)
``` ```
- Production Mode: - Production Mode:
```bash ```bash
./start_prod.sh # or start_prod.bat ./start_prod.sh #<-- start_prod.bat (Windows)
``` ```
Visit [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) or [http://localhost:5000](http://localhost:5000) - *If* you **are** on the host system
Visit http://hosts_private_ip - *If* you are **not** on the host system
--- ---
## 🧭 Navigation & Usage ## 🧭 Navigation & Usage
### 🔑 Generate Passwords
- Click Generate
- Then hit `📋 Copy Password`
- **Note:** This is also used as a seed generator for the Pac-Man *like* game
### 🔐 Encrypt & Decrypt ### 🔐 Encrypt & Decrypt
- Choose between Basic Cipher or Advanced AES - Choose between Basic Cipher or Advanced AES
- Type your message or upload a file
- Enter password (if AES)
- Select mode using toggle (Encrypt/Decrypt) - Select mode using toggle (Encrypt/Decrypt)
- Type your message or upload a file
- Enter password (Advanced AES)
- Hit Execute - Hit Execute
- Then hit `📋 Copy Output`
### 📤 Share Files ### 📤 Share Files
- Upload a file with two passwords: - Upload a file with two passwords:
- Encryption password - Encryption password
- Pickup password - Pickup password
- Get a shareable URL and click 📋 Copy Link - Get a shareable URL and click `📋 Copy Link`
### 🔑 Generate Passwords ### 🎮 Pac-Man *like* Game
- Click Generate
- Then hit 📋 Copy
### 🎮 Pac-Man Game
- Type `pacman` in the input box - Type `pacman` in the input box
- Game appears with Restart/Exit controls - Game appears with `Restart` and `Exit` buttons
- Classic arrow key controls 🕹️ - Arrow key and Swipe controls 🕹️
- Game restarts and a new seed is generated once all dots are eaten
--- ---
@@ -111,31 +116,117 @@ Features:
--- ---
## 🛡️ Deployment Tips ## 🛡️ Deployment Tips
##### I recommend using Linux as the host server, the follow confs are Linux focused
The official PacCrypt host is **Debian** minimal install.
Minimal Nginx config: **HTTP** Nginx config (Not recommended):
```nginx ```nginx
server { server {
listen 80; listen 80;
server_name yourdomain.com; server_name yourdomain.com; #<-- Your URL here
# Basic Privacy-Respecting Logging
access_log off; #<-- set to syslog:server=unix:/dev/log; for logging
error_log syslog:server=unix:/dev/log crit; #<-- Currently set for only critical logs, remove crit for all logs
# Hardened Proxy Settings
location / { location / {
proxy_pass http://127.0.0.1:5000; proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Connection "";
# Timeouts
proxy_connect_timeout 5s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
} }
# Basic Hardening Headers
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer" always;
add_header Permissions-Policy "geolocation=(), microphone=()" always;
# Prevent Abuse
client_max_body_size 10M;
keepalive_timeout 10;
server_tokens off;
} }
``` ```
Use Let's Encrypt to add SSL/TLS support. **HTTPS** Nginx config (Recommended):
```nginx
# Redirect HTTP to HTTPS
server {
listen 80;
server_name yourdomain.com; #<-- Your URL here
# Basic Privacy-Respecting Logging
access_log off; #<-- set to syslog:server=unix:/dev/log; for logging
error_log syslog:server=unix:/dev/log crit; #<-- Currently set for only critical logs, remove crit for all logs
location / {
return 301 https://$host$request_uri;
}
}
# HTTPS Server Block
server {
listen 443 ssl http2;
server_name yourdomain.com;
ssl_certificate path/to/yourdomain.com.cert; #<-- Could also be .cert.pem
ssl_certificate_key path/to/yourdomain.com.key; #<-- Could also be .key.pem
# SSL Hardening
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Strong security headers (adjust as needed)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Referrer-Policy "no-referrer" always;
add_header Permissions-Policy "geolocation=(), camera=()" always;
add_header X-XSS-Protection "1; mode=block" always;
# Basic Privacy-Respecting Logging
access_log off; #<-- set to syslog:server=unix:/dev/log; for logging
error_log syslog:server=unix:/dev/log crit; #<-- Currently set for only critical logs, remove crit for all logs
client_max_body_size xG; #<-- Change to what the max upload for PacCrypt Share
# Reverse proxy to Flask
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
# Comment these out if you want complete anonymity between client and app
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# Optional privacy: strip identifying headers
proxy_hide_header X-Powered-By;
}
}
```
--- ---
## 🗂️ Project Structure ## 🗂️ Project Structure
``` ```
paccrypt-webapp-final/ PacCrypt/
├── app.py ├── app.py
├── requirements.txt ├── requirements.txt
├── README.md ├── README.md
+371 -180
View File
@@ -1,3 +1,4 @@
# ===== Standard Library Imports =====
import os import os
import io import io
import json import json
@@ -5,11 +6,13 @@ import html
import base64 import base64
import hashlib import hashlib
import secrets import secrets
import shutil
import datetime import datetime
import subprocess import subprocess
import platform import platform
from datetime import UTC
import sys
# ===== Third-Party Imports =====
from flask import ( from flask import (
Flask, render_template, request, jsonify, session, Flask, render_template, request, jsonify, session,
redirect, url_for, flash, send_file redirect, url_for, flash, send_file
@@ -20,9 +23,11 @@ from cryptography.hazmat.primitives.hashes import SHA256
from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
# ===== Application Configuration =====
app = Flask(__name__) app = Flask(__name__)
app.secret_key = os.getenv("FLASK_SECRET", os.urandom(24)) app.secret_key = os.getenv("FLASK_SECRET", os.urandom(24))
# ===== Constants =====
ADMIN_CRED_FILE = 'admin_creds.json' ADMIN_CRED_FILE = 'admin_creds.json'
ADMIN_KEY_FILE = 'admin_key.key' ADMIN_KEY_FILE = 'admin_key.key'
ADMIN_LOG_FILE = 'admin_logs.enc' ADMIN_LOG_FILE = 'admin_logs.enc'
@@ -35,8 +40,9 @@ DEFAULT_SETTINGS = {
"max_file_size_bytes": 25 * 1024 * 1024 * 1024 # 25GB "max_file_size_bytes": 25 * 1024 * 1024 * 1024 # 25GB
} }
# === Settings === # ===== Settings Management =====
def load_settings(): def load_settings():
"""Load application settings from file or create with defaults."""
if not os.path.exists(SETTINGS_FILE): if not os.path.exists(SETTINGS_FILE):
with open(SETTINGS_FILE, 'w') as f: with open(SETTINGS_FILE, 'w') as f:
json.dump(DEFAULT_SETTINGS, f) json.dump(DEFAULT_SETTINGS, f)
@@ -48,23 +54,36 @@ UPLOAD_FOLDER = settings["upload_folder"]
MAX_FILE_AGE_DAYS = settings["max_file_age_days"] MAX_FILE_AGE_DAYS = settings["max_file_age_days"]
MAX_FILE_SIZE_BYTES = settings["max_file_size_bytes"] MAX_FILE_SIZE_BYTES = settings["max_file_size_bytes"]
# Ensure upload folder exists and has proper permissions
if not os.path.exists(UPLOAD_FOLDER): if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER) try:
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
# Set permissions to 755 (rwxr-xr-x)
os.chmod(UPLOAD_FOLDER, 0o755)
print(f"[INFO] Created upload directory: {UPLOAD_FOLDER}")
except Exception as e:
print(f"[ERROR] Failed to create upload directory: {str(e)}")
raise
# === Crypto === # ===== Cryptographic Functions =====
def derive_key(password: str, salt: bytes) -> bytes: def derive_key(password: str, salt: bytes) -> bytes:
"""Derive a cryptographic key from password using PBKDF2."""
return PBKDF2HMAC(algorithm=SHA256(), length=32, salt=salt, iterations=200_000).derive(password.encode()) return PBKDF2HMAC(algorithm=SHA256(), length=32, salt=salt, iterations=200_000).derive(password.encode())
def hash_password(password: str, salt: bytes) -> str: def hash_password(password: str, salt: bytes) -> str:
"""Hash a password with salt for secure storage."""
return base64.urlsafe_b64encode(derive_key(password, salt)).decode() return base64.urlsafe_b64encode(derive_key(password, salt)).decode()
def simple_encode(text: str) -> str: def simple_encode(text: str) -> str:
"""Basic Caesar cipher encryption."""
return ''.join(ALPHABET[(ALPHABET.index(c) + 3) % 26] if c in ALPHABET else c for c in text.lower()) 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: def simple_decode(text: str) -> str:
"""Basic Caesar cipher decryption."""
return ''.join(ALPHABET[(ALPHABET.index(c) - 3) % 26] if c in ALPHABET else c for c in text.lower()) 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: def advanced_encrypt(plaintext: str, password: str) -> str:
"""Encrypt text using AES-GCM with password-derived key."""
salt = os.urandom(16) salt = os.urandom(16)
key = derive_key(password, salt) key = derive_key(password, salt)
nonce = os.urandom(12) nonce = os.urandom(12)
@@ -72,6 +91,7 @@ def advanced_encrypt(plaintext: str, password: str) -> str:
return base64.urlsafe_b64encode(salt + nonce + ct).decode() return base64.urlsafe_b64encode(salt + nonce + ct).decode()
def advanced_decrypt(token_b64: str, password: str) -> str: def advanced_decrypt(token_b64: str, password: str) -> str:
"""Decrypt text using AES-GCM with password-derived key."""
try: try:
data = base64.urlsafe_b64decode(token_b64.encode()) data = base64.urlsafe_b64decode(token_b64.encode())
salt, nonce, ct = data[:16], data[16:28], data[28:] salt, nonce, ct = data[:16], data[16:28], data[28:]
@@ -80,8 +100,9 @@ def advanced_decrypt(token_b64: str, password: str) -> str:
except Exception: except Exception:
return "[Error] Invalid password or corrupted data!" return "[Error] Invalid password or corrupted data!"
# === Admin Auth === # ===== Admin Authentication =====
def load_admin_key(): def load_admin_key():
"""Load or generate admin encryption key."""
if not os.path.exists(ADMIN_KEY_FILE): if not os.path.exists(ADMIN_KEY_FILE):
with open(ADMIN_KEY_FILE, 'wb') as f: with open(ADMIN_KEY_FILE, 'wb') as f:
f.write(Fernet.generate_key()) f.write(Fernet.generate_key())
@@ -89,6 +110,7 @@ def load_admin_key():
return f.read() return f.read()
def encrypt_creds(username, password): def encrypt_creds(username, password):
"""Encrypt and store admin credentials."""
key = load_admin_key() key = load_admin_key()
cipher = Fernet(key) cipher = Fernet(key)
salt = os.urandom(16) salt = os.urandom(16)
@@ -98,6 +120,7 @@ def encrypt_creds(username, password):
f.write(cipher.encrypt(data)) f.write(cipher.encrypt(data))
def check_creds(username, password): def check_creds(username, password):
"""Verify admin credentials."""
try: try:
key = load_admin_key() key = load_admin_key()
cipher = Fernet(key) cipher = Fernet(key)
@@ -111,82 +134,107 @@ def check_creds(username, password):
return False return False
def log_admin_event(message: str): def log_admin_event(message: str):
"""Log admin actions securely."""
try: try:
key = load_admin_key() key = load_admin_key()
cipher = Fernet(key) cipher = Fernet(key)
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") timestamp = datetime.datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S")
encrypted = cipher.encrypt(f"[{timestamp}] {message}".encode()) encrypted = cipher.encrypt(f"[{timestamp}] {message}".encode())
with open(ADMIN_LOG_FILE, 'ab') as f: with open(ADMIN_LOG_FILE, 'ab') as f:
f.write(encrypted + b"\n") f.write(encrypted + b"\n")
except Exception as e: except Exception as e:
print("[ERROR] Failed to write admin log:", e) print("[ERROR] Failed to write admin log:", e)
# === Text Encryption Route === # ===== File Management =====
def cleanup_expired_files():
"""Remove files older than MAX_FILE_AGE_DAYS."""
try:
now = datetime.datetime.now(UTC)
for fname in os.listdir(UPLOAD_FOLDER):
if fname.endswith(".enc") or fname.endswith(".json"):
path = os.path.join(UPLOAD_FOLDER, fname)
try:
file_time = datetime.datetime.fromtimestamp(os.path.getmtime(path), UTC)
age = (now - file_time).days
if age > MAX_FILE_AGE_DAYS:
os.remove(path)
print(f"[INFO] Deleted expired file: {fname}")
except Exception as e:
print(f"[ERROR] Could not check/delete file {fname}: {e}")
except Exception as e:
print(f"[ERROR] Failed to cleanup expired files: {str(e)}")
# ===== Route Handlers =====
@app.route("/", methods=["GET", "POST"]) @app.route("/", methods=["GET", "POST"])
def index(): def index():
"""Main application route handling encryption/decryption and file uploads."""
if request.method == 'POST': if request.method == 'POST':
if 'file' in request.files: # <-- Handling file upload if 'file' in request.files:
file = request.files['file'] return handle_file_upload(request)
enc_password = request.form.get('enc_password') else:
pickup_password = request.form.get('pickup_password') return handle_text_operation(request)
if not file or not enc_password or not pickup_password:
flash('Missing fields')
return redirect(url_for('index'))
if file.content_length and file.content_length > MAX_FILE_SIZE_BYTES:
flash(f"File too large! Limit: {MAX_FILE_SIZE_BYTES / (1024**3):.2f} GB")
return redirect(url_for('index'))
filename = secure_filename(file.filename)
temp_path = os.path.join(UPLOAD_FOLDER, filename)
file.save(temp_path)
with open(temp_path, 'rb') as f:
data = f.read()
salt = os.urandom(16)
key = derive_key(enc_password, salt)
nonce = os.urandom(12)
ct = AESGCM(key).encrypt(nonce, data, None)
random_id = secrets.token_urlsafe(24)
with open(os.path.join(UPLOAD_FOLDER, f"{random_id}.enc"), 'wb') as f:
f.write(salt + nonce + ct)
os.remove(temp_path)
meta = {
'pickup_password': base64.urlsafe_b64encode(hashlib.sha256(pickup_password.encode()).digest()).decode(),
'original_name': filename,
'timestamp': datetime.datetime.utcnow().isoformat()
}
with open(os.path.join(UPLOAD_FOLDER, f"{random_id}.json"), 'w') as f:
json.dump(meta, f)
pickup_url = request.host_url.rstrip('/') + url_for('pickup_file', file_id=random_id)
flash(pickup_url)
return redirect(url_for('index'))
else: # <-- Handling encryption/decryption
data = request.get_json()
encryption_type = data.get("encryption-type", "basic")
operation = data.get("operation", "")
message = data.get("message", "")
password = data.get("password", "")
if encryption_type == "basic":
result = simple_encode(message) if operation == "encrypt" else simple_decode(message)
else:
result = advanced_encrypt(message, password) if operation == "encrypt" else advanced_decrypt(message, password)
return jsonify(result=html.escape(result))
return render_template("index.html", result="", password="", encryption_type="advanced", settings=settings) return render_template("index.html", result="", password="", encryption_type="advanced", settings=settings)
# === File Pickup Route === def handle_file_upload(request):
"""Process file upload and encryption."""
file = request.files['file']
enc_password = request.form.get('enc_password')
pickup_password = request.form.get('pickup_password')
if not file or not enc_password or not pickup_password:
return jsonify({"error": "Missing fields"}), 400
if file.content_length and file.content_length > MAX_FILE_SIZE_BYTES:
return jsonify({"error": f"File too large! Limit: {MAX_FILE_SIZE_BYTES / (1024**3):.2f} GB"}), 400
filename = secure_filename(file.filename)
temp_path = os.path.join(UPLOAD_FOLDER, filename)
file.save(temp_path)
with open(temp_path, 'rb') as f:
data = f.read()
salt = os.urandom(16)
key = derive_key(enc_password, salt)
nonce = os.urandom(12)
ct = AESGCM(key).encrypt(nonce, data, None)
random_id = secrets.token_urlsafe(24)
with open(os.path.join(UPLOAD_FOLDER, f"{random_id}.enc"), 'wb') as f:
f.write(salt + nonce + ct)
os.remove(temp_path)
meta = {
'pickup_password': base64.urlsafe_b64encode(hashlib.sha256(pickup_password.encode()).digest()).decode(),
'original_name': filename,
'timestamp': datetime.datetime.now(UTC).isoformat()
}
with open(os.path.join(UPLOAD_FOLDER, f"{random_id}.json"), 'w') as f:
json.dump(meta, f)
pickup_url = request.host_url.rstrip('/') + url_for('pickup_file', file_id=random_id)
return jsonify({"success": True, "pickup_url": pickup_url})
def handle_text_operation(request):
"""Process text encryption/decryption operations."""
data = request.get_json()
encryption_type = data.get("encryption-type", "basic")
operation = data.get("operation", "")
message = data.get("message", "")
password = data.get("password", "")
if encryption_type == "basic":
result = simple_encode(message) if operation == "encrypt" else simple_decode(message)
else:
result = advanced_encrypt(message, password) if operation == "encrypt" else advanced_decrypt(message, password)
return jsonify(result=html.escape(result))
# ===== File Pickup Route =====
@app.route("/pickup/<file_id>", methods=["GET", "POST"]) @app.route("/pickup/<file_id>", methods=["GET", "POST"])
def pickup_file(file_id): def pickup_file(file_id):
"""Handle file pickup and decryption."""
meta_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.json") meta_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.json")
enc_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.enc") enc_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.enc")
@@ -195,59 +243,61 @@ def pickup_file(file_id):
return redirect(url_for('index')) return redirect(url_for('index'))
if request.method == 'POST': if request.method == 'POST':
pickup_password = request.form.get('pickup_password') return handle_file_pickup(request, meta_path, enc_path, file_id)
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) return render_template("pickup.html", file_id=file_id)
def cleanup_expired_files(): def handle_file_pickup(request, meta_path, enc_path, file_id):
now = datetime.datetime.utcnow() """Process file pickup and decryption."""
pickup_password = request.form.get('pickup_password')
enc_password = request.form.get('enc_password')
for fname in os.listdir(UPLOAD_FOLDER): if not pickup_password or not enc_password:
if fname.endswith(".enc") or fname.endswith(".json"): flash("Missing fields")
path = os.path.join(UPLOAD_FOLDER, fname) return redirect(request.url)
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}")
with open(meta_path, 'r') as f:
meta = json.load(f)
# === Admin Log Viewer === expected_hash = base64.urlsafe_b64encode(hashlib.sha256(pickup_password.encode()).digest()).decode()
if expected_hash != meta['pickup_password']:
flash("Incorrect pickup password")
return redirect(request.url)
with open(enc_path, 'rb') as f:
enc_data = f.read()
salt, nonce, ct = enc_data[:16], enc_data[16:28], enc_data[28:]
key = derive_key(enc_password, salt)
try:
decrypted = AESGCM(key).decrypt(nonce, ct, None)
except Exception:
flash("Decryption failed")
return redirect(request.url)
os.remove(meta_path)
os.remove(enc_path)
log_admin_event(f"File {file_id} downloaded and deleted.")
response = send_file(
io.BytesIO(decrypted),
as_attachment=True,
download_name=meta['original_name'],
mimetype='application/octet-stream'
)
# Add headers for better mobile compatibility
response.headers['Content-Disposition'] = f'attachment; filename="{meta["original_name"]}"'
response.headers['Content-Type'] = 'application/octet-stream'
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '0'
return response
# ===== Admin Routes =====
@app.route("/admin-logs") @app.route("/admin-logs")
def admin_logs(): def admin_logs():
"""View admin activity logs."""
if not session.get("admin_logged_in"): if not session.get("admin_logged_in"):
return redirect(url_for("admin_login")) return redirect(url_for("admin_login"))
@@ -270,47 +320,50 @@ def admin_logs():
return jsonify(logs=logs) return jsonify(logs=logs)
# === Admin Settings Editor ===
@app.route("/admin-settings", methods=["GET", "POST"]) @app.route("/admin-settings", methods=["GET", "POST"])
def admin_settings(): def admin_settings():
"""Manage application settings."""
if not session.get("admin_logged_in"): if not session.get("admin_logged_in"):
return redirect(url_for("admin_login")) return redirect(url_for("admin_login"))
current_settings = load_settings() current_settings = load_settings()
if request.method == 'POST': if request.method == 'POST':
upload_folder = request.form.get('upload_folder', current_settings.get('upload_folder', 'uploads')) return handle_settings_update(request, current_settings)
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) return render_template("admin_settings.html", settings=current_settings)
# === Admin Setup === def handle_settings_update(request, current_settings):
"""Process settings update request."""
upload_folder = request.form.get('upload_folder', current_settings.get('upload_folder', 'uploads'))
max_file_age_days = int(request.form.get('max_file_age_days', current_settings.get('max_file_age_days', 14)))
max_file_size_gb = float(request.form.get('max_file_size_gb', current_settings.get('max_file_size_bytes', 25 * 1024 * 1024 * 1024) / (1024 * 1024 * 1024)))
max_file_size_bytes = int(max_file_size_gb * 1024 * 1024 * 1024)
updated_settings = {
"upload_folder": upload_folder,
"max_file_age_days": max_file_age_days,
"max_file_size_bytes": max_file_size_bytes
}
with open(SETTINGS_FILE, 'w') as f:
json.dump(updated_settings, f)
flash("Settings updated successfully!")
global settings, UPLOAD_FOLDER, MAX_FILE_AGE_DAYS, MAX_FILE_SIZE_BYTES
settings = load_settings()
UPLOAD_FOLDER = settings.get('upload_folder', 'uploads')
MAX_FILE_AGE_DAYS = settings.get('max_file_age_days', 14)
MAX_FILE_SIZE_BYTES = settings.get('max_file_size_bytes', 25 * 1024 * 1024 * 1024)
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
return redirect(url_for("admin_settings"))
@app.route("/admin-setup", methods=["GET", "POST"]) @app.route("/admin-setup", methods=["GET", "POST"])
def admin_setup(): def admin_setup():
"""Initial admin account setup."""
if os.path.exists(ADMIN_CRED_FILE): if os.path.exists(ADMIN_CRED_FILE):
return redirect(url_for("admin_login")) return redirect(url_for("admin_login"))
if request.method == "POST": if request.method == "POST":
@@ -323,9 +376,9 @@ def admin_setup():
flash("Both fields required") flash("Both fields required")
return render_template("admin_setup.html") return render_template("admin_setup.html")
# === Admin Login ===
@app.route("/admin-login", methods=["GET", "POST"]) @app.route("/admin-login", methods=["GET", "POST"])
def admin_login(): def admin_login():
"""Admin login handler."""
if request.method == "POST": if request.method == "POST":
u = request.form.get("username") u = request.form.get("username")
p = request.form.get("password") p = request.form.get("password")
@@ -338,15 +391,15 @@ def admin_login():
flash("Incorrect credentials") flash("Incorrect credentials")
return render_template("admin_login.html") return render_template("admin_login.html")
# === Admin Logout ===
@app.route("/admin-logout") @app.route("/admin-logout")
def admin_logout(): def admin_logout():
"""Admin logout handler."""
session.pop("admin_logged_in", None) session.pop("admin_logged_in", None)
return redirect(url_for("index")) return redirect(url_for("index"))
# === Admin Page ===
@app.route("/adminpage") @app.route("/adminpage")
def admin_page(): def admin_page():
"""Admin dashboard."""
if not session.get("admin_logged_in"): if not session.get("admin_logged_in"):
if not os.path.exists(ADMIN_CRED_FILE): if not os.path.exists(ADMIN_CRED_FILE):
return redirect(url_for("admin_setup")) return redirect(url_for("admin_setup"))
@@ -355,32 +408,102 @@ def admin_page():
cleanup_expired_files() cleanup_expired_files()
routes = [rule.rule for rule in app.url_map.iter_rules() if rule.endpoint != 'static'] routes = [rule.rule for rule in app.url_map.iter_rules() if rule.endpoint != 'static']
try: # Get uptime based on OS
uptime = subprocess.check_output("uptime -p", shell=True).decode().strip() if platform.system() == "Windows":
except Exception: try:
uptime = "Unavailable" # Windows uptime using PowerShell
ps_command = "(Get-CimInstance -ClassName Win32_OperatingSystem).LastBootUpTime"
uptime_output = subprocess.check_output(["powershell", "-Command", ps_command], shell=True).decode()
# Convert the PowerShell DateTime to Python datetime
boot_time = datetime.datetime.strptime(uptime_output.strip(), "%A, %B %d, %Y %I:%M:%S %p")
# Make boot_time timezone-aware (assuming local time)
boot_time = boot_time.replace(tzinfo=datetime.timezone.utc)
current_time = datetime.datetime.now(UTC)
uptime = current_time - boot_time
uptime_str = f"{uptime.days} days, {uptime.seconds // 3600} hours, {(uptime.seconds % 3600) // 60} minutes"
except Exception as e:
print(f"[ERROR] Failed to get Windows uptime: {str(e)}")
uptime_str = "Unavailable"
else:
try:
# Try reading from /proc/uptime first
with open('/proc/uptime', 'r') as f:
uptime_seconds = float(f.readline().split()[0])
days = int(uptime_seconds // 86400)
hours = int((uptime_seconds % 86400) // 3600)
minutes = int((uptime_seconds % 3600) // 60)
uptime_str = f"{days} days, {hours} hours, {minutes} minutes"
except Exception:
try:
# Fallback to uptime command if /proc/uptime fails
uptime_str = subprocess.check_output("uptime -p", shell=True).decode().strip()
except Exception as e:
print(f"[ERROR] Failed to get Linux uptime: {str(e)}")
uptime_str = "Unavailable"
server_info = { server_info = {
"uptime": uptime, "uptime": uptime_str,
"time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "time": datetime.datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S"),
"python": platform.python_version(), "python": platform.python_version(),
"debug": app.debug "debug": app.debug
} }
return render_template("admin.html", routes=routes, server_info=server_info) return render_template("admin.html", routes=routes, server_info=server_info)
# === Restart Server === @app.route("/restart-server", methods=["POST"])
@app.route("/restart-server")
def restart_server(): def restart_server():
"""Restart the server."""
if not session.get("admin_logged_in"): if not session.get("admin_logged_in"):
return redirect(url_for("admin_login")) return jsonify({"error": "Unauthorized"}), 401
subprocess.Popen(["sudo", "systemctl", "restart", "paccrypt.service"])
flash("Restart triggered") try:
return redirect(url_for("admin_page")) if platform.system() == "Windows":
# Get the current process ID
current_pid = os.getpid()
# Create a batch file to restart the server
restart_script = f"""
@echo off
timeout /t 2 /nobreak
taskkill /F /PID {current_pid}
set PRODUCTION=true
start "" "python" "app.py"
"""
with open("restart.bat", "w") as f:
f.write(restart_script)
# Start the restart script and exit
subprocess.Popen(["restart.bat"], shell=True)
return jsonify({"message": "Server restart initiated"}), 200
else:
# For Linux/Unix systems, use a Python-based restart
# Get the current Python interpreter and script path
python_path = sys.executable
script_path = os.path.abspath(__file__)
current_pid = os.getpid()
# Create a shell script to restart the server
restart_script = f"""#!/bin/bash
sleep 2
kill -9 {current_pid}
export PRODUCTION=true
{python_path} {script_path}
"""
# Write and make the script executable
with open("restart.sh", "w") as f:
f.write(restart_script)
os.chmod("restart.sh", 0o755)
# Start the restart script and exit
subprocess.Popen(["./restart.sh"], shell=True)
return jsonify({"message": "Server restart initiated"}), 200
except Exception as e:
print(f"[ERROR] Failed to restart server: {str(e)}")
return jsonify({"error": f"Failed to restart server: {str(e)}"}), 500
# === Reset Admin Credentials ===
@app.route("/admin-reset", methods=["POST"]) @app.route("/admin-reset", methods=["POST"])
def admin_reset(): def admin_reset():
"""Reset admin credentials."""
if not session.get("admin_logged_in"): if not session.get("admin_logged_in"):
return redirect(url_for("admin_login")) return redirect(url_for("admin_login"))
try: try:
@@ -395,9 +518,9 @@ def admin_reset():
print("[ERROR] admin_reset failed:", e) print("[ERROR] admin_reset failed:", e)
return redirect(url_for("admin_setup")) return redirect(url_for("admin_setup"))
# === Change Admin Password ===
@app.route("/admin-change-password", methods=["POST"]) @app.route("/admin-change-password", methods=["POST"])
def admin_change_password(): def admin_change_password():
"""Change admin password."""
if not session.get("admin_logged_in"): if not session.get("admin_logged_in"):
return redirect(url_for("admin_login")) return redirect(url_for("admin_login"))
@@ -432,6 +555,7 @@ def admin_change_password():
@app.route("/admin-clear-uploads", methods=["POST"]) @app.route("/admin-clear-uploads", methods=["POST"])
def admin_clear_uploads(): def admin_clear_uploads():
"""Clear all uploaded files."""
if not session.get("admin_logged_in"): if not session.get("admin_logged_in"):
return redirect(url_for("admin_login")) return redirect(url_for("admin_login"))
@@ -449,31 +573,102 @@ def admin_clear_uploads():
@app.route("/admin-update-server", methods=["POST"]) @app.route("/admin-update-server", methods=["POST"])
def admin_update_server(): def admin_update_server():
"""Update server from GitHub repository."""
if not session.get("admin_logged_in"): if not session.get("admin_logged_in"):
return redirect(url_for("admin_login")) return jsonify({"error": "Unauthorized"}), 401
try: try:
output = subprocess.check_output(["git", "pull", "origin", "main"], cwd=os.getcwd()).decode() # Get the absolute path of the current directory
flash("✅ Server updated successfully!<br><pre>" + html.escape(output) + "</pre>", "update") current_dir = os.path.abspath(os.path.dirname(__file__))
except subprocess.CalledProcessError as e:
flash("❌ Failed to update server:<br><pre>" + html.escape(e.output.decode()) + "</pre>", "update")
except Exception as ex:
flash("❌ An error occurred: " + str(ex), "update")
return redirect(url_for("admin_page")) # Try to find git executable
git_paths = [
"/usr/bin/git", # Standard Debian path
"/usr/local/bin/git",
"/bin/git",
"git" # Fallback to PATH
]
@app.route("/sitemap") git_cmd = None
for path in git_paths:
if os.path.exists(path) or path == "git":
try:
# Test if git is executable
subprocess.run([path, "--version"], check=True, capture_output=True)
git_cmd = path
break
except Exception:
continue
if not git_cmd:
return jsonify({"error": "Git executable not found. Please ensure git is installed and accessible."}), 500
# Try to find the git repository by checking parent directories
repo_dir = current_dir
max_depth = 5 # Limit how far up we'll look
found_git = False
for _ in range(max_depth):
git_dir = os.path.join(repo_dir, ".git")
if os.path.exists(git_dir):
found_git = True
break
parent_dir = os.path.dirname(repo_dir)
if parent_dir == repo_dir: # We've reached the root directory
break
repo_dir = parent_dir
if not found_git:
return jsonify({
"error": "Git repository not found. Current directory: " + current_dir,
"details": "Please ensure the application is running from within the git repository directory."
}), 400
# Execute git commands with proper error handling
try:
# Fetch latest changes
fetch_result = subprocess.run([git_cmd, "fetch"], cwd=repo_dir, check=True, capture_output=True, text=True)
# Reset to origin/main
reset_result = subprocess.run([git_cmd, "reset", "--hard", "origin/main"], cwd=repo_dir, check=True, capture_output=True, text=True)
# Pull latest changes
pull_result = subprocess.run([git_cmd, "pull"], cwd=repo_dir, check=True, capture_output=True, text=True)
return jsonify({
"message": "Server updated successfully from GitHub!",
"details": {
"fetch": fetch_result.stdout,
"reset": reset_result.stdout,
"pull": pull_result.stdout
}
}), 200
except subprocess.CalledProcessError as e:
error_msg = f"Git operation failed: {e.stderr if e.stderr else e.stdout}"
print(f"[ERROR] {error_msg}")
return jsonify({"error": error_msg}), 500
except Exception as e:
error_msg = f"Update failed: {str(e)}"
print(f"[ERROR] {error_msg}")
return jsonify({"error": error_msg}), 500
# ===== Sitemap and Robots =====
@app.route("/sitemap", methods=["GET"])
def sitemap(): def sitemap():
output = ["<h1>PacCrypt Sitemap</h1>", "<ul>"] """Generate sitemap.xml."""
for rule in app.url_map.iter_rules(): sitemap_xml = '''<?xml version="1.0" encoding="UTF-8"?>
if rule.endpoint != 'static': <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
url = url_for(rule.endpoint, **{arg: f"<{arg}>" for arg in rule.arguments}) <url><loc>https://paccrypt.unnaturalll.dev/</loc></url>
output.append(f"<li><a href='{url}'>{url}</a></li>") <url><loc>https://paccrypt.unnaturalll.dev/pickup</loc></url>
output.append("</ul>") <url><loc>https://paccrypt.unnaturalll.dev/adminpage</loc></url>
return "\n".join(output) <url><loc>https://paccrypt.unnaturalll.dev/sitemap</loc></url>
</urlset>'''
return sitemap_xml, 200, {'Content-Type': 'application/xml'}
@app.route("/robots.txt") @app.route("/robots.txt")
def robots_txt(): def robots_txt():
"""Generate robots.txt."""
lines = [ lines = [
"User-agent: *", "User-agent: *",
"Disallow: /adminpage", "Disallow: /adminpage",
@@ -489,10 +684,7 @@ def robots_txt():
] ]
return "\n".join(lines), 200, {"Content-Type": "text/plain"} return "\n".join(lines), 200, {"Content-Type": "text/plain"}
# ===== Error Handlers =====
# === Error Handlers ===
@app.errorhandler(404) @app.errorhandler(404)
def page_not_found(e): def page_not_found(e):
return render_template('404.html'), 404 return render_template('404.html'), 404
@@ -514,10 +706,9 @@ def handle_file_not_found(e):
if os.getenv("PRODUCTION", "false").lower() == "true": if os.getenv("PRODUCTION", "false").lower() == "true":
return render_template('500.html'), 500 return render_template('500.html'), 500
else: else:
raise e # re-raise for debugging in development raise e
# ===== Application Entry Point =====
# === Server Mode Execution ===
if __name__ == "__main__": if __name__ == "__main__":
PRODUCTION = os.getenv("PRODUCTION", "false").lower() == "true" PRODUCTION = os.getenv("PRODUCTION", "false").lower() == "true"
if PRODUCTION: if PRODUCTION:
+1
View File
@@ -3,6 +3,7 @@
flask==3.0.3 flask==3.0.3
cryptography==42.0.5 cryptography==42.0.5
waitress==2.1.2 waitress==2.1.2
werkzeug==3.0.1
# nginx - Only needed for Nginx integration, not installed via pip # nginx - Only needed for Nginx integration, not installed via pip
# Run pip install -r requirements.txt # Run pip install -r requirements.txt
+540 -127
View File
@@ -1,15 +1,17 @@
/* ===== Global Reset ===== */ /* ===== Global Reset ===== */
* { * {
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 3px;
padding: 0; padding: 0;
} }
/* ===== Body ===== */ /* ===== Body ===== */
body { body {
font-family: 'Poppins', sans-serif; font-family: 'Press Start 2P', monospace;
background-color: #121212; background-color: #0e0e0e;
color: #00ff99; color: #28E060;
font-size: 13px;
line-height: 1.6;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100vh; min-height: 100vh;
@@ -18,27 +20,111 @@ body {
padding: 20px; padding: 20px;
} }
/* ===== Header ===== */ @media (max-width: 600px) {
header { body {
text-align: center; font-size: 11px;
padding: 25px; padding: 10px;
background-color: #1c1c1c; }
border-radius: 12px;
box-shadow: 0 0 15px rgba(0, 255, 153, 0.4); .button-group {
width: 100%; flex-direction: column;
max-width: 800px; gap: 12px;
margin-bottom: 30px; align-items: stretch;
}
.button-group button {
width: 100%;
min-width: unset;
max-width: 100%;
}
header {
flex-direction: column;
height: auto;
padding-inline: 15px;
padding-block: 20px;
}
.logo-container {
flex-direction: column;
align-items: center;
gap: 12px;
}
.logo-container img {
height: 100px !important;
margin-top: -15px !important;
}
.logo-text {
margin-left: 0 !important;
text-align: center;
}
.logo-text h1 {
font-size: 1.4em;
margin-top: -30px !important;
margin-left: 0 !important;
text-align: center !important;
}
.logo-text p {
font-size: 0.8em;
margin-left: 0 !important;
text-align: center !important;
}
.admin-button-grid {
grid-template-columns: 1fr;
}
} }
header h1 {
font-size: 2.8em;
color: #00ff99;
margin-bottom: 8px;
}
header p {
font-size: 1.2em; /* ===== Header ===== */
} header {
display: flex;
justify-content: center;
align-items: center;
background-color: #111;
border-radius: 12px;
box-shadow: 0 0 15px #28E060;
width: 100%;
max-width: 800px;
margin-bottom: 25px;
padding: 25px;
height: 200px;
}
.logo-container {
display: flex;
align-items: center;
gap: 10px; /* optional: controls spacing between logo and text */
}
.logo-container img {
height: 200px;
width: auto;
}
.logo-text h1 {
font-size: clamp(1.4em, 6vw, 2.8em);
word-break: break-word;
overflow-wrap: break-word;
color: #28E060;
margin: 0;
margin-left: -30px; /* overlap effect */
text-align: left;
}
.logo-text p {
font-size: 1.2em;
color: #28E060;
margin: 0;
margin-left: -30px;
text-align: left;
}
/* ===== Main Layout ===== */ /* ===== Main Layout ===== */
main { main {
@@ -48,8 +134,8 @@ main {
align-items: center; align-items: center;
width: 100%; width: 100%;
max-width: 800px; max-width: 800px;
padding: 20px; padding: 0;
gap: 30px; gap: 0;
} }
/* ===== Card Styling ===== */ /* ===== Card Styling ===== */
@@ -58,7 +144,7 @@ main {
padding: 25px; padding: 25px;
width: 100%; width: 100%;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 0 15px rgba(0, 255, 153, 0.4); box-shadow: 0 0 15px #28E060;
text-align: center; text-align: center;
} }
@@ -68,25 +154,38 @@ main {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 0px; gap: 0px;
max-width: 725px;
width: 100%; width: 100%;
} }
/* ===== Inputs, Textareas, Selects ===== */ /* ===== Inputs, Textareas, Selects ===== */
button,
select,
input,
textarea {
font-family: 'Press Start 2P', monospace;
font-size: 12px !important;
letter-spacing: 0.5px;
}
input, input,
textarea, textarea,
select, select,
input[type="file"] { input[type="file"] {
width: 80%; width: 80%;
max-width: 500px; max-width: 500px;
padding: 12px 20px; padding-inline: 20px;
border: 2px solid #00ff99; padding-block: 12px;
border: 1px solid #28E060;
border-radius: 8px; border-radius: 8px;
background-color: #2c2f33; background-color: #2c2f33;
color: #00ff99; color: #28E060;
font-size: 1em; text-align: left;
text-align: center;
transition: 0.3s; transition: 0.3s;
margin:10px auto; }
select {
text-align: center;
} }
textarea { textarea {
@@ -98,31 +197,41 @@ input[type="password"] {
min-height: 50px; min-height: 50px;
} }
/* ===== File Input Customization ===== */
input[type="file"] { input[type="file"] {
border: 2px dashed #00ff99; border: 2px dashed #28E060;
cursor: pointer; cursor: pointer;
color: #28E060;
background-color: #2c2f33;
} }
input[type="file"]::file-selector-button { input[type="file"]::file-selector-button {
background-color: #00ff99; font-family: 'Press Start 2P', monospace;
color: #121212; font-size: 12px;
border: none; background-color: #2c2f33;
padding: 8px 15px; color: #28E060;
border-radius: 6px; border: 2px solid #28E060;
cursor: pointer; padding-inline: 10px;
transition: 0.3s; padding-block: 8px;
} margin-right: 10px;
border-radius: 6px;
text-transform: uppercase;
cursor: pointer;
transition: 0.3s ease;
}
input[type="file"]::file-selector-button:hover { input[type="file"]::file-selector-button:hover {
background-color: #00cc77; background-color: #28E060;
} color: #000;
box-shadow: 0 0 10px #28E060;
}
/* ===== Focus Effects ===== */ /* ===== Focus Effects ===== */
input:focus, input:focus,
textarea:focus, textarea:focus,
select:focus { select:focus {
outline: none; outline: none;
box-shadow: 0 0 8px rgba(0, 255, 153, 0.8); box-shadow: 0 0 10px #28E060;
} }
/* ===== Textareas Specific Widths ===== */ /* ===== Textareas Specific Widths ===== */
@@ -136,39 +245,138 @@ select:focus {
/* ===== Button Group Styling ===== */ /* ===== Button Group Styling ===== */
.button-group { .button-group {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: nowrap;
justify-content: center; justify-content: center;
gap: 15px; gap: 15px;
margin: 10px auto;
width: 100%; width: 100%;
} }
button { button {
padding: 10px 20px; padding-inline: 20px;
border: 0px solid #00ff99; padding-block: 10px;
border: none;
border-radius: 8px; border-radius: 8px;
background-color: #2c2f33; background-color: #2c2f33;
color: #00ff99; color: #28E060;
font-size: 1em; font-size: 1em;
cursor: pointer; cursor: pointer;
transition: 0.3s; transition: 0.3s;
width: 100%; width: auto;
max-width: 200px; min-width: 225px;
/* margin: 10px auto; */ max-width: 300px;
height: 45px;
} }
button:hover { button:hover {
background-color: #00ff99; background-color: #28E060;
color: #121212; color: #121212;
box-shadow: 0 0 10px #28E060;
} }
.danger-button {
background-color: #5f3131;
box-shadow: 0 0 10px #991717;
}
.danger-button:hover {
background-color: #af0000;
color: #121212;
box-shadow: 0 0 40px #ff0000;
}
.admin-button-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--spacer); /* e.g., 20px between rows and columns */
justify-items: center;
width: 100%;
max-width: 640px;
margin: 0 auto;
}
.admin-button-grid button {
width: 100%;
max-width: 280px;
font-family: 'Press Start 2P', monospace;
font-size: 12px;
}
/* ===== Toggle Switch Styling ===== */ /* ===== Toggle Switch Styling ===== */
.toggle-container {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
}
.toggle-label {
font-family: 'Press Start 2P', monospace;
font-size: 12px;
color: #28E060;
}
.material-switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.material-switch input {
opacity: 0;
width: 0;
height: 0;
}
.material-slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background-color: #222;
border: 2px solid #28E060;
border-radius: 34px;
transition: 0.4s;
margin: unset;
}
.material-slider::before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 2px;
bottom: 2px;
background-color: #28E060;
border-radius: 50%;
transition: 0.4s;
box-shadow: 0 0 6px #28E060;
}
.material-switch input:checked + .material-slider {
background-color: #28E060;
}
.material-switch input:checked + .material-slider::before {
transform: translateX(26px);
background-color: #000;
}
/* Label beside switch */
#toggle-label {
font-family: 'Press Start 2P', monospace;
color: #28E060;
margin-left: 20px;
font-size: 12px;
}
.toggle-container { .toggle-container {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 12px; gap: 12px;
margin-top: 10px;
width: 100%; width: 100%;
} }
@@ -197,7 +405,7 @@ button {
right: 0; right: 0;
bottom: 0; bottom: 0;
background-color: #2c2f33; background-color: #2c2f33;
border: 2px solid #00ff99; border: 2px solid #28E060;
border-radius: 34px; border-radius: 34px;
transition: .4s; transition: .4s;
display: flex; display: flex;
@@ -207,15 +415,15 @@ button {
/* The circle knob */ /* The circle knob */
.slider::before { .slider::before {
content: ""; content: "";
height: 26px; height: 22px;
width: 26px; width: 22px;
background-color: #00ff99; background-color: #28E060;
border-radius: 50%; border-radius: 50%;
transition: .4s; transition: .4s;
transform: translateX(4px); transform: translateX(2px);
position: absolute; position: absolute;
left: 0px; left: auto;
bottom: 2.5px; bottom: auto;
} }
input:checked + .slider::before { input:checked + .slider::before {
@@ -229,7 +437,7 @@ input:checked + .slider::before {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
font-size: 0.9em; font-size: 0.9em;
color: #00ff99; color: #28E060;
margin-top: 5px; margin-top: 5px;
} }
@@ -251,7 +459,7 @@ input:checked + .slider::before {
max-width: 500px; max-width: 500px;
min-height: 50px; min-height: 50px;
background-color: #333; background-color: #333;
color: #00ff99; color: #28E060;
text-align: center; text-align: center;
border-radius: 8px; border-radius: 8px;
padding: 14px; padding: 14px;
@@ -292,16 +500,16 @@ footer {
text-align: center; text-align: center;
padding: 25px; padding: 25px;
background-color: #1c1c1c; background-color: #1c1c1c;
color: #00ff99; color: #28E060;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 0 15px rgba(0, 255, 153, 0.4); box-shadow: 0 0 15px #28E060;
margin-top: 30px;
width: 100%; width: 100%;
max-width: 800px; max-width: 800px;
margin-top: 25px;
} }
footer a { footer a {
color: #00ff99; color: #28E060;
text-decoration: none; text-decoration: none;
} }
@@ -309,39 +517,72 @@ footer {
color: #ff0066; color: #ff0066;
} }
/* ===== Responsive Tweaks ===== */ /* ===== Responsive Design ===== */
@media (max-width: 600px) { @media (max-width: 600px) {
input, textarea, select, #input-text, #output-text { input,
textarea,
select,
#input-text,
#output-text {
width: 100%; width: 100%;
max-width: 90%; max-width: 90%;
} }
} }
/* ===== Copy Feedback Message ===== */ /* ===== Copy Feedback Message ===== */
.copy-feedback { .copy-feedback, #shared-link-feedback {
background-color: #2a2a2a; background-color: #2c2f33;
border: 1px solid #00ff99; padding-inline: 12px;
padding: 6px 12px; padding-block: 6px;
margin-top: 6px; margin-top: 6px;
border-radius: 6px; border-radius: 6px;
color: #00ff99; color: #28E060;
font-size: 0.9em; font-size: 0.9em;
display: none;
opacity: 0; opacity: 0;
transition: opacity 0.3s ease;
text-align: center; text-align: center;
max-width: 300px; max-width: 500px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
transition: opacity 0.3s ease;
} }
.copy-feedback.show { .copy-feedback.show, #shared-link-feedback.show {
opacity: 1; display: block;
} opacity: 1;
.hidden {
display: none !important;
} }
.share-link-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
margin-top: 12px;
margin-bottom: 12px;
}
#share-link {
display: block;
background-color: #2c2f33;
padding-inline: 16px;
padding-block: 8px;
border-radius: 6px;
color: #28E060;
font-size: 0.9em;
text-align: center;
max-width: 720px;
width: 100%;
word-break: break-all;
text-decoration: none;
transition: all 0.3s ease;
}
#share-link:hover {
color: #00cc77;
background-color: #36393f;
}
/* ===== Form Styling ===== */
form { form {
width: 100%; width: 100%;
display: flex; display: flex;
@@ -349,65 +590,40 @@ form {
align-items: center; align-items: center;
} }
form input, form input {
form button { min-height: 50px;
width: 80%; width: 80%;
max-width: 500px; max-width: 500px;
margin-bottom: 12px; text-align: left;
text-align: center; }
}
/* ===== Section Card Styling ===== */
section.card { section.card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
} }
.copy-feedback.show { /* ===== Pacman Game Styling ===== */
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 { #pacmanCanvas {
background-color: black; background-color: black;
display: block; display: block;
margin: auto; border: 2px solid #28E060;
border: 2px solid #00ff99;
border-radius: 12px; border-radius: 12px;
align-items: center; max-width: 700px;
justify-content: center; width: 100%;
aspect-ratio: 4/3;
object-fit: contain;
} }
#pacman-section { #pacman-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center;
gap: 20px; gap: 20px;
padding: 20px; margin-bottom: 25px;
display: block; max-width: 725px;
margin: auto; width: 100%;
border: 2px solid #00ff99;
border-radius: 12px;
} }
.pacman-wrapper { .pacman-wrapper {
@@ -415,10 +631,207 @@ section.card {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 100%; width: 100%;
padding: 0;
margin: 0;
} }
/* ===== Utility Classes ===== */
/* ===== Utility: Hidden Class ===== */
.hidden { .hidden {
display: none !important; display: none !important;
} }
/* ===== Section Spacing ===== */
#password-generator-section {
margin-bottom: 25px;
}
#encoding-section {
margin-bottom: 25px;
}
/* Pickup page sections */
#pickup-section {
margin-bottom: 25px;
}
#security-notice-section {
margin-bottom: 25px;
}
/* ===== File Input Section ===== */
#encoding-section #file-section {
display: none;
}
#encoding-section #file-section:not(.hidden) {
display: flex;
}
/* Ensure PacCrypt sharing file uploader is always visible */
#sharing-section #file-section {
display: flex;
}
/* Mobile-friendly download button */
.download-btn {
width: 100%;
padding: 12px;
font-size: 16px;
cursor: pointer;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
transition: background-color 0.3s;
}
.download-btn:hover {
background-color: var(--primary-hover);
}
/* Mobile form adjustments */
.pickup-form {
max-width: 100%;
margin: 0 auto;
}
.pickup-form input[type="password"] {
width: 100%;
padding: 12px;
margin-bottom: 10px;
font-size: 16px;
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--input-bg);
color: var(--text-color);
}
/* Mobile-specific styles */
@media (max-width: 768px) {
.download-btn {
padding: 15px;
font-size: 18px;
}
.pickup-form input[type="password"] {
padding: 15px;
font-size: 18px;
}
}
/* ===== Admin Section Styling ===== */
#sitemap-section,
#password-change-section,
#server-update-section,
#server-status-section,
#server-logs-section,
#system-settings-section {
margin-bottom: 25px;
padding: 25px;
background-color: #1e1e1e;
border-radius: 12px;
box-shadow: 0 0 15px #28E060;
}
.sitemap-header {
display: flex;
justify-content: space-between;
align-items: center;
margin: 15px 0;
}
.sitemap-header h3 {
color: #28E060;
margin: 0;
}
.collapse-btn {
background: none;
border: none;
color: #28E060;
font-size: 1.2em;
cursor: pointer;
padding-inline: 10px;
padding-block: 5px;
transition: transform 0.3s ease;
}
.collapse-btn:hover {
transform: scale(1.1);
}
.sitemap-content {
transition: all 0.3s ease;
margin-bottom: 15px;
}
#sitemap-section ul,
#server-status-section ul {
list-style: none;
padding-left: 0;
margin-top: 15px;
}
#sitemap-section li,
#server-status-section li {
margin-bottom: 10px;
padding: 8px;
background-color: #2c2f33;
border-radius: 6px;
color: #28E060;
}
#server-logs-section button {
margin-bottom: 15px;
width: 100%;
max-width: 300px;
}
#logLoader {
color: #28E060;
text-align: center;
padding: 10px;
}
#logContainer {
background-color: #2c2f33;
color: #28E060;
padding: 15px;
border-radius: 8px;
max-height: 400px;
overflow-y: auto;
font-family: monospace;
white-space: pre-wrap;
}
#system-settings-section {
margin-bottom: unset !important;
padding: 25px;
background-color: #1e1e1e;
border-radius: 12px;
box-shadow: 0 0 15px #28E060;
}
/* ===== Mobile Responsive Adjustments ===== */
@media (max-width: 768px) {
#sitemap-section,
#password-change-section,
#server-update-section,
#server-status-section,
#server-logs-section,
#system-settings-section {
padding: 20px;
margin-bottom: 20px;
}
#sitemap-section li,
#server-status-section li {
font-size: 0.9em;
padding: 6px;
}
#logContainer {
font-size: 0.9em;
padding: 10px;
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1006 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

+29 -11
View File
@@ -1,10 +1,21 @@
// encryption.js /**
* Encryption module.
* Handles cryptographic operations using Web Crypto API.
* Implements AES-GCM encryption with PBKDF2 key derivation.
*/
// ===== Constants =====
const SALT_LENGTH = 16;
const IV_LENGTH = 12;
const PBKDF2_ITERATIONS = 200_000;
const KEY_LENGTH = 256;
// ===== Key Derivation =====
/** /**
* Derives an AES-GCM key from a password using PBKDF2. * Derives an AES-GCM key from a password using PBKDF2.
* @param {string} password - User-supplied password. * @param {string} password - User-supplied password.
* @param {Uint8Array} salt - Randomly generated salt. * @param {Uint8Array} salt - Randomly generated salt.
* @returns {Promise<CryptoKey>} * @returns {Promise<CryptoKey>} - Derived cryptographic key.
*/ */
export async function deriveKey(password, salt) { export async function deriveKey(password, salt) {
const encoder = new TextEncoder(); const encoder = new TextEncoder();
@@ -20,16 +31,17 @@ export async function deriveKey(password, salt) {
{ {
name: 'PBKDF2', name: 'PBKDF2',
salt, salt,
iterations: 200_000, iterations: PBKDF2_ITERATIONS,
hash: 'SHA-256' hash: 'SHA-256'
}, },
keyMaterial, keyMaterial,
{ name: 'AES-GCM', length: 256 }, { name: 'AES-GCM', length: KEY_LENGTH },
false, false,
['encrypt', 'decrypt'] ['encrypt', 'decrypt']
); );
} }
// ===== Encryption =====
/** /**
* Encrypts a message using AES-GCM with a derived key. * Encrypts a message using AES-GCM with a derived key.
* @param {string} message - Plaintext message to encrypt. * @param {string} message - Plaintext message to encrypt.
@@ -38,12 +50,16 @@ export async function deriveKey(password, salt) {
*/ */
export async function encryptAdvanced(message, password) { export async function encryptAdvanced(message, password) {
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const salt = crypto.getRandomValues(new Uint8Array(16)); const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
const iv = crypto.getRandomValues(new Uint8Array(12)); const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
const key = await deriveKey(password, salt); const key = await deriveKey(password, salt);
const encoded = encoder.encode(message); const encoded = encoder.encode(message);
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded); const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
encoded
);
const output = new Uint8Array(salt.length + iv.length + ciphertext.byteLength); const output = new Uint8Array(salt.length + iv.length + ciphertext.byteLength);
output.set(salt); output.set(salt);
@@ -53,6 +69,7 @@ export async function encryptAdvanced(message, password) {
return btoa(String.fromCharCode(...output)); return btoa(String.fromCharCode(...output));
} }
// ===== Decryption =====
/** /**
* Decrypts an AES-GCM encrypted string. * Decrypts an AES-GCM encrypted string.
* @param {string} encryptedData - Base64-encoded ciphertext. * @param {string} encryptedData - Base64-encoded ciphertext.
@@ -64,9 +81,9 @@ export async function decryptAdvanced(encryptedData, password) {
atob(encryptedData).split('').map(c => c.charCodeAt(0)) atob(encryptedData).split('').map(c => c.charCodeAt(0))
); );
const salt = encrypted.slice(0, 16); const salt = encrypted.slice(0, SALT_LENGTH);
const iv = encrypted.slice(16, 28); const iv = encrypted.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
const ciphertext = encrypted.slice(28); const ciphertext = encrypted.slice(SALT_LENGTH + IV_LENGTH);
const key = await deriveKey(password, salt); const key = await deriveKey(password, salt);
const decrypted = await crypto.subtle.decrypt( const decrypted = await crypto.subtle.decrypt(
@@ -78,8 +95,9 @@ export async function decryptAdvanced(encryptedData, password) {
return new TextDecoder().decode(decrypted); return new TextDecoder().decode(decrypted);
} }
// ===== Module Initialization =====
/** /**
* Optional init logging for module diagnostics. * Initializes the encryption module and logs its status.
*/ */
export function setupEncryption() { export function setupEncryption() {
console.log('[Encryption] Module loaded'); console.log('[Encryption] Module loaded');
+107 -80
View File
@@ -1,92 +1,119 @@
// fileops.js
import { encryptAdvanced, decryptAdvanced } from './encryption.js';
/** /**
* Encrypts the selected file and triggers download of the encrypted version. * File operations module.
* @param {HTMLInputElement} fileInput - The input element of type 'file'. * Handles file encryption and decryption operations.
* @param {string} password - Password for encryption.
*/ */
export function encryptFile(fileInput, password) {
if (!fileInput.files.length) {
alert("Please select a file!");
return;
}
// ===== Constants =====
const CHUNK_SIZE = 1024 * 1024; // 1MB chunks
// ===== Public Interface =====
export async function encryptFile(fileInput, password) {
const file = fileInput.files[0]; const file = fileInput.files[0];
const reader = new FileReader(); if (!file) return;
reader.onload = async (e) => { try {
const rawBytes = new Uint8Array(e.target.result); const encryptedChunks = await processFile(file, password, true);
const base64 = btoa(String.fromCharCode(...rawBytes)); downloadEncryptedFile(encryptedChunks, file.name);
const encrypted = await encryptAdvanced(base64, password); } catch (error) {
downloadFile(encrypted, file.name + ".enc"); alert("Error encrypting file: " + error.message);
}; }
reader.readAsArrayBuffer(file);
} }
/** export async function decryptFile(fileInput, password) {
* Decrypts the selected encrypted file and triggers download of the original. const file = fileInput.files[0];
* @param {HTMLInputElement} fileInput - The input element of type 'file'. if (!file) return;
* @param {string} password - Password for decryption.
*/ try {
export function decryptFile(fileInput, password) { const decryptedChunks = await processFile(file, password, false);
if (!fileInput.files.length) { downloadDecryptedFile(decryptedChunks, file.name);
alert("Please select a file!"); } catch (error) {
return; alert("Error decrypting file: " + error.message);
}
}
// ===== File Processing =====
async function processFile(file, password, isEncrypt) {
const chunks = [];
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
let processedChunks = 0;
for (let start = 0; start < file.size; start += CHUNK_SIZE) {
const chunk = file.slice(start, start + CHUNK_SIZE);
const arrayBuffer = await chunk.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
const processedChunk = await processChunk(uint8Array, password, isEncrypt);
chunks.push(processedChunk);
processedChunks++;
updateProgress(processedChunks, totalChunks);
} }
const file = fileInput.files[0]; return chunks;
const reader = new FileReader(); }
reader.onload = async (e) => { async function processChunk(data, password, isEncrypt) {
try { const payload = {
const encryptedText = e.target.result; "encryption-type": "advanced",
const base64Decrypted = await decryptAdvanced(encryptedText, password); operation: isEncrypt ? "encrypt" : "decrypt",
const byteArray = new Uint8Array( message: Array.from(data).join(','),
[...atob(base64Decrypted)].map(c => c.charCodeAt(0)) password: password
); };
downloadFileBinary(byteArray, file.name.replace(/\.enc$/, ''));
} catch (err) { const response = await fetch("/", {
console.error("[Decryption Error]", err); method: "POST",
alert("Decryption failed: wrong password or corrupted file."); headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return new Uint8Array(result.result.split(',').map(Number));
}
// ===== File Download =====
function downloadEncryptedFile(chunks, originalName) {
const blob = new Blob(chunks, { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = originalName + '.encrypted';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function downloadDecryptedFile(chunks, originalName) {
const blob = new Blob(chunks, { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = originalName.replace('.encrypted', '');
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// ===== Progress Tracking =====
function updateProgress(processed, total) {
const progressBar = document.getElementById("file-progress");
const progressText = document.getElementById("file-progress-text");
if (progressBar && progressText) {
const percent = Math.round((processed / total) * 100);
progressBar.style.width = percent + "%";
progressText.textContent = `Processing: ${percent}%`;
if (processed === total) {
setTimeout(() => {
progressBar.style.width = "0%";
progressText.textContent = "";
}, 1000);
} }
}; }
reader.readAsText(file);
}
/**
* Downloads a text-based file (encrypted string).
* @param {string} content - The file content to download.
* @param {string} filename - Desired name for the downloaded file.
*/
function downloadFile(content, filename) {
const blob = new Blob([content], { type: "application/octet-stream" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
/**
* Downloads a binary file (Uint8Array).
* @param {Uint8Array} byteArray - The binary content.
* @param {string} filename - Desired name for the downloaded file.
*/
function downloadFileBinary(byteArray, filename) {
const blob = new Blob([byteArray], { type: "application/octet-stream" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
} }
+5 -4
View File
@@ -1,11 +1,12 @@
// main.js /**
* Main application entry point.
* Initializes UI and game components when the DOM is loaded.
*/
import { setupUI } from './ui.js'; import { setupUI } from './ui.js';
import { setupGame } from './pacman.js'; import { setupGame } from './pacman.js';
/** // Initialize application when DOM is fully loaded
* Initialize UI and game once the DOM is fully loaded.
*/
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", () => {
setupUI(); setupUI();
setupGame(); setupGame();
+169 -55
View File
@@ -1,52 +1,45 @@
// pacman.js /**
* Pacman game module.
* Handles game logic, rendering, and user interaction.
*/
// ===== Game Constants =====
const PACMAN_SPEED = 40;
const ENEMY_SPEED = 20;
const CELL_SIZE = 40;
const DOT_SIZE = 5;
// ===== Game State =====
let canvas, ctx, pacman, enemy, walls, dots, score;
let cols, rows, randSeed, gameInterval;
// ===== Public Interface =====
export function setupGame() { export function setupGame() {
console.log('[PacMan] Game module loaded.'); console.log('[PacMan] Game module loaded.');
window.startPacman = startPacman; window.startPacman = startPacman;
window.exitGame = exitGame; 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() { export function startPacman() {
canvas = document.getElementById("pacmanCanvas"); // Scroll to the Pacman section
ctx = canvas.getContext("2d"); const pacmanSection = document.getElementById("pacman-section");
if (pacmanSection) {
pacmanSection.scrollIntoView({ behavior: 'smooth' });
}
cols = Math.floor(canvas.width / cellSize); // Initialize game state
rows = Math.floor(canvas.height / cellSize); initializeGame();
walls = []; setupGameLoop();
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() { export function stopPacman() {
clearInterval(gameInterval); clearInterval(gameInterval);
if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height); if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height);
// Restore scrolling
document.body.style.overflow = '';
document.removeEventListener('wheel', preventScroll);
document.removeEventListener('touchmove', preventScroll);
} }
export function resetGame() { export function resetGame() {
@@ -61,8 +54,88 @@ export function exitGame() {
document.getElementById("encoding-section").style.display = "block"; document.getElementById("encoding-section").style.display = "block";
} }
// ====== Game Setup Helpers ====== // ===== Game Initialization =====
function initializeGame() {
canvas = document.getElementById("pacmanCanvas");
ctx = canvas.getContext("2d");
cols = Math.floor(canvas.width / CELL_SIZE);
rows = Math.floor(canvas.height / CELL_SIZE);
walls = [];
dots = [];
score = 0;
clearInterval(gameInterval);
// Get seed from generated password or use default
const passwordField = document.getElementById("generated-password");
const seedSource = passwordField?.value || "pacman";
randSeed = [...seedSource].reduce((s, c) => s + c.charCodeAt(0), 0);
generateWalls();
generateDots();
pacman = spawn();
do { enemy = spawn(); } while (enemy.x === pacman.x && enemy.y === pacman.y);
pacman.dx = pacman.dy = 0;
document.addEventListener("keydown", movePacman);
// Prevent scrolling
document.body.style.overflow = 'hidden';
document.addEventListener('wheel', preventScroll, { passive: false });
document.addEventListener('touchmove', preventScroll, { passive: false });
// Add touch controls
let touchStartX = 0;
let touchStartY = 0;
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
}, { passive: false });
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
}, { passive: false });
canvas.addEventListener('touchend', (e) => {
e.preventDefault();
const touchEndX = e.changedTouches[0].clientX;
const touchEndY = e.changedTouches[0].clientY;
const dx = touchEndX - touchStartX;
const dy = touchEndY - touchStartY;
// Determine swipe direction based on the larger movement
if (Math.abs(dx) > Math.abs(dy)) {
// Horizontal swipe
if (dx > 0) {
pacman.dx = PACMAN_SPEED;
pacman.dy = 0;
} else {
pacman.dx = -PACMAN_SPEED;
pacman.dy = 0;
}
} else {
// Vertical swipe
if (dy > 0) {
pacman.dx = 0;
pacman.dy = PACMAN_SPEED;
} else {
pacman.dx = 0;
pacman.dy = -PACMAN_SPEED;
}
}
}, { passive: false });
}
function setupGameLoop() {
gameInterval = setInterval(gameLoop, 150);
}
// ===== Game Setup Helpers =====
function spawn() { function spawn() {
const options = []; const options = [];
for (let c = 1; c < cols - 1; c++) { for (let c = 1; c < cols - 1; c++) {
@@ -80,9 +153,9 @@ function spawn() {
} }
const s = options[Math.floor(rand() * options.length)]; const s = options[Math.floor(rand() * options.length)];
return { return {
x: s.c * cellSize + cellSize / 2, x: s.c * CELL_SIZE + CELL_SIZE / 2,
y: s.r * cellSize + cellSize / 2, y: s.r * CELL_SIZE + CELL_SIZE / 2,
size: cellSize / 2 - 5, size: CELL_SIZE / 2 - 5,
dx: 0, dx: 0,
dy: 0 dy: 0
}; };
@@ -94,6 +167,7 @@ function rand() {
} }
function generateWalls() { function generateWalls() {
// First pass: generate initial walls
for (let c = 0; c < cols; c++) { for (let c = 0; c < cols; c++) {
for (let r = 0; r < rows; r++) { for (let r = 0; r < rows; r++) {
if (c === 0 || r === 0 || c === cols - 1 || r === rows - 1 || rand() < 0.2) { if (c === 0 || r === 0 || c === cols - 1 || r === rows - 1 || rand() < 0.2) {
@@ -101,6 +175,25 @@ function generateWalls() {
} }
} }
} }
// Second pass: check for enclosed spaces
for (let c = 1; c < cols - 1; c++) {
for (let r = 1; r < rows - 1; r++) {
// Skip if already a wall
if (walls.some(w => w.c === c && w.r === r)) continue;
// Check all four sides
const hasWallAbove = walls.some(w => w.c === c && w.r === r - 1);
const hasWallBelow = walls.some(w => w.c === c && w.r === r + 1);
const hasWallLeft = walls.some(w => w.c === c - 1 && w.r === r);
const hasWallRight = walls.some(w => w.c === c + 1 && w.r === r);
// If all sides are walls, make this spot a wall too
if (hasWallAbove && hasWallBelow && hasWallLeft && hasWallRight) {
walls.push({ c, r });
}
}
}
} }
function generateDots() { function generateDots() {
@@ -120,8 +213,7 @@ function generateDots() {
} }
} }
// ====== Game Loop & Drawing ====== // ===== Game Loop & Rendering =====
function gameLoop() { function gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
drawWalls(); drawWalls();
@@ -137,7 +229,7 @@ function gameLoop() {
function drawWalls() { function drawWalls() {
ctx.fillStyle = "blue"; ctx.fillStyle = "blue";
walls.forEach(w => { walls.forEach(w => {
ctx.fillRect(w.c * cellSize, w.r * cellSize, cellSize, cellSize); ctx.fillRect(w.c * CELL_SIZE, w.r * CELL_SIZE, CELL_SIZE, CELL_SIZE);
}); });
} }
@@ -151,7 +243,10 @@ function drawChar(ch, color) {
function drawScore() { function drawScore() {
ctx.fillStyle = "white"; ctx.fillStyle = "white";
ctx.font = "20px Poppins"; ctx.font = "20px Poppins";
ctx.fillText("Score: " + score, 10, 25); ctx.textAlign = "left";
// Add padding to prevent clipping
const padding = 10;
ctx.fillText("Score: " + score, padding, 25);
} }
function checkGameOver() { function checkGameOver() {
@@ -167,17 +262,16 @@ function checkGameOver() {
} }
} }
// ====== Movement Logic ====== // ===== Movement Logic =====
function movePacman(e) { function movePacman(e) {
const k = e.key; const k = e.key;
if (!["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(k)) return; if (!["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(k)) return;
e.preventDefault(); e.preventDefault();
if (k === "ArrowUp") { pacman.dx = 0; pacman.dy = -pacmanSpeed; } if (k === "ArrowUp") { pacman.dx = 0; pacman.dy = -PACMAN_SPEED; }
if (k === "ArrowDown") { pacman.dx = 0; pacman.dy = pacmanSpeed; } if (k === "ArrowDown") { pacman.dx = 0; pacman.dy = PACMAN_SPEED; }
if (k === "ArrowLeft") { pacman.dx = -pacmanSpeed; pacman.dy = 0; } if (k === "ArrowLeft") { pacman.dx = -PACMAN_SPEED; pacman.dy = 0; }
if (k === "ArrowRight") { pacman.dx = pacmanSpeed; pacman.dy = 0; } if (k === "ArrowRight") { pacman.dx = PACMAN_SPEED; pacman.dy = 0; }
} }
function moveChar(ch) { function moveChar(ch) {
@@ -191,7 +285,7 @@ function moveChar(ch) {
function moveEnemy() { function moveEnemy() {
const options = []; const options = [];
const moves = [[enemySpeed, 0], [-enemySpeed, 0], [0, enemySpeed], [0, -enemySpeed]]; const moves = [[ENEMY_SPEED, 0], [-ENEMY_SPEED, 0], [0, ENEMY_SPEED], [0, -ENEMY_SPEED]];
moves.forEach(([dx, dy]) => { moves.forEach(([dx, dy]) => {
const nx = enemy.x + dx; const nx = enemy.x + dx;
@@ -225,8 +319,8 @@ function willCollide(x, y, size) {
const top = y - size, bottom = y + size; const top = y - size, bottom = y + size;
return walls.some(w => { return walls.some(w => {
const wx1 = w.c * cellSize, wy1 = w.r * cellSize; const wx1 = w.c * CELL_SIZE, wy1 = w.r * CELL_SIZE;
const wx2 = wx1 + cellSize, wy2 = wy1 + cellSize; const wx2 = wx1 + CELL_SIZE, wy2 = wy1 + CELL_SIZE;
return right > wx1 && left < wx2 && bottom > wy1 && top < wy2; return right > wx1 && left < wx2 && bottom > wy1 && top < wy2;
}); });
} }
@@ -235,8 +329,8 @@ function eatDots() {
const chompSound = document.getElementById("chomp-sound"); const chompSound = document.getElementById("chomp-sound");
dots = dots.filter(d => { dots = dots.filter(d => {
const dx = d.c * cellSize + cellSize / 2; const dx = d.c * CELL_SIZE + CELL_SIZE / 2;
const dy = d.r * cellSize + cellSize / 2; const dy = d.r * CELL_SIZE + CELL_SIZE / 2;
if (Math.abs(pacman.x - dx) < pacman.size && Math.abs(pacman.y - dy) < pacman.size) { if (Math.abs(pacman.x - dx) < pacman.size && Math.abs(pacman.y - dy) < pacman.size) {
score++; score++;
@@ -250,13 +344,33 @@ function eatDots() {
return true; return true;
}); });
// Check if all dots are eaten
if (dots.length === 0) {
// Trigger password generator for new random map
const generateBtn = document.getElementById("generate-btn");
if (generateBtn) {
generateBtn.click();
}
// Auto-restart the game after a short delay
setTimeout(() => {
resetGame();
}, 1000);
}
ctx.fillStyle = "white"; ctx.fillStyle = "white";
dots.forEach(d => { dots.forEach(d => {
ctx.beginPath(); ctx.beginPath();
ctx.arc(d.c * cellSize + cellSize / 2, d.r * cellSize + cellSize / 2, dotSize, 0, Math.PI * 2); ctx.arc(d.c * CELL_SIZE + CELL_SIZE / 2, d.r * CELL_SIZE + CELL_SIZE / 2, DOT_SIZE, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
}); });
} }
// ===== Global Functions =====
window.resetGame = resetGame; window.resetGame = resetGame;
window.exitGame = exitGame; window.exitGame = exitGame;
// Add scroll prevention function
function preventScroll(e) {
e.preventDefault();
}
-475
View File
@@ -1,475 +0,0 @@
// ===== AES Advanced Encryption =====
async function encryptAdvanced(message, password) {
const salt = crypto.getRandomValues(new Uint8Array(16));
const key = await deriveKey(password, salt);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encoder = new TextEncoder();
const encodedMessage = encoder.encode(message);
const encryptedMessage = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
key,
encodedMessage
);
const encryptedArray = new Uint8Array(salt.length + iv.length + encryptedMessage.byteLength);
encryptedArray.set(salt);
encryptedArray.set(iv, salt.length);
encryptedArray.set(new Uint8Array(encryptedMessage), salt.length + iv.length);
return btoa(String.fromCharCode(...encryptedArray));
}
async function deriveKey(password, salt) {
const encoder = new TextEncoder();
const passwordBuffer = encoder.encode(password);
const keyMaterial = await crypto.subtle.importKey(
'raw',
passwordBuffer,
{ name: 'PBKDF2' },
false,
['deriveKey']
);
return crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 200000,
hash: 'SHA-256',
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
async function decryptAdvanced(encryptedData, password) {
const encryptedArray = new Uint8Array(atob(encryptedData).split("").map(c => c.charCodeAt(0)));
const salt = encryptedArray.slice(0, 16);
const iv = encryptedArray.slice(16, 28);
const encryptedMessage = encryptedArray.slice(28);
const key = await deriveKey(password, salt);
const decryptedMessage = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv },
key,
encryptedMessage
);
const decoder = new TextDecoder();
return decoder.decode(decryptedMessage);
}
// ===== UI Toggles =====
function toggleEncryptionOptions() {
const type = document.getElementById("encryption-type").value;
document.getElementById("password-input").style.display = (type === 'advanced') ? 'flex' : 'none';
if (type === 'basic') removeFile();
toggleInputMode();
document.getElementById("encrypt-label").textContent = (type === 'basic') ? "Encode" : "Encrypt";
document.getElementById("decrypt-label").textContent = (type === 'basic') ? "Decode" : "Decrypt";
}
function removeFile() {
document.getElementById("file-input").value = "";
document.getElementById("remove-file-btn").style.display = 'none';
toggleInputMode();
}
function toggleInputMode() {
const textValue = document.getElementById("input-text").value.trim();
const fileSelected = document.getElementById("file-input").files.length > 0;
const isAdvanced = document.getElementById("encryption-type").value === 'advanced';
document.getElementById("text-section").style.display = fileSelected ? 'none' : 'flex';
document.getElementById("file-section").style.display = (isAdvanced && !textValue) ? 'flex' : 'none';
document.getElementById("remove-file-btn").style.display = fileSelected ? 'inline-block' : 'none';
if (isAdvanced) {
document.getElementById("password-input").style.display = 'flex';
}
}
// ===== Form Submission =====
async function handleSubmit(event) {
event.preventDefault();
const password = document.getElementById("password").value;
const encryptionType = document.getElementById("encryption-type").value;
if (encryptionType === 'advanced' && !password) {
alert("Password is required for advanced encryption.");
return;
}
const payload = {
"encryption-type": encryptionType,
operation: document.querySelector('input[name="operation"]:checked').value,
message: document.getElementById("input-text").value,
password: password
};
const fileInput = document.getElementById("file-input");
if (fileInput.files.length > 0) {
const op = document.querySelector('input[name="operation"]:checked').value;
if (op === 'encrypt') encryptFile();
else decryptFile();
return;
}
try {
const resp = await fetch("/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const data = await resp.json();
document.getElementById("output-text").value = data.result;
} catch (err) {
alert("Error processing request: " + err);
}
}
// ===== File Encryption / Decryption =====
function encryptFile() {
const f = document.getElementById("file-input");
const pwd = document.getElementById("password").value;
if (!pwd) return alert("Please enter a password!");
if (!f.files.length) return alert("Please select a file!");
const reader = new FileReader();
reader.onload = async (e) => {
const raw = new Uint8Array(e.target.result);
const base64Raw = btoa(String.fromCharCode(...raw));
const encrypted = await encryptAdvanced(base64Raw, pwd);
downloadFile(encrypted, f.files[0].name + ".enc");
};
reader.readAsArrayBuffer(f.files[0]);
}
function decryptFile() {
const f = document.getElementById("file-input");
const pwd = document.getElementById("password").value;
if (!pwd) return alert("Please enter a password!");
if (!f.files.length) return alert("Please select a file!");
const reader = new FileReader();
reader.onload = async (e) => {
try {
const encryptedText = e.target.result;
const decryptedBase64 = await decryptAdvanced(encryptedText, pwd);
const byteString = atob(decryptedBase64);
const byteArray = new Uint8Array([...byteString].map(c => c.charCodeAt(0)));
downloadFileBinary(byteArray, f.files[0].name.replace(/\.enc$/, ''));
} catch (err) {
alert("Decryption failed: wrong password or corrupted file.");
}
};
reader.readAsText(f.files[0]);
}
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);
}
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);
}
// ===== Password Generator =====
function generateRandomPassword() {
const length = 30;
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+[]{}|;:,.<>?/~";
let password = "";
for (let i = 0; i < length; i++) {
password += charset.charAt(Math.floor(Math.random() * charset.length));
}
document.getElementById("password-field").value = password;
}
// ===== Copy to Clipboard =====
function copyToClipboard(elementId, toastId) {
const copyText = document.getElementById(elementId);
copyText.select();
copyText.setSelectionRange(0, 99999);
document.execCommand("copy");
const toast = document.getElementById(toastId);
toast.classList.add("show");
setTimeout(() => toast.classList.remove("show"), 2000);
}
// ===== Pacman Easter Egg =====
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';
startPacman();
} else if (pacSection.style.display === 'block' && !val.includes('pacman')) {
exitGame();
}
}
// ===== Game Exit & Restart =====
function exitGame() {
stopPacman();
document.getElementById("input-text").value = "";
document.getElementById("pacman-section").style.display = 'none';
document.getElementById("encoding-section").style.display = 'block';
}
function resetGame() {
stopPacman();
startPacman();
}
// ===== Clear All =====
function clearAll() {
document.getElementById("input-text").value = "";
document.getElementById("output-text").value = "";
document.getElementById("file-input").value = "";
document.getElementById("password").value = "";
document.getElementById("pacman-section").style.display = "none";
document.getElementById("encoding-section").style.display = "block";
removeFile();
toggleInputMode();
}
// ===== Initialize =====
document.addEventListener("DOMContentLoaded", () => {
toggleEncryptionOptions();
toggleInputMode();
document.getElementById("input-text").addEventListener("input", checkForPacman);
});
// ===== Pacman Game Variables & Logic =====
let canvas, ctx, pacman, enemy, walls, dots, score;
let pacmanSpeed = 40, enemySpeed = 20, cellSize = 40, dotSize = 5;
let cols, rows, randSeed, gameInterval;
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);
randSeed = Array.from(
document.getElementById("password-field").value
).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);
}
function stopPacman() {
clearInterval(gameInterval);
if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function spawn() {
const opts = [];
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)
)) {
opts.push({ c, r });
}
}
}
}
const s = opts[Math.floor(rand() * opts.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 });
}
}
}
function movePacman(e) {
if (!["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(e.key)) return;
e.preventDefault();
if (e.key==="ArrowUp") { pacman.dx=0; pacman.dy=-pacmanSpeed; }
if (e.key==="ArrowDown") { pacman.dx=0; pacman.dy=pacmanSpeed; }
if (e.key==="ArrowLeft") { pacman.dx=-pacmanSpeed; pacman.dy=0; }
if (e.key==="ArrowRight") { pacman.dx=pacmanSpeed; pacman.dy=0; }
}
// ===== Collision Helper =====
function willCollide(x, y, size) {
const left = x - size, right = x + size;
const top = y - size, bottom = y + size;
for (let w of walls) {
const wx1 = w.c * cellSize, wy1 = w.r * cellSize;
const wx2 = wx1 + cellSize, wy2 = wy1 + cellSize;
if (right > wx1 && left < wx2 && bottom > wy1 && top < wy2) {
return true;
}
}
return false;
}
function moveChar(ch) {
const nx = ch.x + ch.dx, ny = ch.y + ch.dy;
if (!willCollide(nx, ny, ch.size)) {
ch.x = nx; ch.y = ny;
}
}
function moveEnemy() {
const options = [];
[[enemySpeed,0],[-enemySpeed,0],[0,enemySpeed],[0,-enemySpeed]].forEach(
([dx,dy]) => {
const nx = enemy.x + dx, ny = enemy.y + dy;
if (!willCollide(nx, ny, enemy.size)) options.push({dx,dy});
}
);
if (!options.length) return;
let best = options[0];
let bestD = Math.abs(enemy.x+best.dx-pacman.x)+Math.abs(enemy.y+best.dy-pacman.y);
for (let opt of options) {
const d = Math.abs(enemy.x+opt.dx-pacman.x)+Math.abs(enemy.y+opt.dy-pacman.y);
if (d < bestD) { best=opt; bestD=d; }
}
enemy.x += best.dx; enemy.y += best.dy;
}
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 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; // Reset sound
chompSound.volume = 0.4;
chompSound.play();
}
return false; // Remove dot
}
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();
});
}
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);
}
}
+150 -60
View File
@@ -1,72 +1,103 @@
// ui.js /**
* UI management module.
* Handles user interface interactions and form handling.
*/
import { encryptFile, decryptFile } from './fileops.js'; import { encryptFile, decryptFile } from './fileops.js';
/** // ===== UI Initialization =====
* Initialize all UI functionality after DOM is loaded
*/
export function setupUI() { export function setupUI() {
toggleEncryptionOptions(); // Set initial state of remove button to hidden
toggleInputMode(); const removeBtn = document.getElementById("remove-file-btn");
if (removeBtn) {
removeBtn.style.display = "none";
}
const encryptionTypeEl = document.getElementById("encryption-type"); initializeEventListeners();
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 ( // ===== Event Listeners =====
encryptionTypeEl && inputTextEl && formEl && removeFileBtn && function initializeEventListeners() {
clearAllBtn && generateBtn && copyPasswordBtn && toggleSwitch const elements = {
) { encryptionType: document.getElementById("encryption-type"),
encryptionTypeEl.addEventListener("change", toggleEncryptionOptions); inputText: document.getElementById("input-text"),
inputTextEl.addEventListener("input", () => { form: document.getElementById("crypto-form"),
toggleInputMode(); removeFileBtn: document.getElementById("remove-file-btn"),
checkForPacman(); clearAllBtn: document.getElementById("clear-all-btn"),
}); generateBtn: document.getElementById("generate-btn"),
formEl.addEventListener("submit", handleSubmit); copyPasswordBtn: document.getElementById("copy-btn"),
removeFileBtn.addEventListener("click", removeFile); copyOutputBtn: document.getElementById("copy-output-btn"),
clearAllBtn.addEventListener("click", clearAll); toggleSwitch: document.getElementById("operation-toggle"),
generateBtn.addEventListener("click", generateRandomPassword); copyShareBtn: document.getElementById("copy-share-btn"),
copyPasswordBtn.addEventListener("click", () => copyToClipboard("generated-password", "password-copy-feedback")); shareLink: document.getElementById("share-link")
copyOutputBtn?.addEventListener("click", () => copyToClipboard("output-text", "output-copy-feedback")); };
toggleSwitch.addEventListener("change", updateToggleLabels);
const copySharedLinkBtn = document.getElementById("copy-shared-link"); if (validateElements(elements)) {
const sharedLinkEl = document.getElementById("shared-link"); setupElementListeners(elements);
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 validateElements(elements) {
return elements.encryptionType && elements.inputText && elements.form &&
elements.removeFileBtn && elements.clearAllBtn && elements.generateBtn &&
elements.copyPasswordBtn && elements.toggleSwitch;
}
function setupElementListeners(elements) {
elements.encryptionType.addEventListener("change", toggleEncryptionOptions);
elements.inputText.addEventListener("input", handleInputChange);
elements.form.addEventListener("submit", handleSubmit);
elements.removeFileBtn.addEventListener("click", removeFile);
elements.clearAllBtn.addEventListener("click", clearAll);
elements.generateBtn.addEventListener("click", generateRandomPassword);
elements.copyPasswordBtn.addEventListener("click", () => copyToClipboard("generated-password", "password-copy-feedback"));
elements.copyOutputBtn?.addEventListener("click", () => copyToClipboard("output-text", "output-copy-feedback"));
elements.toggleSwitch.addEventListener("change", () => {
console.log("Mode:", elements.toggleSwitch.checked ? "Decrypt" : "Encrypt");
});
// Add file input change listener
const fileInput = document.getElementById("file-input");
if (fileInput) {
fileInput.addEventListener("change", () => {
const removeBtn = document.getElementById("remove-file-btn");
if (removeBtn) {
removeBtn.style.display = fileInput.files.length > 0 ? "inline-block" : "none";
}
});
}
setupShareLinkListeners(elements);
}
function setupShareLinkListeners(elements) {
if (elements.copyShareBtn && elements.shareLink) {
elements.copyShareBtn.addEventListener("click", () => {
const linkText = elements.shareLink.textContent.trim();
navigator.clipboard.writeText(linkText).then(() => {
const feedback = document.getElementById("shared-link-feedback");
if (feedback) {
feedback.style.display = "block";
feedback.classList.add("show");
setTimeout(() => {
feedback.classList.remove("show");
setTimeout(() => {
feedback.style.display = "none";
}, 300);
}, 3000);
}
});
});
}
}
// ===== UI State Management =====
function toggleEncryptionOptions() { function toggleEncryptionOptions() {
const type = document.getElementById("encryption-type").value.trim().toLowerCase(); const type = document.getElementById("encryption-type").value.trim().toLowerCase();
const passwordInputWrapper = document.getElementById("password-input"); const passwordInputWrapper = document.getElementById("password-input");
const fileSection = document.querySelector("#encoding-section #file-section");
const isAdvanced = type.includes("advanced"); const isAdvanced = type.includes("advanced");
if (passwordInputWrapper) { if (passwordInputWrapper) {
@@ -77,11 +108,18 @@ function toggleEncryptionOptions() {
} }
} }
if (fileSection) {
if (isAdvanced) {
fileSection.classList.remove("hidden");
} else {
fileSection.classList.add("hidden");
}
}
updateToggleLabels(); updateToggleLabels();
toggleInputMode(); toggleInputMode();
} }
function updateToggleLabels() { function updateToggleLabels() {
const type = document.getElementById("encryption-type")?.value; const type = document.getElementById("encryption-type")?.value;
const leftLabel = document.getElementById("toggle-left-label"); const leftLabel = document.getElementById("toggle-left-label");
@@ -112,6 +150,7 @@ function toggleInputMode() {
removeBtn.style.display = fileSelected ? "inline-block" : "none"; removeBtn.style.display = fileSelected ? "inline-block" : "none";
} }
// ===== Form Handling =====
async function handleSubmit(event) { async function handleSubmit(event) {
event.preventDefault(); event.preventDefault();
@@ -133,6 +172,10 @@ async function handleSubmit(event) {
: decryptFile(fileInput, password); : decryptFile(fileInput, password);
} }
await handleTextOperation(encryptionType, operation, password);
}
async function handleTextOperation(encryptionType, operation, password) {
const payload = { const payload = {
"encryption-type": encryptionType, "encryption-type": encryptionType,
operation: operation, operation: operation,
@@ -153,6 +196,7 @@ async function handleSubmit(event) {
} }
} }
// ===== Utility Functions =====
function removeFile() { function removeFile() {
const fileInput = document.getElementById("file-input"); const fileInput = document.getElementById("file-input");
if (fileInput) fileInput.value = ""; if (fileInput) fileInput.value = "";
@@ -168,20 +212,60 @@ function generateRandomPassword() {
charset.charAt(Math.floor(Math.random() * charset.length)) charset.charAt(Math.floor(Math.random() * charset.length))
).join(""); ).join("");
const passwordField = document.getElementById("generated-password"); const passwordField = document.getElementById("generated-password");
if (passwordField) passwordField.value = password; if (passwordField) {
passwordField.value = password;
// Check if we should start Pacman
checkForPacman();
}
} }
function copyToClipboard(elementId, feedbackId) { function copyToClipboard(elementId, feedbackId) {
const el = document.getElementById(elementId); const el = document.getElementById(elementId);
const feedback = document.getElementById(feedbackId); const feedback = document.getElementById(feedbackId);
if (!el || !feedback) return;
navigator.clipboard.writeText(el.textContent || el.value || "").then(() => { if (!el || !el.value) return;
// Create a temporary textarea element
const textarea = document.createElement('textarea');
textarea.value = el.value;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
// Select and copy the text
textarea.select();
textarea.setSelectionRange(0, 99999); // For mobile devices
try {
// Try using the modern clipboard API first
navigator.clipboard.writeText(el.value).then(() => {
showFeedback(feedback);
}).catch(() => {
// Fallback to execCommand for older browsers
document.execCommand('copy');
showFeedback(feedback);
});
} catch (err) {
// Final fallback
document.execCommand('copy');
showFeedback(feedback);
}
// Clean up
document.body.removeChild(textarea);
}
function showFeedback(feedback) {
if (feedback) {
feedback.style.display = "block";
feedback.classList.add("show"); feedback.classList.add("show");
setTimeout(() => { setTimeout(() => {
feedback.classList.remove("show"); feedback.classList.remove("show");
setTimeout(() => {
feedback.style.display = "none";
}, 300);
}, 3000); }, 3000);
}); }
} }
function clearAll() { function clearAll() {
@@ -196,6 +280,11 @@ function clearAll() {
document.getElementById("encoding-section")?.style.setProperty("display", "block"); document.getElementById("encoding-section")?.style.setProperty("display", "block");
} }
function handleInputChange() {
toggleInputMode();
checkForPacman();
}
function checkForPacman() { function checkForPacman() {
const val = document.getElementById("input-text").value.trim().toLowerCase(); const val = document.getElementById("input-text").value.trim().toLowerCase();
const pacSection = document.getElementById("pacman-section"); const pacSection = document.getElementById("pacman-section");
@@ -210,6 +299,7 @@ function checkForPacman() {
} }
} }
function startPacman() { } function startPacman() { }
function exitGame() { } function exitGame() { }
+24 -13
View File
@@ -3,30 +3,43 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>403 - PacCrypt</title> <meta name="description" content="PacCrypt - 403 Forbidden Access" />
<title>403 Forbidden - PacCrypt</title>
<!-- Favicon -->
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}" /> <link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}" />
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet" />
<!-- Stylesheets -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" /> <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<!-- Scripts -->
<script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script> <script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script>
</head> </head>
<body class="dark"> <body class="dark">
<!-- Header -->
<header class="card logo-header">
<div class="logo-container">
<img src="{{ url_for('static', filename='img/PacCrypt.png') }}" alt="PacCrypt Logo" />
<div class="logo-text">
<h1>PACCRYPT</h1>
<p>Securely Share Text and Files</p>
</div>
</div>
</header>
<header class="card mb-5"> <!-- Main Content -->
<h1>PacCrypt</h1>
<p>Secure Encoding, Encryption and Password Generation</p>
</header>
<main> <main>
<section class="card form-group" style="padding: 50px 30px;"> <section class="card form-group" style="padding: 50px 30px;">
<h2 style="color: #00ff99; font-size: 2.5em;">🚫 403 - Forbidden</h2> <h2 style="color: #00ff99; font-size: 2.5em;">403 - Forbidden</h2>
<p class="mt-4" style="font-size: 1.2em; color: #cccccc;"> <p class="mt-4" style="font-size: 1.2em; color: #cccccc;">
Looks like this area is locked behind a secret ghost door! 🛡️👻 Looks like this area is locked behind a secret ghost door!
</p> </p>
<!-- Navigation -->
<div class="button-group mt-4"> <div class="button-group mt-4">
<a href="{{ url_for('index') }}"> <a href="{{ url_for('index') }}">
<button type="button">⬅️ Return Home</button> <button type="button">Return Home</button>
</a> </a>
</div> </div>
</section> </section>
@@ -36,10 +49,8 @@
<footer> <footer>
<p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p> <p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p>
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link"> <a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
<img src="\static\img\Github_logo.png" <img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
alt="GitHub Logo" width="100" />
</a> </a>
</footer> </footer>
</body> </body>
</html> </html>
+23 -12
View File
@@ -3,27 +3,40 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>404 - PacCrypt</title> <meta name="description" content="PacCrypt - 404 Page Not Found" />
<title>404 Not Found - PacCrypt</title>
<!-- Favicon -->
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}" /> <link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}" />
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet" />
<!-- Stylesheets -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" /> <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<!-- Scripts -->
<script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script> <script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script>
</head> </head>
<body class="dark"> <body class="dark">
<!-- Header -->
<header class="card logo-header">
<div class="logo-container">
<img src="{{ url_for('static', filename='img/PacCrypt.png') }}" alt="PacCrypt Logo" />
<div class="logo-text">
<h1>PACCRYPT</h1>
<p>Securely Share Text and Files</p>
</div>
</div>
</header>
<header class="card mb-5"> <!-- Main Content -->
<h1>PacCrypt</h1>
<p>Secure Encoding, Encryption and Password Generation</p>
</header>
<main> <main>
<section class="card form-group" style="padding: 50px 30px;"> <section class="card form-group" style="padding: 50px 30px;">
<h2 style="color: #ff0066; font-size: 2.5em;">404 - Not Found</h2> <h2 style="color: #ff0066; font-size: 2.5em;">404 - Not Found</h2>
<p class="mt-4" style="font-size: 1.2em; color: #cccccc;"> <p class="mt-4" style="font-size: 1.2em; color: #cccccc;">
Whoops! That page doesnt seem to exist. Maybe it got encrypted? 🧩🔐 Whoops! That page doesn't seem to exist. Maybe it got encrypted?
</p> </p>
<!-- Navigation -->
<div class="button-group mt-4"> <div class="button-group mt-4">
<a href="{{ url_for('index') }}"> <a href="{{ url_for('index') }}">
<button type="button">⬅️ Return Home</button> <button type="button">⬅️ Return Home</button>
@@ -36,10 +49,8 @@
<footer> <footer>
<p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p> <p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p>
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link"> <a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
<img src="\static\img\Github_logo.png" <img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
alt="GitHub Logo" width="100" />
</a> </a>
</footer> </footer>
</body> </body>
</html> </html>
+22 -11
View File
@@ -3,28 +3,41 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>500 - PacCrypt</title> <meta name="description" content="PacCrypt - 500 Internal Server Error" />
<title>500 Server Error - PacCrypt</title>
<!-- Favicon -->
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}" /> <link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}" />
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet" />
<!-- Stylesheets -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" /> <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<!-- Scripts -->
<script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script> <script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script>
</head> </head>
<body class="dark"> <body class="dark">
<!-- Header -->
<header class="card logo-header">
<div class="logo-container">
<img src="{{ url_for('static', filename='img/PacCrypt.png') }}" alt="PacCrypt Logo" />
<div class="logo-text">
<h1>PACCRYPT</h1>
<p>Securely Share Text and Files</p>
</div>
</div>
</header>
<header class="card mb-5"> <!-- Main Content -->
<h1>PacCrypt</h1>
<p>Secure Encoding, Encryption and Password Generation</p>
</header>
<main> <main>
<section class="card form-group" style="padding: 50px 30px;"> <section class="card form-group" style="padding: 50px 30px;">
<h2 style="color: #ff3300; font-size: 2.5em;">💥 500 - Server Error</h2> <h2 style="color: #ff3300; font-size: 2.5em;">💥 500 - Server Error</h2>
<p class="mt-4" style="font-size: 1.2em; color: #cccccc;"> <p class="mt-4" style="font-size: 1.2em; color: #cccccc;">
Uh oh! The ghosts chomped the server wires. 🧟‍♂️👾 Uh oh! The ghosts chomped the server wires. 🧟‍♂️👾
Were working on patching it up. We're working on patching it up.
</p> </p>
<!-- Navigation -->
<div class="button-group mt-4"> <div class="button-group mt-4">
<a href="{{ url_for('index') }}"> <a href="{{ url_for('index') }}">
<button type="button">⬅️ Return Home</button> <button type="button">⬅️ Return Home</button>
@@ -37,10 +50,8 @@
<footer> <footer>
<p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p> <p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p>
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link"> <a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
<img src="\static\img\Github_logo.png" <img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
alt="GitHub Logo" width="100" />
</a> </a>
</footer> </footer>
</body> </body>
</html> </html>
+193 -92
View File
@@ -1,135 +1,117 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="PacCrypt - Admin Panel" />
<title>Admin Panel - PacCrypt</title> <title>Admin Panel - PacCrypt</title>
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet"> <!-- Favicon -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}"> <link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}" />
<!-- Stylesheets -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<!-- Scripts -->
<script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script> <script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script>
</head> </head>
<body class="dark"> <body class="dark">
<header class="card mb-5"> <!-- Header -->
<h1>PacCrypt Admin Panel</h1> <header class="card logo-header">
<p>Site Overview & Controls</p> <div class="logo-container">
</header> <img src="{{ url_for('static', filename='img/PacCrypt.png') }}" alt="PacCrypt Logo" />
<div class="logo-text">
<h1>PACCRYPT</h1>
<p>ADMIN PANEL</p>
</div>
</div>
</header>
<!-- Main Content -->
<main> <main>
<!-- Site Map Section -->
<section id="sitemap-section" class="card form-group">
<h2>Server Management</h2>
<!-- Site Map --> <div class="sitemap-header">
<section class="card form-group"> <button onclick="toggleSitemap()" style="margin-bottom: 10px;">Show Site Map</button>
<h2>🔍 Site Map</h2>
<ul style="list-style: none; padding-left: 0;">
{% for route in routes %}
<li style="margin-bottom: 5px;">🔗 <code>{{ route }}</code></li>
{% endfor %}
</ul>
<div class="button-group mt-4">
<a href="{{ url_for('restart_server') }}">
<button type="button">🔁 Restart Server</button>
</a>
<a href="{{ url_for('admin_logout') }}">
<button type="button">🚪 Log Out</button>
</a>
</div> </div>
<form action="{{ url_for('admin_reset') }}" method="POST" onsubmit="return confirm('Are you sure you want to reset admin credentials?');"> <div id="sitemap-list" class="sitemap-content" style="display: none;">
<button type="submit" class="mt-4" style="background-color: #b90000;">⚠️ Reset Admin</button> <ul style="list-style: none; padding-left: 0;">
</form> {% for route in routes %}
<li style="margin-bottom: 5px;"><code>{{ route }}</code></li>
{% endfor %}
</ul>
</div>
<form action="{{ url_for('admin_clear_uploads') }}" method="POST" <!-- Server Management Buttons -->
onsubmit="return confirm('Are you sure you want to delete ALL uploaded files?');"> <div class="admin-button-grid">
<button type="submit">🗑 Clear All Uploaded Files</button> <button onclick="restartServer()">Restart Server</button>
</form> <form action="{{ url_for('admin_logout') }}" method="GET" style="display: inline;">
<button type="submit">Log Out</button>
</form>
<button onclick="updateServer()">Pull Latest Changes</button>
<form action="{{ url_for('admin_settings') }}" method="GET" style="display: inline;">
<button type="submit">Manage Upload Settings</button>
</form>
<button onclick="resetAdmin()" class="danger-button">Reset Admin</button>
<button onclick="clearUploads()" class="danger-button">Clear Uploaded Files</button>
</div>
{% with messages = get_flashed_messages(with_categories=true, category_filter=['clear-feedback']) %}
{% for category, message in messages %}
<div class="copy-feedback show">{{ message }}</div>
{% endfor %}
{% endwith %}
<!-- Flash Messages -->
<div id="admin-feedback" class="copy-feedback" style="display: none;"></div>
</section> </section>
<!-- Change Admin Password --> <!-- Password Change Section -->
<section class="card form-group mt-5"> <section id="password-change-section" class="card form-group">
<h2>🔑 Change Admin Password</h2> <h2>Change Admin Password</h2>
<!-- Password Feedback -->
{% with messages = get_flashed_messages(with_categories=true, category_filter=['password-feedback']) %} {% with messages = get_flashed_messages(with_categories=true, category_filter=['password-feedback']) %}
{% for category, message in messages %} {% for category, message in messages %}
<div class="copy-feedback show">{{ message }}</div> <div class="copy-feedback show">{{ message }}</div>
{% endfor %} {% endfor %}
{% endwith %} {% endwith %}
<!-- Password Change Form -->
<form method="POST" action="{{ url_for('admin_change_password') }}"> <form method="POST" action="{{ url_for('admin_change_password') }}">
<input type="password" name="current_password" placeholder="Current Password" required> <input type="password" name="current_password" placeholder="Current Password" required />
<input type="password" name="new_password" placeholder="New Password" required> <input type="password" name="new_password" placeholder="New Password" required />
<button type="submit">Update Password</button> <button type="submit">Update Password</button>
</form> </form>
</section> </section>
<!-- Update Server --> <!-- Server Status Section -->
<section class="card form-group mt-5"> <section id="server-status-section" class="card form-group">
<h2>📦 Update Server from GitHub</h2> <h2>Server Status</h2>
<form method="POST" action="{{ url_for('admin_update_server') }}"> <ul style="width: 400px;">
<button type="submit" onclick="return confirm('Are you sure you want to pull the latest changes from GitHub?')"> <li>Uptime: <code>0 days, 11 hours, 47 minutes</code></li>
🔁 Pull Latest Changes <li>Server Time: <code>2025-05-14 14:32:18</code></li>
</button> <li>Python Version: <code>3.13.3</code></li>
</form> <li>Flask Debug Mode: <code>True</code></li>
{% with update_msgs = get_flashed_messages(category_filter=["update"]) %}
{% if update_msgs %}
<div class="copy-feedback show" style="margin-top: 12px;">
{{ update_msgs[0] | safe }}
</div>
{% endif %}
{% endwith %}
</section>
<!-- Server Status -->
<section class="card form-group mt-5">
<h2>📊 Server Status</h2>
<ul style="list-style: none; padding-left: 0;">
<li>🕒 Uptime: <code>{{ server_info.uptime }}</code></li>
<li>📅 Server Time: <code>{{ server_info.time }}</code></li>
<li>🐍 Python Version: <code>{{ server_info.python }}</code></li>
<li>⚙️ Flask Debug Mode: <code>{{ server_info.debug }}</code></li>
</ul> </ul>
</section> </section>
<!-- Server Logs --> <!-- Server Logs Section -->
<section class="card form-group mt-5"> <section id="server-logs-section" class="card form-group">
<h2>📜 Server Logs</h2> <h2>Server Logs</h2>
<button onclick="toggleLogs()" style="margin-bottom: 10px;">🔽 Show/Hide Logs</button> <button onclick="toggleLogs()" style="margin-bottom: 10px;">Show/Hide Logs</button>
<div id="logLoader" style="display: none; margin-bottom: 10px;">Loading logs...</div> <div id="logLoader" style="display: none; margin-bottom: 10px;">Loading logs...</div>
<pre id="logContainer" style="display: none; max-height: 400px; overflow-y: auto; background: black; color: lime; padding: 10px; border-radius: 8px;"></pre> <pre id="logContainer" style="display: none;"></pre>
</section> </section>
<!-- System Settings -->
<section class="card form-group mt-5">
<h2>🧩 System Configuration</h2>
<p>You can manage upload storage, limits, and expiration policies here:</p>
<a href="{{ url_for('admin_settings') }}">
<button type="button">🛠️ Manage Upload Settings</button>
</a>
</section>
</main>
<!-- Footer --> <!-- Footer -->
<footer> <footer>
<p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p> <p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p>
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link"> <a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
<img src="\static\img\Github_logo.png" <img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
alt="GitHub Logo" width="100" />
</a> </a>
<a href="{{ url_for('sitemap') }}"> <a href="{{ url_for('sitemap') }}">
<img src="\static\img\sitemap.png" <img src="\static\img\sitemap.png" alt="Sitemap Png" width="55" />
alt="Sitemap Png" width="55" />
</a> </a>
</footer> </footer>
@@ -140,10 +122,10 @@
const logLoader = document.getElementById('logLoader'); const logLoader = document.getElementById('logLoader');
if (logContainer.style.display === 'none') { if (logContainer.style.display === 'none') {
logLoader.style.display = 'block'; logLoader.style.display = 'block';
const response = await fetch('{{ url_for('admin_logs') }}'); const response = await fetch("{{ url_for('admin_logs') }}");
const data = await response.json(); const data = await response.json();
logLoader.style.display = 'none'; logLoader.style.display = 'none';
logContainer.innerText = data.logs.join('\\n'); logContainer.innerText = data.logs.join('\n');
logContainer.style.display = 'block'; logContainer.style.display = 'block';
} else { } else {
logContainer.style.display = 'none'; logContainer.style.display = 'none';
@@ -151,5 +133,124 @@
} }
</script> </script>
<script>
function toggleSitemap() {
const list = document.getElementById('sitemap-list');
const button = document.querySelector('.sitemap-header button');
if (list.style.display === 'none') {
list.style.display = 'block';
button.textContent = 'Hide Site Map';
} else {
list.style.display = 'none';
button.textContent = 'Show Site Map';
}
}
async function restartServer() {
if (!confirm('Are you sure you want to restart the server? This will temporarily disconnect all users.')) return;
try {
const response = await fetch('{{ url_for("restart_server") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok) {
showFeedback(data.message);
// Add a small delay before redirecting to allow the server to restart
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
showFeedback(data.error || 'Failed to restart server.');
}
} catch (error) {
showFeedback('Failed to restart server.');
}
}
async function updateServer() {
if (!confirm('Are you sure you want to pull the latest changes from GitHub?')) return;
try {
const response = await fetch('{{ url_for("admin_update_server") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok) {
showFeedback(data.message);
} else {
showFeedback(data.error || 'Failed to update server from GitHub.');
}
} catch (error) {
showFeedback('Failed to update server from GitHub.');
}
}
async function resetAdmin() {
if (!confirm('Are you sure you want to reset admin credentials?')) return;
try {
const response = await fetch('{{ url_for("admin_reset") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
showFeedback('Admin credentials reset. Please create new credentials.');
setTimeout(() => {
window.location.href = '{{ url_for("admin_setup") }}';
}, 2000);
}
} catch (error) {
showFeedback('Failed to reset admin credentials.');
}
}
async function clearUploads() {
if (!confirm('Are you sure you want to delete ALL uploaded files?')) return;
try {
const response = await fetch('{{ url_for("admin_clear_uploads") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
showFeedback('All uploaded files have been cleared.');
}
} catch (error) {
showFeedback('Failed to clear uploaded files.');
}
}
function showFeedback(message) {
const feedback = document.getElementById('admin-feedback');
feedback.textContent = message;
feedback.style.display = 'block';
feedback.classList.add('show');
setTimeout(() => {
feedback.classList.remove('show');
setTimeout(() => {
feedback.style.display = 'none';
}, 300);
}, 3000);
}
</script>
</body> </body>
</html> </html>
+24 -14
View File
@@ -1,34 +1,45 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Admin Login - PacCrypt</title> <meta name="description" content="PacCrypt - Admin Login" />
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}"> <title>Admin Login - PacCrypt</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}"> <!-- Favicon -->
<script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script> <link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}" />
<!-- Stylesheets -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<!-- Scripts -->
<script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script>
</head> </head>
<body class="dark"> <body class="dark">
<!-- Header -->
<header class="card mb-5"> <header class="card">
<h1>PacCrypt Admin</h1> <h1>PacCrypt Admin</h1>
<p>Administrator Login</p> <p>Administrator Login</p>
</header> </header>
<!-- Main Content -->
<main> <main>
<!-- Login Form Section -->
<section class="card form-group"> <section class="card form-group">
<h2>🔑 Admin Login</h2> <h2>🔑 Admin Login</h2>
<!-- Flash Messages -->
{% with messages = get_flashed_messages() %} {% with messages = get_flashed_messages() %}
{% if messages %} {% if messages %}
<p style="color: red;">{{ messages[0] }}</p> <p style="color: red;">{{ messages[0] }}</p>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
<!-- Login Form -->
<form method="POST"> <form method="POST">
<input type="text" name="username" placeholder="Username" required> <input type="text" name="username" placeholder="Username" required />
<input type="password" name="password" placeholder="Password" required> <input type="password" name="password" placeholder="Password" required />
<div class="button-group mt-3"> <div class="button-group mt-3">
<button type="submit">🚪 Log In</button> <button type="submit">🚪 Log In</button>
</div> </div>
@@ -36,13 +47,12 @@
</section> </section>
</main> </main>
<!-- Footer -->
<footer> <footer>
<p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p> <p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p>
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link"> <a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
<img src="\static\img\Github_logo.png" <img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
alt="GitHub Logo" width="100" />
</a> </a>
</footer> </footer>
</body> </body>
</html> </html>
+23 -14
View File
@@ -1,24 +1,33 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="PacCrypt - Admin Settings" />
<title>Admin Settings - PacCrypt</title> <title>Admin Settings - PacCrypt</title>
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet"> <!-- Favicon -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}"> <link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}" />
<!-- Stylesheets -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
</head> </head>
<body class="dark"> <body class="dark">
<!-- Header -->
<header class="card mb-5"> <header class="card">
<h1>PacCrypt Admin Settings</h1> <h1>PacCrypt Admin Settings</h1>
<p>Manage upload configuration securely</p> <p>Manage upload configuration</p>
</header> </header>
<!-- Main Content -->
<main> <main>
<!-- Settings Form Section -->
<section class="card form-group"> <section class="card form-group">
<h2>⚙️ Upload Settings</h2> <h2>⚙️ Upload Settings</h2>
<!-- Flash Messages -->
{% with messages = get_flashed_messages() %} {% with messages = get_flashed_messages() %}
{% if messages %} {% if messages %}
<ul style="color: lime;"> <ul style="color: lime;">
@@ -29,16 +38,18 @@
{% endif %} {% endif %}
{% endwith %} {% endwith %}
<!-- Settings Form -->
<form method="POST"> <form method="POST">
<label for="upload_folder">Upload Folder Path:</label> <label for="upload_folder">Upload Folder Path:</label>
<input type="text" name="upload_folder" id="upload_folder" value="{{ settings.upload_folder }}" required> <input type="text" name="upload_folder" id="upload_folder" value="{{ settings.upload_folder }}" required />
<label for="max_file_age_days">Max File Age (Days):</label> <label for="max_file_age_days">Max File Age (Days):</label>
<input type="number" name="max_file_age_days" id="max_file_age_days" value="{{ settings.max_file_age_days }}" min="1" required> <input type="number" name="max_file_age_days" id="max_file_age_days" value="{{ settings.max_file_age_days }}" min="1" required />
<label for="max_file_size_gb">Max File Size (GB):</label> <label for="max_file_size_gb">Max File Size (GB):</label>
<input type="number" name="max_file_size_gb" id="max_file_size_gb" value="{{ settings.max_file_size_bytes // (1024 * 1024 * 1024) }}" step="0.1" min="0.1" required> <input type="number" name="max_file_size_gb" id="max_file_size_gb" value="{{ settings.max_file_size_bytes // (1024 * 1024 * 1024) }}" step="0.1" min="0.1" required />
<!-- Action Buttons -->
<div class="button-group mt-4"> <div class="button-group mt-4">
<button type="submit">💾 Save Settings</button> <button type="submit">💾 Save Settings</button>
<a href="{{ url_for('admin_page') }}"> <a href="{{ url_for('admin_page') }}">
@@ -53,10 +64,8 @@
<footer> <footer>
<p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p> <p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p>
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link"> <a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
<img src="\static\img\Github_logo.png" <img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
alt="GitHub Logo" width="100" />
</a> </a>
</footer> </footer>
</body> </body>
</html> </html>
+32 -18
View File
@@ -1,36 +1,52 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Admin Setup - PacCrypt</title> <meta name="description" content="PacCrypt - Admin Setup" />
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}"> <title>PacCrypt - Admin Setup</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}"> <!-- Favicon -->
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}" />
<!-- Stylesheets -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<!-- Scripts -->
<script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script> <script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script>
</head> </head>
<body class="dark"> <body class="dark">
<!-- Header -->
<header class="card logo-header">
<div class="logo-container">
<img src="{{ url_for('static', filename='img/PacCrypt.png') }}" alt="PacCrypt Logo" />
<div class="logo-text">
<h1>PACCRYPT</h1>
<p>Admin Setup</p>
</div>
</div>
</header>
<header class="card mb-5"> <!-- Main Content -->
<h1>PacCrypt Admin</h1>
<p>Secure Admin Setup</p>
</header>
<main> <main>
<!-- Setup Form Section -->
<section class="card form-group"> <section class="card form-group">
<h2>🛡️ Create Admin Account</h2> <h2>Create Admin Account</h2>
<!-- Flash Messages -->
{% with messages = get_flashed_messages() %} {% with messages = get_flashed_messages() %}
{% if messages %} {% if messages %}
<p style="color: red;">{{ messages[0] }}</p> <p style="color: red;">{{ messages[0] }}</p>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
<!-- Setup Form -->
<form method="POST"> <form method="POST">
<input type="text" name="username" placeholder="Username" required> <input type="text" name="username" placeholder="Username" required />
<input type="password" name="password" placeholder="Password" required> <input type="password" name="password" placeholder="Password" required />
<div class="button-group mt-3"> <div class="button-group mt-3">
<button type="submit">📝 Set Credentials</button> <button type="submit">Set Credentials</button>
</div> </div>
</form> </form>
</section> </section>
@@ -40,10 +56,8 @@
<footer> <footer>
<p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p> <p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p>
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link"> <a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
<img src="\static\img\Github_logo.png" <img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
alt="GitHub Logo" width="100" />
</a> </a>
</footer> </footer>
</body> </body>
</html> </html>
+116 -50
View File
@@ -3,31 +3,43 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="PacCrypt - Secure text and file encryption with password generation" />
<title>PacCrypt</title> <title>PacCrypt</title>
<!-- Favicon -->
<link rel="icon" href="{{ url_for('static', filename='img/PacCrypt.png') }}" type="image/png" /> <link rel="icon" href="{{ url_for('static', filename='img/PacCrypt.png') }}" type="image/png" />
<!-- Stylesheets -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" /> <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<!-- Scripts -->
<script type="module" src="{{ url_for('static', filename='js/main.js') }}" defer></script> <script type="module" src="{{ url_for('static', filename='js/main.js') }}" defer></script>
</head> </head>
<body class="dark"> <body class="dark">
<!-- Header -->
<header class="card logo-header">
<div class="logo-container">
<img src="{{ url_for('static', filename='img/PacCrypt.png') }}" alt="PacCrypt Logo" />
<div class="logo-text">
<h1>PACCRYPT</h1>
<p>Securely Share Text and Files</p>
</div>
</div>
</header>
<header class="card mb-5"> <!-- Main Content -->
<h1>PacCrypt</h1>
<p>Encrypt and share your text or files securely</p>
</header>
<main> <main>
<!-- Password Generator Section -->
<!-- Password Generator --> <section id="password-generator-section" class="card form-group">
<section class="card form-group mt-5"> <h2>Password Generator</h2>
<h2>🔑 Password Generator</h2>
<div class="form-group"> <div class="form-group">
<input type="text" id="generated-password" readonly /> <input type="text" id="generated-password" readonly />
<div class="button-group"> <div class="button-group">
<button type="button" id="generate-btn">🎲 Generate</button> <button type="button" id="generate-btn">Generate</button>
<button type="button" id="copy-btn">📋 Copy</button> <button type="button" id="copy-btn">Copy Password</button>
</div> </div>
<div id="password-copy-feedback" class="copy-feedback">Copied to clipboard!</div> <div id="password-copy-feedback" class="copy-feedback">Password copied to clipboard!</div>
</div> </div>
</section> </section>
@@ -43,12 +55,11 @@
</div> </div>
</section> </section>
<!-- Encryption/Decryption Section -->
<!-- Encrypt & Decrypt Section --> <section id="encoding-section" class="card form-group">
<section class="card form-group" id="encoding-section"> <h2>Encrypt & Decrypt</h2>
<h2>🔐 Encrypt & Decrypt</h2>
<form id="crypto-form" class="form-group"> <form id="crypto-form" class="form-group">
<!-- Encryption Type Selection -->
<div class="form-group"> <div class="form-group">
<label for="encryption-type">Encryption Type:</label> <label for="encryption-type">Encryption Type:</label>
<select id="encryption-type"> <select id="encryption-type">
@@ -57,46 +68,55 @@
</select> </select>
</div> </div>
<!-- Operation Toggle -->
<div class="toggle-container"> <div class="toggle-container">
<span id="toggle-left-label">Encrypt</span> <span class="toggle-label">Encrypt</span>
<label class="switch"> <label class="material-switch">
<input type="checkbox" id="operation-toggle" /> <input type="checkbox" id="operation-toggle">
<span class="slider round"></span> <span class="material-slider"></span>
</label> </label>
<span id="toggle-right-label">Decrypt</span> <span class="toggle-label">Decrypt</span>
</div> </div>
<!-- Text Input Section -->
<div id="text-section" class="form-group"> <div id="text-section" class="form-group">
<textarea id="input-text" placeholder="Enter your message..."></textarea> <textarea id="input-text" placeholder="Enter your message..."></textarea>
</div> </div>
<!-- Password Input -->
<div id="password-input" class="form-group"> <div id="password-input" class="form-group">
<input type="password" id="password" placeholder="Password (AES only)" /> <input type="password" id="password" placeholder="Encryption/Decryption Password" />
</div> </div>
<!-- File Input Section -->
<div id="file-section" class="form-group" style="display: none;"> <div id="file-section" class="form-group" style="display: none;">
<input type="file" id="file-input" /> <input type="file" id="file-input" />
<button type="button" id="remove-file-btn">🗑 Remove File</button> <button type="button" id="remove-file-btn">Remove File</button>
</div> </div>
<div class="button-group mt-3"> <!-- Action Buttons -->
<button type="submit">⚡ Execute</button>
<button type="button" id="clear-all-btn">🧹 Clear All</button>
</div>
<textarea id="output-text" readonly placeholder="Result will appear here..."></textarea>
<div class="button-group"> <div class="button-group">
<button type="button" id="copy-output-btn">📋 Copy Output</button> <button type="submit">Execute</button>
<button type="button" id="copy-output-btn">Copy Output</button>
</div> </div>
<div id="output-copy-feedback" class="copy-feedback">Copied to clipboard!</div>
<!-- Output Section -->
<textarea id="output-text" readonly placeholder="Encrypted/Decrypted Output"></textarea>
<div class="button-group">
<button type="button" id="clear-all-btn" class="danger-button">Clear All</button>
</div>
<div id="output-copy-feedback" class="copy-feedback">Text copied to clipboard!</div>
</form> </form>
</section> </section>
<!-- PacCrypt Sharing --> <!-- File Sharing Section -->
<section class="card form-group mt-5"> <section id="sharing-section" class="card form-group">
<h2>📤 PacCrypt Sharing</h2> <h2>PacCrypt Share</h2>
<p>Securely share a file with encryption and a pickup password.</p> <h3>Securely share encrypted files.</h3>
<p>Do not lose your passwords, data will be lost forever!</p>
<!-- Flash Messages -->
{% with messages = get_flashed_messages() %} {% with messages = get_flashed_messages() %}
{% if messages %} {% if messages %}
<ul style="color: lime; list-style: none; padding-left: 0;"> <ul style="color: lime; list-style: none; padding-left: 0;">
@@ -104,10 +124,12 @@
<li> <li>
{{ message | safe }} {{ message | safe }}
{% if "pickup" in message %} {% if "pickup" in message %}
<br /> <div class="share-link-container">
<span id="shared-link">{{ message.split(" at ")[1] }}</span> <a id="share-link" href="{{ message.split(' at ')[1] }}" target="_blank">{{ message.split(" at ")[1] }}</a>
<button type="button" id="copy-shared-link">📋 Copy Link</button> <!--- <span id="share-link">{{ message.split(" at ")[1] }}</span> --->
<div id="shared-link-feedback" class="copy-feedback">Copied to clipboard!</div> <button type="button" id="copy-share-btn">Copy Link</button>
<div id="shared-link-feedback" class="copy-feedback">Link copied to clipboard!</div>
</div>
{% endif %} {% endif %}
</li> </li>
{% endfor %} {% endfor %}
@@ -116,31 +138,75 @@
{% endif %} {% endif %}
{% endwith %} {% endwith %}
<form method="POST" enctype="multipart/form-data" class="form-group"> <!-- File Upload Form -->
<!-- Share Link Container (initially hidden) -->
<div class="share-link-container" id="share-link-container" style="display: none;">
<a id="share-link" href="#" target="_blank"></a>
<button type="button" id="copy-share-btn">Copy Link</button>
<div id="shared-link-feedback" class="copy-feedback">Link copied to clipboard!</div>
</div>
<form method="POST" enctype="multipart/form-data" class="form-group" id="upload-form">
<input type="file" name="file" id="upload-file" required /> <input type="file" name="file" id="upload-file" required />
<input type="password" name="enc_password" placeholder="Encryption Password" required /> <input type="password" name="enc_password" placeholder="Encryption/Decryption Password" required />
<input type="password" name="pickup_password" placeholder="Pickup Password" required /> <input type="password" name="pickup_password" placeholder="Pickup Password" required />
<div class="button-group mt-3"> <div class="button-group">
<button type="submit">🔒 Upload and Generate Link</button> <button type="submit">Upload and Generate Link</button>
</div> </div>
</form> </form>
<script>
document.getElementById('upload-form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
try {
const response = await fetch('/', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.error) {
alert(data.error);
return;
}
if (data.success && data.pickup_url) {
const shareLink = document.getElementById('share-link');
const shareLinkContainer = document.getElementById('share-link-container');
shareLink.href = data.pickup_url;
shareLink.textContent = data.pickup_url;
shareLinkContainer.style.display = 'flex';
// Clear form fields
document.getElementById('upload-file').value = '';
document.getElementsByName('enc_password')[0].value = '';
document.getElementsByName('pickup_password')[0].value = '';
// Scroll to the share link
shareLinkContainer.scrollIntoView({ behavior: 'smooth' });
}
} catch (error) {
alert('Error uploading file: ' + error.message);
}
});
</script>
<!-- File Limits Information -->
<p class="text-muted mt-3" style="font-size: 0.85em;"> <p class="text-muted mt-3" style="font-size: 0.85em;">
Files expire after {{ settings.max_file_age_days }} days.<br /> Files expire after {{ settings.max_file_age_days }} days.<br />
Max file size: {{ settings.max_file_size_bytes // (1024 * 1024 * 1024) }} GB. Max file size: {{ settings.max_file_size_bytes // (1024 * 1024 * 1024) }} GB.
</p> </p>
</section> </section>
</main> </main>
<!-- Footer --> <!-- Footer -->
<footer> <footer>
<p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p> <p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p>
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link"> <a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
<img src="\static\img\Github_logo.png" <img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
alt="GitHub Logo" width="100" />
</a> </a>
</footer> </footer>
</body> </body>
</html> </html>
+60 -22
View File
@@ -1,27 +1,35 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Pickup File - PacCrypt</title> <meta name="description" content="PacCrypt - Secure file pickup and decryption" />
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}"> <title>PacCrypt - Secure File Pickup</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}"> <!-- Favicon -->
<link rel="icon" href="{{ url_for('static', filename='img/PacCrypt.png') }}" type="image/png" />
<!-- Stylesheets -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
</head> </head>
<body class="dark"> <body class="dark">
<!-- Header -->
<header class="card mb-5"> <header class="card">
<h1>PacCrypt Pickup</h1> <h1>PacCrypt</h1>
<p>Enter passwords to retrieve your file securely</p> <p>Secure File Pickup and Decryption</p>
</header> </header>
<!-- Main Content -->
<main> <main>
<section class="card form-group"> <!-- File Pickup Section -->
<h2>🔐 Decrypt and Download</h2> <section id="pickup-section" class="card form-group">
<h2>🔐 File Pickup</h2>
<!-- Flash Messages -->
{% with messages = get_flashed_messages() %} {% with messages = get_flashed_messages() %}
{% if messages %} {% if messages %}
<ul style="color: red;"> <ul style="color: lime; list-style: none; padding-left: 0;">
{% for message in messages %} {% for message in messages %}
<li>{{ message }}</li> <li>{{ message }}</li>
{% endfor %} {% endfor %}
@@ -29,17 +37,49 @@
{% endif %} {% endif %}
{% endwith %} {% endwith %}
<form method="POST"> <!-- File Info -->
<input type="password" name="pickup_password" placeholder="Pickup Password" required> <div class="form-group">
<input type="password" name="enc_password" placeholder="Encryption Password" required> <p style="color: #00ff99; margin-bottom: 15px;">File ID: <code>{{ file_id }}</code></p>
<div class="button-group mt-3"> </div>
<!-- Pickup Form -->
<form method="POST" class="form-group">
<div class="form-group">
<input type="password"
name="pickup_password"
placeholder="Pickup Password"
required
autocomplete="off" />
</div>
<div class="form-group">
<input type="password"
name="enc_password"
placeholder="Encryption Password"
required
autocomplete="off" />
</div>
<div class="button-group">
<button type="submit">📥 Decrypt and Download</button> <button type="submit">📥 Decrypt and Download</button>
</div> </div>
</form> </form>
</section> </section>
<section class="card form-group mt-5"> <!-- Security Notice Section -->
<p style="font-size: 0.9em; color: gray;">Link ID: <code>{{ file_id }}</code></p> <section id="security-notice-section" class="card form-group">
<h2>🛡️ Security Notice</h2>
<p style="color: #00ff99; text-align: center;">
Make sure you're on the correct domain before entering any passwords.<br>
Your file will be permanently deleted after download.
</p>
</section>
<!-- Link ID Section -->
<section id="link-id-section" class="card form-group">
<p style="color: #00ff99; text-align: center; font-family: monospace; font-size: 1.1em;">
Link ID: <code>{{ file_id }}</code>
</p>
</section> </section>
</main> </main>
@@ -47,10 +87,8 @@
<footer> <footer>
<p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p> <p>&copy; 2025 UnNaturalll-Dev. All rights reserved.</p>
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link"> <a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
<img src="\static\img\Github_logo.png" <img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
alt="GitHub Logo" width="100" />
</a> </a>
</footer> </footer>
</body> </body>
</html> </html>