Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f2d56c05a | |||
| bb8690b74f | |||
| ed11ccd2a1 | |||
| db00538aee | |||
| 05a9ada8d9 | |||
| 61193320d4 | |||
| 1c1fed1dd5 | |||
| 1edd1c858c | |||
| 1d55d4f4ce | |||
| 7aefd5aff8 | |||
| 271b4cdc91 | |||
| 90dcb7ecb8 | |||
| 7ec213fad0 | |||
| 766386501b | |||
| 6ad2b65aba | |||
| 265dff3329 | |||
| 9e45c34365 |
@@ -1,77 +1,268 @@
|
||||
# PacCrypt WebApp
|
||||
# PacCrypt
|
||||
|
||||
**PacCrypt** is a web-based application designed to provide secure encoding, encryption, and password generation. It allows users to easily encrypt and decrypt text and files, with both basic and advanced encryption options. It also features a password generator and a simple Pac-Man game as an Easter egg!
|
||||
**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! 🕹️
|
||||
|
||||
## Features
|
||||
Officially Hosted Here: [paccrypt.unnaturalll.dev](http://paccrypt.unnaturalll.dev)
|
||||
|
||||
- **Basic and Advanced Encryption**: Choose between simple encryption (Caesar Cipher) or more secure AES-GCM encryption.
|
||||
- **File Encryption/Decryption**: Encrypt or decrypt files with a password.
|
||||
- **Password Generator**: Generate secure random passwords with customizable length and complexity.
|
||||
- **Pac-Man Game**: A fun Easter egg! Play a Pac-Man game when you type "pacman" in the text area.
|
||||
- **Copy to Clipboard**: Copy generated passwords or encrypted results with one click.
|
||||
- **Responsive Design**: Fully responsive web design that works across different screen sizes.
|
||||
---
|
||||
|
||||
## Installation
|
||||
## ✨ Features
|
||||
|
||||
### Prerequisites
|
||||
- 🔒 Basic and Advanced Encryption for Text & Files
|
||||
- 📁 Secure File Uploads with Pickup Passwords
|
||||
- 🔑 Random Password Generator
|
||||
- 🎮 Hidden Pac-Man Game — type `pacman` to play
|
||||
- 🧠 Smart UI: Auto-switches input sections, toggles encryption labels
|
||||
- 📋 Clipboard Copy Feedback with styled status boxes
|
||||
- 🧾 Admin Panel:
|
||||
- Site map with live route list
|
||||
- Server restart & GitHub update button
|
||||
- Secure admin credential management
|
||||
- Server logs & upload cleanup
|
||||
- 🧩 System Settings Page for upload config
|
||||
- 📜 Custom 403, 404, and 500 Error Pages
|
||||
- 🤖 robots.txt and /sitemap for crawlers
|
||||
- 📱 Mobile-Responsive UI
|
||||
|
||||
- **Python 3.7+**
|
||||
- **Nginx** (for reverse proxy and SSL configuration)
|
||||
---
|
||||
|
||||
Official PacCrypt website: paccrypt.unnaturalll.dev
|
||||
## 👨💻 Installation
|
||||
|
||||
### Steps to Set Up Locally (Windows)
|
||||
### 📋 Prerequisites
|
||||
|
||||
1. Clone the repository:
|
||||
- Python 3.7+
|
||||
- Flask 3+
|
||||
- Cryptography 42+
|
||||
- Waitress 2.1+
|
||||
- Git (For update feature)
|
||||
- Nginx (Recommended)
|
||||
- Cockpit (Recommended if hosted on **Linux**)
|
||||
|
||||
---
|
||||
|
||||
### ⚡ Quick Setup
|
||||
|
||||
```bash
|
||||
git clone https://github.com/TySP-Dev/PacCrypt.git
|
||||
cd paccrypt-webapp
|
||||
|
||||
2. Create and activate a virtual environment:
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate # On Windows, use `venv\Scripts\activate`
|
||||
|
||||
3. Install the required Python dependencies:
|
||||
cd paccrypt-webapp-final
|
||||
python -m venv venv
|
||||
source venv/bin/activate # or venv\Scripts\activate on Windows
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
4. Run the Flask app:
|
||||
python app.py
|
||||
Then run:
|
||||
|
||||
5. Open http://127.0.0.1:5000 to access the app locally.
|
||||
- Development Mode:
|
||||
```bash
|
||||
./start_dev.sh #<-- start_dev.bat (Windows)
|
||||
```
|
||||
|
||||
## Usage
|
||||
- Production Mode:
|
||||
```bash
|
||||
./start_prod.sh #<-- start_prod.bat (Windows)
|
||||
```
|
||||
|
||||
### Encryption and Decryption
|
||||
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
|
||||
|
||||
Select the encryption type (Basic or Advanced).
|
||||
---
|
||||
|
||||
For text encryption/decryption:
|
||||
## 🧭 Navigation & Usage
|
||||
|
||||
Enter text in the Input Text area.
|
||||
### 🔑 Generate Passwords
|
||||
|
||||
Choose whether to Encrypt or Decrypt.
|
||||
- Click Generate
|
||||
- Then hit `📋 Copy Password`
|
||||
- **Note:** This is also used as a seed generator for the Pac-Man *like* game
|
||||
|
||||
Enter a password (if using advanced encryption).
|
||||
### 🔐 Encrypt & Decrypt
|
||||
|
||||
For file encryption/decryption:
|
||||
- Choose between Basic Cipher or Advanced AES
|
||||
- Select mode using toggle (Encrypt/Decrypt)
|
||||
- Type your message or upload a file
|
||||
- Enter password (Advanced AES)
|
||||
- Hit Execute
|
||||
- Then hit `📋 Copy Output`
|
||||
|
||||
Upload a file.
|
||||
### 📤 Share Files
|
||||
|
||||
Enter a password for encryption/decryption.
|
||||
- Upload a file with two passwords:
|
||||
- Encryption password
|
||||
- Pickup password
|
||||
- Get a shareable URL and click `📋 Copy Link`
|
||||
|
||||
Click Encrypt or Decrypt.
|
||||
### 🎮 Pac-Man *like* Game
|
||||
|
||||
### Password Generation
|
||||
- Type `pacman` in the input box
|
||||
- Game appears with `Restart` and `Exit` buttons
|
||||
- Arrow key and Swipe controls 🕹️
|
||||
- Game restarts and a new seed is generated once all dots are eaten
|
||||
|
||||
Click the Generate button to create a random password, then use the Copy button to copy it to your clipboard.
|
||||
---
|
||||
|
||||
### Pac-Man Game (Easter Egg)
|
||||
## 🛠️ Admin Panel
|
||||
|
||||
Type the word "pacman" in the input box to unlock the Pac-Man game!
|
||||
Visit `/adminpage` after setting up credentials at `/admin-setup`.
|
||||
|
||||
### Contributing
|
||||
Features:
|
||||
- 🔄 Restart server
|
||||
- 🔃 Update from GitHub (git pull)
|
||||
- 🧽 Clear uploads
|
||||
- 🔐 Change admin password
|
||||
- 📝 View logs
|
||||
- ⚙️ Adjust upload settings
|
||||
|
||||
Feel free to open an issue or submit a pull request for improvements, bug fixes, or new features!
|
||||
---
|
||||
|
||||
### License
|
||||
## 🛡️ Deployment Tips
|
||||
##### I recommend using Linux as the host server, the follow confs are Linux focused
|
||||
The official PacCrypt host is **Debian** minimal install.
|
||||
|
||||
This project is open source and available under the MIT License.
|
||||
**HTTP** Nginx config (Not recommended):
|
||||
|
||||
```nginx
|
||||
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
|
||||
|
||||
# Hardened Proxy Settings
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
```
|
||||
PacCrypt/
|
||||
├── app.py
|
||||
├── requirements.txt
|
||||
├── README.md
|
||||
├── templates/
|
||||
│ ├── index.html
|
||||
│ ├── 404.html
|
||||
│ └── 403.html
|
||||
│ └── 500.html
|
||||
│ └── admin.html
|
||||
│ └── admin_login.html
|
||||
│ └── admin_settings.html
|
||||
│ └── admin_setup.html
|
||||
│ └── pickup.html
|
||||
├── static/
|
||||
│ ├── css/
|
||||
│ │ └── styles.css
|
||||
│ ├── js/
|
||||
│ │ └── ui.js
|
||||
│ │ └── pacman.js
|
||||
│ │ └── main.js
|
||||
│ │ └── fileops.js
|
||||
│ │ └── encryption.js
|
||||
│ ├── img/
|
||||
│ │ └── PacCrypt.png
|
||||
│ │ └── Github_logo.png
|
||||
│ │ └── sitemap.png
|
||||
│ └── audio/
|
||||
│ └── chomp.mp3
|
||||
├── start_dev.bat
|
||||
├── start_prod.bat
|
||||
├── start_dev.sh
|
||||
├── start_prod.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT © [TySP-Dev](https://github.com/TySP-Dev)
|
||||
|
||||
@@ -1,88 +1,697 @@
|
||||
from flask import Flask, render_template, request, jsonify
|
||||
import html
|
||||
# ===== Standard Library Imports =====
|
||||
import os
|
||||
import io
|
||||
import json
|
||||
import html
|
||||
import base64
|
||||
import hashlib
|
||||
import secrets
|
||||
import subprocess
|
||||
import platform
|
||||
from datetime import datetime, timedelta
|
||||
import sys
|
||||
import psutil
|
||||
|
||||
# ===== Third-Party Imports =====
|
||||
from flask import (
|
||||
Flask, render_template, request, jsonify, session,
|
||||
redirect, url_for, flash, send_file
|
||||
)
|
||||
from werkzeug.utils import secure_filename
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from cryptography.hazmat.primitives.hashes import SHA256
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
from waitress import serve
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
# ===== Application Configuration =====
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.getenv("FLASK_SECRET", os.urandom(24))
|
||||
|
||||
# Basic Encoder/Decoder
|
||||
# ===== Constants =====
|
||||
ADMIN_CRED_FILE = 'admin_creds.json'
|
||||
ADMIN_KEY_FILE = 'admin_key.key'
|
||||
ADMIN_LOG_FILE = 'admin_logs.enc'
|
||||
SETTINGS_FILE = 'settings.json'
|
||||
ALPHABET = list('abcdefghijklmnopqrstuvwxyz')
|
||||
|
||||
DEFAULT_SETTINGS = {
|
||||
"upload_folder": "uploads",
|
||||
"max_file_age_days": 14,
|
||||
"max_file_size_bytes": 25 * 1024 * 1024 * 1024 # 25GB
|
||||
}
|
||||
|
||||
# ===== Settings Management =====
|
||||
def load_settings():
|
||||
"""Load application settings from file or create with defaults."""
|
||||
if not os.path.exists(SETTINGS_FILE):
|
||||
with open(SETTINGS_FILE, 'w') as f:
|
||||
json.dump(DEFAULT_SETTINGS, f)
|
||||
with open(SETTINGS_FILE, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
settings = load_settings()
|
||||
UPLOAD_FOLDER = settings["upload_folder"]
|
||||
MAX_FILE_AGE_DAYS = settings["max_file_age_days"]
|
||||
MAX_FILE_SIZE_BYTES = settings["max_file_size_bytes"]
|
||||
|
||||
# Ensure upload folder exists and has proper permissions
|
||||
if not os.path.exists(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
|
||||
|
||||
# ===== Cryptographic Functions =====
|
||||
def derive_key(password: str, salt: bytes) -> bytes:
|
||||
"""Derive a cryptographic key from password using PBKDF2."""
|
||||
return PBKDF2HMAC(algorithm=SHA256(), length=32, salt=salt, iterations=200_000).derive(password.encode())
|
||||
|
||||
def hash_password(password: str, salt: bytes) -> str:
|
||||
"""Hash a password with salt for secure storage."""
|
||||
return base64.urlsafe_b64encode(derive_key(password, salt)).decode()
|
||||
|
||||
def simple_encode(text: str) -> str:
|
||||
return ''.join(
|
||||
ALPHABET[(ALPHABET.index(c) + 3) % 26] if c in ALPHABET else c
|
||||
for c in text.lower()
|
||||
)
|
||||
"""Basic Caesar cipher encryption."""
|
||||
return ''.join(ALPHABET[(ALPHABET.index(c) + 3) % 26] if c in ALPHABET else c for c in text.lower())
|
||||
|
||||
def simple_decode(text: str) -> str:
|
||||
return ''.join(
|
||||
ALPHABET[(ALPHABET.index(c) - 3) % 26] if c in ALPHABET else c
|
||||
for c in text.lower()
|
||||
)
|
||||
|
||||
# Advanced Encrypt/Decrypt using AES-GCM
|
||||
def derive_key(password: str, salt: bytes) -> bytes:
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=SHA256(),
|
||||
length=32,
|
||||
salt=salt,
|
||||
iterations=200_000,
|
||||
)
|
||||
return kdf.derive(password.encode())
|
||||
"""Basic Caesar cipher decryption."""
|
||||
return ''.join(ALPHABET[(ALPHABET.index(c) - 3) % 26] if c in ALPHABET else c for c in text.lower())
|
||||
|
||||
def advanced_encrypt(plaintext: str, password: str) -> str:
|
||||
"""Encrypt text using AES-GCM with password-derived key."""
|
||||
salt = os.urandom(16)
|
||||
key = derive_key(password, salt)
|
||||
|
||||
aesgcm = AESGCM(key)
|
||||
nonce = os.urandom(12)
|
||||
|
||||
ct = aesgcm.encrypt(nonce, plaintext.encode(), None)
|
||||
encrypted = salt + nonce + ct
|
||||
return base64.urlsafe_b64encode(encrypted).decode()
|
||||
ct = AESGCM(key).encrypt(nonce, plaintext.encode(), None)
|
||||
return base64.urlsafe_b64encode(salt + nonce + ct).decode()
|
||||
|
||||
def advanced_decrypt(token_b64: str, password: str) -> str:
|
||||
"""Decrypt text using AES-GCM with password-derived key."""
|
||||
try:
|
||||
data = base64.urlsafe_b64decode(token_b64.encode())
|
||||
salt, nonce, ct = data[:16], data[16:28], data[28:]
|
||||
key = derive_key(password, salt)
|
||||
aesgcm = AESGCM(key)
|
||||
pt = aesgcm.decrypt(nonce, ct, None)
|
||||
return pt.decode()
|
||||
return AESGCM(key).decrypt(nonce, ct, None).decode()
|
||||
except Exception:
|
||||
return "[Error] Invalid password or corrupted data!"
|
||||
|
||||
# Combined Route for Page & AJAX
|
||||
# ===== Admin Authentication =====
|
||||
def load_admin_key():
|
||||
"""Load or generate admin encryption key."""
|
||||
if not os.path.exists(ADMIN_KEY_FILE):
|
||||
with open(ADMIN_KEY_FILE, 'wb') as f:
|
||||
f.write(Fernet.generate_key())
|
||||
with open(ADMIN_KEY_FILE, 'rb') as f:
|
||||
return f.read()
|
||||
|
||||
def encrypt_creds(username, password):
|
||||
"""Encrypt and store admin credentials."""
|
||||
key = load_admin_key()
|
||||
cipher = Fernet(key)
|
||||
salt = os.urandom(16)
|
||||
hashed_pw = hash_password(password, salt)
|
||||
data = json.dumps({"u": username, "p": hashed_pw, "s": base64.b64encode(salt).decode()}).encode()
|
||||
with open(ADMIN_CRED_FILE, 'wb') as f:
|
||||
f.write(cipher.encrypt(data))
|
||||
|
||||
def check_creds(username, password):
|
||||
"""Verify admin credentials."""
|
||||
try:
|
||||
key = load_admin_key()
|
||||
cipher = Fernet(key)
|
||||
with open(ADMIN_CRED_FILE, 'rb') as f:
|
||||
decrypted = cipher.decrypt(f.read())
|
||||
creds = json.loads(decrypted)
|
||||
salt = base64.b64decode(creds["s"])
|
||||
return creds["u"] == username and creds["p"] == hash_password(password, salt)
|
||||
except Exception as e:
|
||||
print("[ERROR] check_creds failed:", e)
|
||||
return False
|
||||
|
||||
def log_admin_event(message: str):
|
||||
"""Log admin actions securely."""
|
||||
try:
|
||||
key = load_admin_key()
|
||||
cipher = Fernet(key)
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
encrypted = cipher.encrypt(f"[{timestamp}] {message}".encode())
|
||||
with open(ADMIN_LOG_FILE, 'ab') as f:
|
||||
f.write(encrypted + b"\n")
|
||||
except Exception as e:
|
||||
print("[ERROR] Failed to write admin log:", e)
|
||||
|
||||
# ===== File Management =====
|
||||
def cleanup_expired_files():
|
||||
"""Remove files older than MAX_FILE_AGE_DAYS."""
|
||||
try:
|
||||
now = datetime.now()
|
||||
for fname in os.listdir(UPLOAD_FOLDER):
|
||||
if fname.endswith(".enc") or fname.endswith(".json"):
|
||||
path = os.path.join(UPLOAD_FOLDER, fname)
|
||||
try:
|
||||
file_time = datetime.datetime.fromtimestamp(os.path.getmtime(path), )
|
||||
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"])
|
||||
def index():
|
||||
"""Main application route handling encryption/decryption and file uploads."""
|
||||
if request.method == 'POST':
|
||||
if 'file' in request.files:
|
||||
return handle_file_upload(request)
|
||||
else:
|
||||
return handle_text_operation(request)
|
||||
return render_template("index.html", result="", password="", encryption_type="advanced", settings=settings)
|
||||
|
||||
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().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", "")
|
||||
file_password = data.get("file-password", "")
|
||||
|
||||
final_password = file_password if file_password else password
|
||||
|
||||
if encryption_type == "basic":
|
||||
result = simple_encode(message) if operation == "encrypt" else simple_decode(message)
|
||||
else:
|
||||
result = advanced_encrypt(message, final_password) if operation == "encrypt" else advanced_decrypt(message, final_password)
|
||||
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"
|
||||
# ===== File Pickup Route =====
|
||||
@app.route("/pickup/<file_id>", methods=["GET", "POST"])
|
||||
def pickup_file(file_id):
|
||||
"""Handle file pickup and decryption."""
|
||||
meta_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.json")
|
||||
enc_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.enc")
|
||||
|
||||
if not os.path.exists(meta_path) or not os.path.exists(enc_path):
|
||||
flash("File not found or expired")
|
||||
return redirect(url_for('index'))
|
||||
|
||||
if request.method == 'POST':
|
||||
return handle_file_pickup(request, meta_path, enc_path, file_id)
|
||||
return render_template("pickup.html", file_id=file_id)
|
||||
|
||||
def handle_file_pickup(request, meta_path, enc_path, file_id):
|
||||
"""Process file pickup and decryption."""
|
||||
pickup_password = request.form.get('pickup_password')
|
||||
enc_password = request.form.get('enc_password')
|
||||
|
||||
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.")
|
||||
|
||||
response = send_file(
|
||||
io.BytesIO(decrypted),
|
||||
as_attachment=True,
|
||||
download_name=meta['original_name'],
|
||||
mimetype='application/octet-stream'
|
||||
)
|
||||
|
||||
# Add headers for better mobile compatibility
|
||||
response.headers['Content-Disposition'] = f'attachment; filename="{meta["original_name"]}"'
|
||||
response.headers['Content-Type'] = 'application/octet-stream'
|
||||
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
||||
response.headers['Pragma'] = 'no-cache'
|
||||
response.headers['Expires'] = '0'
|
||||
|
||||
return response
|
||||
|
||||
# ===== Admin Routes =====
|
||||
@app.route("/admin-logs")
|
||||
def admin_logs():
|
||||
"""View admin activity logs."""
|
||||
if not session.get("admin_logged_in"):
|
||||
return redirect(url_for("admin_login"))
|
||||
|
||||
logs = []
|
||||
try:
|
||||
key = load_admin_key()
|
||||
cipher = Fernet(key)
|
||||
if os.path.exists(ADMIN_LOG_FILE):
|
||||
with open(ADMIN_LOG_FILE, 'rb') as f:
|
||||
lines = f.readlines()
|
||||
for line in lines[-100:]:
|
||||
if line.strip():
|
||||
try:
|
||||
decrypted = cipher.decrypt(line.strip())
|
||||
logs.append(decrypted.decode())
|
||||
except Exception:
|
||||
logs.append("[Error] Corrupted log entry.")
|
||||
except Exception as e:
|
||||
logs.append(f"[Error loading logs] {str(e)}")
|
||||
|
||||
return jsonify(logs=logs)
|
||||
|
||||
@app.route("/admin-settings", methods=["GET", "POST"])
|
||||
def admin_settings():
|
||||
"""Manage application settings."""
|
||||
if not session.get("admin_logged_in"):
|
||||
return redirect(url_for("admin_login"))
|
||||
|
||||
current_settings = load_settings()
|
||||
|
||||
if request.method == 'POST':
|
||||
return handle_settings_update(request, current_settings)
|
||||
return render_template("admin_settings.html", settings=current_settings)
|
||||
|
||||
def handle_settings_update(request, current_settings):
|
||||
"""Process settings update request."""
|
||||
upload_folder = request.form.get('upload_folder', current_settings.get('upload_folder', 'uploads'))
|
||||
max_file_age_days = int(request.form.get('max_file_age_days', current_settings.get('max_file_age_days', 14)))
|
||||
max_file_size_gb = float(request.form.get('max_file_size_gb', current_settings.get('max_file_size_bytes', 25 * 1024 * 1024 * 1024) / (1024 * 1024 * 1024)))
|
||||
max_file_size_bytes = int(max_file_size_gb * 1024 * 1024 * 1024)
|
||||
|
||||
updated_settings = {
|
||||
"upload_folder": upload_folder,
|
||||
"max_file_age_days": max_file_age_days,
|
||||
"max_file_size_bytes": max_file_size_bytes
|
||||
}
|
||||
|
||||
with open(SETTINGS_FILE, 'w') as f:
|
||||
json.dump(updated_settings, f)
|
||||
|
||||
flash("Settings updated successfully!")
|
||||
|
||||
global settings, UPLOAD_FOLDER, MAX_FILE_AGE_DAYS, MAX_FILE_SIZE_BYTES
|
||||
settings = load_settings()
|
||||
UPLOAD_FOLDER = settings.get('upload_folder', 'uploads')
|
||||
MAX_FILE_AGE_DAYS = settings.get('max_file_age_days', 14)
|
||||
MAX_FILE_SIZE_BYTES = settings.get('max_file_size_bytes', 25 * 1024 * 1024 * 1024)
|
||||
|
||||
if not os.path.exists(UPLOAD_FOLDER):
|
||||
os.makedirs(UPLOAD_FOLDER)
|
||||
|
||||
return redirect(url_for("admin_settings"))
|
||||
|
||||
@app.route("/admin-setup", methods=["GET", "POST"])
|
||||
def admin_setup():
|
||||
"""Initial admin account setup."""
|
||||
if os.path.exists(ADMIN_CRED_FILE):
|
||||
return redirect(url_for("admin_login"))
|
||||
if request.method == "POST":
|
||||
u = request.form.get("username")
|
||||
p = request.form.get("password")
|
||||
if u and p:
|
||||
encrypt_creds(u, p)
|
||||
session["admin_logged_in"] = True
|
||||
return redirect(url_for("admin_page"))
|
||||
flash("Both fields required")
|
||||
return render_template("admin_setup.html")
|
||||
|
||||
@app.route("/admin-login", methods=["GET", "POST"])
|
||||
def admin_login():
|
||||
"""Admin login handler."""
|
||||
if request.method == "POST":
|
||||
u = request.form.get("username")
|
||||
p = request.form.get("password")
|
||||
if check_creds(u, p):
|
||||
session["admin_logged_in"] = True
|
||||
log_admin_event("Admin login successful.")
|
||||
return redirect(url_for("admin_page"))
|
||||
else:
|
||||
log_admin_event("Admin login failed.")
|
||||
flash("Incorrect credentials")
|
||||
return render_template("admin_login.html")
|
||||
|
||||
@app.route("/admin-logout")
|
||||
def admin_logout():
|
||||
"""Admin logout handler."""
|
||||
session.pop("admin_logged_in", None)
|
||||
return redirect(url_for("index"))
|
||||
|
||||
@app.route("/adminpage")
|
||||
def admin_page():
|
||||
"""Admin dashboard."""
|
||||
if not session.get("admin_logged_in"):
|
||||
if not os.path.exists(ADMIN_CRED_FILE):
|
||||
return redirect(url_for("admin_setup"))
|
||||
return redirect(url_for("admin_login"))
|
||||
|
||||
cleanup_expired_files()
|
||||
routes = [rule.rule for rule in app.url_map.iter_rules() if rule.endpoint != 'static']
|
||||
|
||||
now = datetime.now()
|
||||
try:
|
||||
boot_time = datetime.fromtimestamp(psutil.boot_time())
|
||||
|
||||
uptime = now - boot_time
|
||||
days = uptime.days
|
||||
hours, remainder = divmod(uptime.seconds, 3600)
|
||||
minutes = remainder // 60
|
||||
uptime_str = f"{days} days, {hours} hours, {minutes} minutes"
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Uptime calculation failed: {e}")
|
||||
uptime_str = "Unavailable"
|
||||
|
||||
server_info = {
|
||||
"uptime": uptime_str,
|
||||
"server_time": now.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"python_version": platform.python_version(),
|
||||
"debug_mode": app.debug
|
||||
}
|
||||
|
||||
return render_template("admin.html", routes=routes, server_info=server_info)
|
||||
|
||||
|
||||
|
||||
@app.route("/restart-server", methods=["POST"])
|
||||
def restart_server():
|
||||
"""Restart the server."""
|
||||
if not session.get("admin_logged_in"):
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
|
||||
try:
|
||||
if platform.system() == "Windows":
|
||||
current_pid = os.getpid()
|
||||
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)
|
||||
subprocess.Popen(["restart.bat"], shell=True)
|
||||
return jsonify({"message": "Server restart initiated"}), 200
|
||||
else:
|
||||
current_pid = os.getpid()
|
||||
python_path = sys.executable
|
||||
script_path = os.path.abspath(__file__)
|
||||
|
||||
# Create a safer and cleaner restart script
|
||||
restart_script = """#!/bin/bash
|
||||
sleep 2
|
||||
PID=$1
|
||||
kill "$PID"
|
||||
while kill -0 "$PID" 2>/dev/null; do sleep 0.5; done
|
||||
export PRODUCTION=true
|
||||
exec "$2" "$3"
|
||||
"""
|
||||
|
||||
with open("restart.sh", "w") as f:
|
||||
f.write(restart_script)
|
||||
os.chmod("restart.sh", 0o755)
|
||||
|
||||
subprocess.Popen(["./restart.sh", str(current_pid), python_path, script_path])
|
||||
return jsonify({"message": "Server restart initiated"}), 200
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to restart server: {str(e)}")
|
||||
return jsonify({"error": f"Failed to restart server: {str(e)}"}), 500
|
||||
|
||||
@app.route("/admin-reset", methods=["POST"])
|
||||
def admin_reset():
|
||||
"""Reset admin credentials."""
|
||||
if not session.get("admin_logged_in"):
|
||||
return redirect(url_for("admin_login"))
|
||||
try:
|
||||
if os.path.exists(ADMIN_CRED_FILE):
|
||||
os.remove(ADMIN_CRED_FILE)
|
||||
if os.path.exists(ADMIN_KEY_FILE):
|
||||
os.remove(ADMIN_KEY_FILE)
|
||||
session.pop("admin_logged_in", None)
|
||||
flash("Admin credentials reset. Please create new credentials.")
|
||||
except Exception as e:
|
||||
flash("Failed to reset admin credentials.")
|
||||
print("[ERROR] admin_reset failed:", e)
|
||||
return redirect(url_for("admin_setup"))
|
||||
|
||||
@app.route("/admin-change-password", methods=["POST"])
|
||||
def admin_change_password():
|
||||
"""Change admin password."""
|
||||
if not session.get("admin_logged_in"):
|
||||
return redirect(url_for("admin_login"))
|
||||
|
||||
current = request.form.get("current_password")
|
||||
new = request.form.get("new_password")
|
||||
|
||||
try:
|
||||
key = load_admin_key()
|
||||
cipher = Fernet(key)
|
||||
with open(ADMIN_CRED_FILE, 'rb') as file:
|
||||
decrypted = cipher.decrypt(file.read())
|
||||
creds = json.loads(decrypted)
|
||||
|
||||
salt = base64.b64decode(creds["s"])
|
||||
if hash_password(current, salt) != creds["p"]:
|
||||
flash("Current password is incorrect")
|
||||
return redirect(url_for("admin_page"))
|
||||
|
||||
creds["p"] = hash_password(new, salt)
|
||||
encrypted = cipher.encrypt(json.dumps(creds).encode())
|
||||
with open(ADMIN_CRED_FILE, 'wb') as file:
|
||||
file.write(encrypted)
|
||||
|
||||
log_admin_event("Admin password changed.")
|
||||
flash("Password updated successfully", "password-feedback")
|
||||
return redirect(url_for("admin_page"))
|
||||
|
||||
except Exception as e:
|
||||
flash("Failed to update password")
|
||||
print("[ERROR] Password change failed:", e)
|
||||
return redirect(url_for("admin_page"))
|
||||
|
||||
@app.route("/admin-clear-uploads", methods=["POST"])
|
||||
def admin_clear_uploads():
|
||||
"""Clear all uploaded files."""
|
||||
if not session.get("admin_logged_in"):
|
||||
return redirect(url_for("admin_login"))
|
||||
|
||||
deleted = 0
|
||||
for filename in os.listdir(UPLOAD_FOLDER):
|
||||
if filename.endswith(".enc") or filename.endswith(".json"):
|
||||
try:
|
||||
os.remove(os.path.join(UPLOAD_FOLDER, filename))
|
||||
deleted += 1
|
||||
except Exception as e:
|
||||
print("[ERROR] Failed to delete:", filename, e)
|
||||
|
||||
flash(f"Cleared {deleted} uploaded file(s).", "clear-feedback")
|
||||
return redirect(url_for("admin_page"))
|
||||
|
||||
@app.route("/admin-update-server", methods=["POST"])
|
||||
def admin_update_server():
|
||||
"""Update server from GitHub repository."""
|
||||
if not session.get("admin_logged_in"):
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
|
||||
try:
|
||||
# Get the absolute path of the current directory
|
||||
current_dir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
# Try to find git executable
|
||||
git_paths = [
|
||||
"/usr/bin/git", # Standard Debian path
|
||||
"/usr/local/bin/git",
|
||||
"/bin/git",
|
||||
"git" # Fallback to PATH
|
||||
]
|
||||
|
||||
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():
|
||||
"""Generate sitemap.xml."""
|
||||
sitemap_xml = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url><loc>https://paccrypt.unnaturalll.dev/</loc></url>
|
||||
<url><loc>https://paccrypt.unnaturalll.dev/pickup</loc></url>
|
||||
<url><loc>https://paccrypt.unnaturalll.dev/adminpage</loc></url>
|
||||
<url><loc>https://paccrypt.unnaturalll.dev/sitemap</loc></url>
|
||||
</urlset>'''
|
||||
return sitemap_xml, 200, {'Content-Type': 'application/xml'}
|
||||
|
||||
@app.route("/robots.txt")
|
||||
def robots_txt():
|
||||
"""Generate robots.txt."""
|
||||
lines = [
|
||||
"User-agent: *",
|
||||
"Disallow: /adminpage",
|
||||
"Disallow: /admin-login",
|
||||
"Disallow: /admin-setup",
|
||||
"Disallow: /admin-reset",
|
||||
"Disallow: /admin-settings",
|
||||
"Disallow: /restart-server",
|
||||
"Disallow: /pickup",
|
||||
"Disallow: /admin-change-password",
|
||||
"Allow: /",
|
||||
f"Sitemap: {url_for('sitemap', _external=True)}"
|
||||
]
|
||||
return "\n".join(lines), 200, {"Content-Type": "text/plain"}
|
||||
|
||||
# ===== Error Handlers =====
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
return render_template('404.html'), 404
|
||||
|
||||
@app.errorhandler(500)
|
||||
def server_error(e):
|
||||
return render_template('500.html'), 500
|
||||
|
||||
@app.errorhandler(403)
|
||||
def forbidden(e):
|
||||
return render_template('403.html'), 403
|
||||
|
||||
@app.errorhandler(405)
|
||||
def method_not_allowed(e):
|
||||
return render_template('403.html'), 403
|
||||
|
||||
@app.errorhandler(FileNotFoundError)
|
||||
def handle_file_not_found(e):
|
||||
if os.getenv("PRODUCTION", "false").lower() == "true":
|
||||
return render_template('500.html'), 500
|
||||
else:
|
||||
raise e
|
||||
|
||||
# ===== Application Entry Point =====
|
||||
if __name__ == "__main__":
|
||||
# Use Waitress to serve the app in production
|
||||
PRODUCTION = os.getenv("PRODUCTION", "false").lower() == "true"
|
||||
if PRODUCTION:
|
||||
from waitress import serve
|
||||
print("[INFO] Running in PRODUCTION mode with Waitress.")
|
||||
serve(app, host="0.0.0.0", port=5000)
|
||||
else:
|
||||
print("[INFO] Running in DEVELOPMENT mode with Flask server.")
|
||||
app.run(debug=True, host="0.0.0.0", port=5000)
|
||||
@@ -1,5 +1,10 @@
|
||||
### **requirements.txt**
|
||||
|
||||
Flask==2.1.2
|
||||
cryptography==3.4.8
|
||||
nginx==1.21.0 # Only needed for Nginx integration, not installed via pip
|
||||
flask==3.0.3
|
||||
cryptography==42.0.5
|
||||
waitress==2.1.2
|
||||
werkzeug==3.0.1
|
||||
psutil>=5.9.0,<6.0.0
|
||||
|
||||
# nginx - Only needed for Nginx integration, not installed via pip
|
||||
# Run pip install -r requirements.txt
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
@echo off
|
||||
timeout /t 2 /nobreak
|
||||
taskkill /F /PID 15428
|
||||
set PRODUCTION=true
|
||||
start "" "python" "app.py"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
sleep 2
|
||||
|
||||
# Save current process PID
|
||||
PID=$1
|
||||
|
||||
# Gracefully stop the current server
|
||||
kill "$PID"
|
||||
|
||||
# Wait until it exits
|
||||
while kill -0 "$PID" 2>/dev/null; do
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
# Restart with the same interpreter and script
|
||||
export PRODUCTION=true
|
||||
exec "$2" "$3"
|
||||
@@ -0,0 +1 @@
|
||||
{"upload_folder": "uploads", "max_file_age_days": 14, "max_file_size_bytes": 26843545600}
|
||||
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
echo Starting PacCrypt in DEVELOPMENT mode...
|
||||
set PRODUCTION=false
|
||||
python app.py
|
||||
pause
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
echo "Starting PacCrypt in DEVELOPMENT mode..."
|
||||
export PRODUCTION=false
|
||||
python3 app.py
|
||||
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
echo Starting PacCrypt in PRODUCTION mode...
|
||||
set PRODUCTION=true
|
||||
python app.py
|
||||
pause
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
echo "Starting PacCrypt in PRODUCTION mode..."
|
||||
export PRODUCTION=true
|
||||
python3 app.py
|
||||
@@ -1,44 +1,157 @@
|
||||
/* ===== Global Reset ===== */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
gap: 6px !important;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* ===== Body ===== */
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background-color: #121212;
|
||||
color: #f0f0f0;
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
background-color: #0e0e0e;
|
||||
color: #28E060;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
justify-content: center; /* Vertically center content */
|
||||
align-items: center; /* Horizontally center content */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
#sitemap-section,
|
||||
#password-change-section,
|
||||
#server-update-section,
|
||||
#server-status-section,
|
||||
#server-logs-section,
|
||||
#system-settings-section {
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#sitemap-section li,
|
||||
#server-status-section li {
|
||||
font-size: 0.9em;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
#logContainer {
|
||||
font-size: 0.9em;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 11px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.button-group,
|
||||
.admin-button-grid {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button-group button,
|
||||
.admin-button-grid button {
|
||||
min-width: 75%;
|
||||
max-width: 75%;
|
||||
}
|
||||
|
||||
header {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
padding-inline: 15px;
|
||||
padding-block: 20px;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.status-list {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Header ===== */
|
||||
header {
|
||||
text-align: center;
|
||||
padding: 30px 20px;
|
||||
background-color: #1c1c1c;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.5);
|
||||
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;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2.8em;
|
||||
color: #00ff99;
|
||||
margin-bottom: 8px;
|
||||
.logo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 1.1em;
|
||||
color: #00ff99;
|
||||
.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 {
|
||||
flex: 1;
|
||||
@@ -47,44 +160,68 @@ main {
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
padding: 20px;
|
||||
gap: 30px;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* ===== Section Card Styling ===== */
|
||||
/* ===== Card Styling ===== */
|
||||
.card {
|
||||
background-color: #1e1e1e;
|
||||
padding: 20px 25px;
|
||||
padding: 25px;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 0 15px rgba(0, 255, 153, 0.4);
|
||||
box-shadow: 0 0 15px #28E060;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ===== Uniform Form Inputs ===== */
|
||||
/* ===== Form Group Styling ===== */
|
||||
.form-group {
|
||||
display: flex;
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
max-width: 725px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.status-list {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
|
||||
/* ===== Inputs, Textareas, Selects ===== */
|
||||
|
||||
button,
|
||||
select,
|
||||
input,
|
||||
textarea {
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 12px !important;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
input[type="file"] {
|
||||
width: 80%;
|
||||
max-width: 500px;
|
||||
padding: 12px 20px;
|
||||
border: 2px solid #00ff99;
|
||||
padding-inline: 20px;
|
||||
padding-block: 12px;
|
||||
border: 1px solid #28E060;
|
||||
border-radius: 8px;
|
||||
background-color: #2c2f33;
|
||||
color: #00ff99;
|
||||
font-size: 1em;
|
||||
color: #28E060;
|
||||
text-align: left;
|
||||
transition: 0.3s;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
select {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
textarea {
|
||||
@@ -92,148 +229,278 @@ textarea {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
input[type="password"] {
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
/* ===== File Input Customization ===== */
|
||||
input[type="file"] {
|
||||
border: 2px dashed #00ff99;
|
||||
border: 2px dashed #28E060;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
color: #28E060;
|
||||
background-color: #2c2f33;
|
||||
}
|
||||
|
||||
input[type="file"]::file-selector-button {
|
||||
background-color: #00ff99;
|
||||
color: #121212;
|
||||
border: none;
|
||||
padding: 8px 15px;
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 12px;
|
||||
background-color: #2c2f33;
|
||||
color: #28E060;
|
||||
border: 2px solid #28E060;
|
||||
padding-inline: 10px;
|
||||
padding-block: 8px;
|
||||
margin-right: 10px;
|
||||
border-radius: 6px;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
transition: 0.3s ease;
|
||||
}
|
||||
|
||||
input[type="file"]::file-selector-button:hover {
|
||||
background-color: #00cc77;
|
||||
background-color: #28E060;
|
||||
color: #000;
|
||||
box-shadow: 0 0 10px #28E060;
|
||||
}
|
||||
|
||||
/* ===== Focus Effects ===== */
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 8px rgba(0, 255, 153, 0.8);
|
||||
box-shadow: 0 0 10px #28E060;
|
||||
}
|
||||
|
||||
/* ===== Match input and output textarea sizes ===== */
|
||||
/* ===== Textareas Specific Widths ===== */
|
||||
#input-text,
|
||||
#output-text {
|
||||
width: 80%;
|
||||
max-width: 500px;
|
||||
height: 140px;
|
||||
box-sizing: border-box;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
/* ===== Buttons ===== */
|
||||
/* ===== Button Group Styling ===== */
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
border: 2px solid #00ff99;
|
||||
padding-inline: 20px;
|
||||
padding-block: 10px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background-color: #2c2f33;
|
||||
color: #00ff99;
|
||||
color: #28E060;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
width: 100%; /* Makes buttons stretch to fill container */
|
||||
max-width: 200px; /* Restricts button width */
|
||||
width: auto;
|
||||
min-width: 225px;
|
||||
max-width: 300px;
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #00ff99;
|
||||
background-color: #28E060;
|
||||
color: #121212;
|
||||
box-shadow: 0 0 10px #28E060;
|
||||
}
|
||||
|
||||
/* ===== Toggle Buttons ===== */
|
||||
.radio-group {
|
||||
.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));
|
||||
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-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
/* Make sure the switch aligns well */
|
||||
.switch {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center; /* <-- Ensures vertical centering */
|
||||
justify-content: center;
|
||||
padding: 8px 18px;
|
||||
border: 2px solid #00ff99;
|
||||
border-radius: 8px;
|
||||
width: 70px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
/* Hide the checkbox */
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* The slider */
|
||||
.slider {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #2c2f33;
|
||||
color: #00ff99;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
border: 2px solid #28E060;
|
||||
border-radius: 34px;
|
||||
transition: .4s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.radio-button:hover {
|
||||
background-color: #00ff99;
|
||||
color: #121212;
|
||||
/* The circle knob */
|
||||
.slider::before {
|
||||
content: "";
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
background-color: #28E060;
|
||||
border-radius: 50%;
|
||||
transition: .4s;
|
||||
transform: translateX(2px);
|
||||
position: absolute;
|
||||
left: auto;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.radio-button input {
|
||||
display: none;
|
||||
input:checked + .slider::before {
|
||||
transform: translateX(36px);
|
||||
}
|
||||
|
||||
.radio-button input:checked + span {
|
||||
background-color: #00ff99;
|
||||
color: #121212;
|
||||
padding: 8px 18px;
|
||||
border-radius: 8px;
|
||||
display: inline-block;
|
||||
/* Toggle Labels */
|
||||
.labels {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.9em;
|
||||
color: #28E060;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* ===== Remove File Button ===== */
|
||||
#remove-file-btn {
|
||||
display: none; /* only shows when a file is selected */
|
||||
margin-top: 8px;
|
||||
padding: 8px 16px;
|
||||
border: 2px solid #ff5555;
|
||||
background-color: #2c2f33;
|
||||
color: #ff5555;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
.labels::before,
|
||||
.labels::after {
|
||||
content: attr(data-on);
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#remove-file-btn:hover {
|
||||
background-color: #ff5555;
|
||||
color: #2c2f33;
|
||||
.labels::after {
|
||||
content: attr(data-off);
|
||||
}
|
||||
|
||||
/* ===== Toast Notifications ===== */
|
||||
.toast {
|
||||
visibility: hidden;
|
||||
min-width: 250px;
|
||||
width: 80%;
|
||||
max-width: 500px;
|
||||
min-height: 50px;
|
||||
background-color: #333;
|
||||
color: #00ff99;
|
||||
color: #28E060;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
margin-top: 8px;
|
||||
font-size: 0.9em;
|
||||
animation: fadein 0.5s, fadeout 0.5s 2.5s;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
margin: 10px auto 0 auto;
|
||||
font-size: 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
visibility: visible;
|
||||
animation: fadein 0.5s, fadeout 0.5s 2.5s;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
@@ -256,33 +523,21 @@ button {
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Pacman Canvas ===== */
|
||||
.pacman-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
#pacmanCanvas {
|
||||
background-color: black;
|
||||
border: 2px solid #00ff99;
|
||||
border-radius: 10px;
|
||||
width: 800px;
|
||||
height: 600px;
|
||||
}
|
||||
|
||||
/* ===== Footer ===== */
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 18px;
|
||||
padding: 25px;
|
||||
background-color: #1c1c1c;
|
||||
color: #00ff99;
|
||||
margin-top: auto;
|
||||
color: #28E060;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 0 15px #28E060;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #00ff99;
|
||||
color: #28E060;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -290,22 +545,289 @@ footer {
|
||||
color: #ff0066;
|
||||
}
|
||||
|
||||
/* ===== Password Input Field ===== */
|
||||
#password-input {
|
||||
display: flex; /* Password input is visible by default */
|
||||
margin-top: 15px;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
/* ===== Responsive Design ===== */
|
||||
@media (max-width: 600px) {
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
#input-text,
|
||||
#output-text {
|
||||
width: 100% !important;
|
||||
max-width: 90% !important;
|
||||
}
|
||||
}
|
||||
|
||||
#password-input input {
|
||||
padding: 12px;
|
||||
font-size: 1em;
|
||||
border: 2px solid #00ff99;
|
||||
border-radius: 8px;
|
||||
/* ===== Copy Feedback Message ===== */
|
||||
.copy-feedback, #shared-link-feedback {
|
||||
background-color: #2c2f33;
|
||||
color: #00ff99;
|
||||
width: 100%; /* Ensure the password field takes full width */
|
||||
padding-inline: 12px;
|
||||
padding-block: 6px;
|
||||
margin-top: 6px;
|
||||
border-radius: 6px;
|
||||
color: #28E060;
|
||||
font-size: 0.9em;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.copy-feedback.show, #shared-link-feedback.show {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.share-link-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
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 {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
/* ===== Section Card Styling ===== */
|
||||
section.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* ===== Pacman Game Styling ===== */
|
||||
#pacmanCanvas {
|
||||
background-color: black;
|
||||
display: block;
|
||||
border: 2px solid #28E060;
|
||||
border-radius: 12px;
|
||||
max-width: 700px;
|
||||
width: 100%;
|
||||
aspect-ratio: 4/3;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
#pacman-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
max-width: 725px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pacman-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ===== Utility Classes ===== */
|
||||
.hidden {
|
||||
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: 6px;
|
||||
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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 1006 KiB After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Encryption module.
|
||||
* Handles cryptographic operations using Web Crypto API.
|
||||
* Implements AES-GCM encryption with PBKDF2 key derivation.
|
||||
*/
|
||||
|
||||
// ===== Constants =====
|
||||
const SALT_LENGTH = 16;
|
||||
const IV_LENGTH = 12;
|
||||
const PBKDF2_ITERATIONS = 200_000;
|
||||
const KEY_LENGTH = 256;
|
||||
|
||||
// ===== Key Derivation =====
|
||||
/**
|
||||
* Derives an AES-GCM key from a password using PBKDF2.
|
||||
* @param {string} password - User-supplied password.
|
||||
* @param {Uint8Array} salt - Randomly generated salt.
|
||||
* @returns {Promise<CryptoKey>} - Derived cryptographic key.
|
||||
*/
|
||||
export async function deriveKey(password, salt) {
|
||||
const encoder = new TextEncoder();
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(password),
|
||||
{ name: 'PBKDF2' },
|
||||
false,
|
||||
['deriveKey']
|
||||
);
|
||||
|
||||
return crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt,
|
||||
iterations: PBKDF2_ITERATIONS,
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
keyMaterial,
|
||||
{ name: 'AES-GCM', length: KEY_LENGTH },
|
||||
false,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
}
|
||||
|
||||
// ===== Encryption =====
|
||||
/**
|
||||
* Encrypts a message using AES-GCM with a derived key.
|
||||
* @param {string} message - Plaintext message to encrypt.
|
||||
* @param {string} password - User password for key derivation.
|
||||
* @returns {Promise<string>} - Base64-encoded encrypted string.
|
||||
*/
|
||||
export async function encryptAdvanced(message, password) {
|
||||
const encoder = new TextEncoder();
|
||||
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
||||
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
||||
const key = await deriveKey(password, salt);
|
||||
const encoded = encoder.encode(message);
|
||||
|
||||
const ciphertext = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
key,
|
||||
encoded
|
||||
);
|
||||
|
||||
const output = new Uint8Array(salt.length + iv.length + ciphertext.byteLength);
|
||||
output.set(salt);
|
||||
output.set(iv, salt.length);
|
||||
output.set(new Uint8Array(ciphertext), salt.length + iv.length);
|
||||
|
||||
return btoa(String.fromCharCode(...output));
|
||||
}
|
||||
|
||||
// ===== Decryption =====
|
||||
/**
|
||||
* Decrypts an AES-GCM encrypted string.
|
||||
* @param {string} encryptedData - Base64-encoded ciphertext.
|
||||
* @param {string} password - Password used to derive the decryption key.
|
||||
* @returns {Promise<string>} - Decrypted plaintext.
|
||||
*/
|
||||
export async function decryptAdvanced(encryptedData, password) {
|
||||
const encrypted = new Uint8Array(
|
||||
atob(encryptedData).split('').map(c => c.charCodeAt(0))
|
||||
);
|
||||
|
||||
const salt = encrypted.slice(0, SALT_LENGTH);
|
||||
const iv = encrypted.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
|
||||
const ciphertext = encrypted.slice(SALT_LENGTH + IV_LENGTH);
|
||||
const key = await deriveKey(password, salt);
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
key,
|
||||
ciphertext
|
||||
);
|
||||
|
||||
return new TextDecoder().decode(decrypted);
|
||||
}
|
||||
|
||||
// ===== Module Initialization =====
|
||||
/**
|
||||
* Initializes the encryption module and logs its status.
|
||||
*/
|
||||
export function setupEncryption() {
|
||||
console.log('[Encryption] Module loaded');
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* File operations module.
|
||||
* Handles file encryption and decryption operations.
|
||||
*/
|
||||
|
||||
// ===== Constants =====
|
||||
const CHUNK_SIZE = 1024 * 1024; // 1MB chunks
|
||||
|
||||
// ===== Public Interface =====
|
||||
export async function encryptFile(fileInput, password) {
|
||||
const file = fileInput.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const encryptedChunks = await processFile(file, password, true);
|
||||
downloadEncryptedFile(encryptedChunks, file.name);
|
||||
} catch (error) {
|
||||
alert("Error encrypting file: " + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function decryptFile(fileInput, password) {
|
||||
const file = fileInput.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const decryptedChunks = await processFile(file, password, false);
|
||||
downloadDecryptedFile(decryptedChunks, file.name);
|
||||
} catch (error) {
|
||||
alert("Error decrypting file: " + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== File Processing =====
|
||||
async function processFile(file, password, isEncrypt) {
|
||||
const chunks = [];
|
||||
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
||||
let processedChunks = 0;
|
||||
|
||||
for (let start = 0; start < file.size; start += CHUNK_SIZE) {
|
||||
const chunk = file.slice(start, start + CHUNK_SIZE);
|
||||
const arrayBuffer = await chunk.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
|
||||
const processedChunk = await processChunk(uint8Array, password, isEncrypt);
|
||||
chunks.push(processedChunk);
|
||||
|
||||
processedChunks++;
|
||||
updateProgress(processedChunks, totalChunks);
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
async function processChunk(data, password, isEncrypt) {
|
||||
const payload = {
|
||||
"encryption-type": "advanced",
|
||||
operation: isEncrypt ? "encrypt" : "decrypt",
|
||||
message: Array.from(data).join(','),
|
||||
password: password
|
||||
};
|
||||
|
||||
const response = await fetch("/", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return new Uint8Array(result.result.split(',').map(Number));
|
||||
}
|
||||
|
||||
// ===== File Download =====
|
||||
function downloadEncryptedFile(chunks, originalName) {
|
||||
const blob = new Blob(chunks, { type: 'application/octet-stream' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = originalName + '.encrypted';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function downloadDecryptedFile(chunks, originalName) {
|
||||
const blob = new Blob(chunks, { type: 'application/octet-stream' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = originalName.replace('.encrypted', '');
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// ===== Progress Tracking =====
|
||||
function updateProgress(processed, total) {
|
||||
const progressBar = document.getElementById("file-progress");
|
||||
const progressText = document.getElementById("file-progress-text");
|
||||
|
||||
if (progressBar && progressText) {
|
||||
const percent = Math.round((processed / total) * 100);
|
||||
progressBar.style.width = percent + "%";
|
||||
progressText.textContent = `Processing: ${percent}%`;
|
||||
|
||||
if (processed === total) {
|
||||
setTimeout(() => {
|
||||
progressBar.style.width = "0%";
|
||||
progressText.textContent = "";
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Main application entry point.
|
||||
* Initializes UI and game components when the DOM is loaded.
|
||||
*/
|
||||
|
||||
import { setupUI } from './ui.js';
|
||||
import { setupGame } from './pacman.js';
|
||||
|
||||
// Initialize application when DOM is fully loaded
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
setupUI();
|
||||
setupGame();
|
||||
});
|
||||
@@ -0,0 +1,376 @@
|
||||
/**
|
||||
* Pacman game module.
|
||||
* Handles game logic, rendering, and user interaction.
|
||||
*/
|
||||
|
||||
// ===== Game Constants =====
|
||||
const PACMAN_SPEED = 40;
|
||||
const ENEMY_SPEED = 20;
|
||||
const CELL_SIZE = 40;
|
||||
const DOT_SIZE = 5;
|
||||
|
||||
// ===== Game State =====
|
||||
let canvas, ctx, pacman, enemy, walls, dots, score;
|
||||
let cols, rows, randSeed, gameInterval;
|
||||
|
||||
// ===== Public Interface =====
|
||||
export function setupGame() {
|
||||
console.log('[PacMan] Game module loaded.');
|
||||
window.startPacman = startPacman;
|
||||
window.exitGame = exitGame;
|
||||
}
|
||||
|
||||
export function startPacman() {
|
||||
// Scroll to the Pacman section
|
||||
const pacmanSection = document.getElementById("pacman-section");
|
||||
if (pacmanSection) {
|
||||
pacmanSection.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// Initialize game state
|
||||
initializeGame();
|
||||
setupGameLoop();
|
||||
}
|
||||
|
||||
export function stopPacman() {
|
||||
clearInterval(gameInterval);
|
||||
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() {
|
||||
stopPacman();
|
||||
startPacman();
|
||||
}
|
||||
|
||||
export function exitGame() {
|
||||
stopPacman();
|
||||
document.getElementById("input-text").value = "";
|
||||
document.getElementById("pacman-section").style.display = "none";
|
||||
document.getElementById("encoding-section").style.display = "block";
|
||||
}
|
||||
|
||||
// ===== Game 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() {
|
||||
const options = [];
|
||||
for (let c = 1; c < cols - 1; c++) {
|
||||
for (let r = 1; r < rows - 1; r++) {
|
||||
if (!walls.some(w => w.c === c && w.r === r)) {
|
||||
const neighbors = [
|
||||
{ c: c + 1, r }, { c: c - 1, r },
|
||||
{ c, r: r + 1 }, { c, r: r - 1 }
|
||||
];
|
||||
if (neighbors.some(n => !walls.some(w => w.c === n.c && w.r === n.r))) {
|
||||
options.push({ c, r });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const s = options[Math.floor(rand() * options.length)];
|
||||
return {
|
||||
x: s.c * CELL_SIZE + CELL_SIZE / 2,
|
||||
y: s.r * CELL_SIZE + CELL_SIZE / 2,
|
||||
size: CELL_SIZE / 2 - 5,
|
||||
dx: 0,
|
||||
dy: 0
|
||||
};
|
||||
}
|
||||
|
||||
function rand() {
|
||||
const x = Math.sin(randSeed++) * 10000;
|
||||
return x - Math.floor(x);
|
||||
}
|
||||
|
||||
function generateWalls() {
|
||||
// First pass: generate initial walls
|
||||
for (let c = 0; c < cols; c++) {
|
||||
for (let r = 0; r < rows; r++) {
|
||||
if (c === 0 || r === 0 || c === cols - 1 || r === rows - 1 || rand() < 0.2) {
|
||||
walls.push({ c, r });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
dots = [];
|
||||
for (let c = 1; c < cols - 1; c++) {
|
||||
for (let r = 1; r < rows - 1; r++) {
|
||||
if (walls.some(w => w.c === c && w.r === r)) continue;
|
||||
|
||||
const isEnclosed =
|
||||
walls.some(w => w.c === c + 1 && w.r === r) &&
|
||||
walls.some(w => w.c === c - 1 && w.r === r) &&
|
||||
walls.some(w => w.c === c && w.r === r + 1) &&
|
||||
walls.some(w => w.c === c && w.r === r - 1);
|
||||
|
||||
if (!isEnclosed) dots.push({ c, r });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Game Loop & Rendering =====
|
||||
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 * CELL_SIZE, w.r * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||
});
|
||||
}
|
||||
|
||||
function drawChar(ch, color) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(ch.x, ch.y, ch.size, 0, Math.PI * 2);
|
||||
ctx.fillStyle = color;
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function drawScore() {
|
||||
ctx.fillStyle = "white";
|
||||
ctx.font = "20px Poppins";
|
||||
ctx.textAlign = "left";
|
||||
// Add padding to prevent clipping
|
||||
const padding = 10;
|
||||
ctx.fillText("Score: " + score, padding, 25);
|
||||
}
|
||||
|
||||
function checkGameOver() {
|
||||
if (
|
||||
Math.abs(pacman.x - enemy.x) < pacman.size &&
|
||||
Math.abs(pacman.y - enemy.y) < pacman.size
|
||||
) {
|
||||
ctx.fillStyle = "#00ff99";
|
||||
ctx.font = "40px Poppins";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText("Game Over!", canvas.width / 2, canvas.height / 2);
|
||||
clearInterval(gameInterval);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Movement Logic =====
|
||||
function movePacman(e) {
|
||||
const k = e.key;
|
||||
if (!["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(k)) return;
|
||||
e.preventDefault();
|
||||
|
||||
if (k === "ArrowUp") { pacman.dx = 0; pacman.dy = -PACMAN_SPEED; }
|
||||
if (k === "ArrowDown") { pacman.dx = 0; pacman.dy = PACMAN_SPEED; }
|
||||
if (k === "ArrowLeft") { pacman.dx = -PACMAN_SPEED; pacman.dy = 0; }
|
||||
if (k === "ArrowRight") { pacman.dx = PACMAN_SPEED; pacman.dy = 0; }
|
||||
}
|
||||
|
||||
function moveChar(ch) {
|
||||
const nx = ch.x + ch.dx;
|
||||
const ny = ch.y + ch.dy;
|
||||
if (!willCollide(nx, ny, ch.size)) {
|
||||
ch.x = nx;
|
||||
ch.y = ny;
|
||||
}
|
||||
}
|
||||
|
||||
function moveEnemy() {
|
||||
const options = [];
|
||||
const moves = [[ENEMY_SPEED, 0], [-ENEMY_SPEED, 0], [0, ENEMY_SPEED], [0, -ENEMY_SPEED]];
|
||||
|
||||
moves.forEach(([dx, dy]) => {
|
||||
const nx = enemy.x + dx;
|
||||
const ny = enemy.y + dy;
|
||||
if (!willCollide(nx, ny, enemy.size)) options.push({ dx, dy });
|
||||
});
|
||||
|
||||
if (!options.length) return;
|
||||
|
||||
let best = options[0];
|
||||
let bestDist = dist(enemy.x + best.dx, enemy.y + best.dy, pacman.x, pacman.y);
|
||||
|
||||
for (const opt of options) {
|
||||
const d = dist(enemy.x + opt.dx, enemy.y + opt.dy, pacman.x, pacman.y);
|
||||
if (d < bestDist) {
|
||||
best = opt;
|
||||
bestDist = d;
|
||||
}
|
||||
}
|
||||
|
||||
enemy.x += best.dx;
|
||||
enemy.y += best.dy;
|
||||
}
|
||||
|
||||
function dist(x1, y1, x2, y2) {
|
||||
return Math.abs(x1 - x2) + Math.abs(y1 - y2);
|
||||
}
|
||||
|
||||
function willCollide(x, y, size) {
|
||||
const left = x - size, right = x + size;
|
||||
const top = y - size, bottom = y + size;
|
||||
|
||||
return walls.some(w => {
|
||||
const wx1 = w.c * CELL_SIZE, wy1 = w.r * CELL_SIZE;
|
||||
const wx2 = wx1 + CELL_SIZE, wy2 = wy1 + CELL_SIZE;
|
||||
return right > wx1 && left < wx2 && bottom > wy1 && top < wy2;
|
||||
});
|
||||
}
|
||||
|
||||
function eatDots() {
|
||||
const chompSound = document.getElementById("chomp-sound");
|
||||
|
||||
dots = dots.filter(d => {
|
||||
const dx = d.c * CELL_SIZE + CELL_SIZE / 2;
|
||||
const dy = d.r * CELL_SIZE + CELL_SIZE / 2;
|
||||
|
||||
if (Math.abs(pacman.x - dx) < pacman.size && Math.abs(pacman.y - dy) < pacman.size) {
|
||||
score++;
|
||||
if (chompSound) {
|
||||
chompSound.currentTime = 0;
|
||||
chompSound.volume = 0.4;
|
||||
chompSound.play();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
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";
|
||||
dots.forEach(d => {
|
||||
ctx.beginPath();
|
||||
ctx.arc(d.c * CELL_SIZE + CELL_SIZE / 2, d.r * CELL_SIZE + CELL_SIZE / 2, DOT_SIZE, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Global Functions =====
|
||||
window.resetGame = resetGame;
|
||||
window.exitGame = exitGame;
|
||||
|
||||
// Add scroll prevention function
|
||||
function preventScroll(e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
@@ -1,496 +0,0 @@
|
||||
// ===== AES Encryption =====
|
||||
async function encryptAdvanced(message, password) {
|
||||
// Create a random salt for key derivation
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||
|
||||
// Derive a key from the password using PBKDF2 and the salt
|
||||
const key = await deriveKey(password, salt);
|
||||
|
||||
// Create a random initialization vector (IV)
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
|
||||
// Encode the message as a Uint8Array
|
||||
const encoder = new TextEncoder();
|
||||
const encodedMessage = encoder.encode(message);
|
||||
|
||||
// Encrypt the message using AES-GCM
|
||||
const encryptedMessage = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv: iv },
|
||||
key,
|
||||
encodedMessage
|
||||
);
|
||||
|
||||
// Combine salt, IV, and encrypted message
|
||||
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);
|
||||
|
||||
// Convert the result to base64 to send to the server
|
||||
return btoa(String.fromCharCode.apply(null, encryptedArray));
|
||||
}
|
||||
|
||||
// Derive a key from the password using PBKDF2
|
||||
async function deriveKey(password, salt) {
|
||||
const encoder = new TextEncoder();
|
||||
const passwordBuffer = encoder.encode(password);
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
passwordBuffer,
|
||||
{ name: 'PBKDF2' },
|
||||
false,
|
||||
['deriveKey']
|
||||
);
|
||||
|
||||
return crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: salt,
|
||||
iterations: 100000,
|
||||
hash: 'SHA-256',
|
||||
},
|
||||
key,
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
}
|
||||
|
||||
// ===== AES Decryption =====
|
||||
async function decryptAdvanced(encryptedData, password) {
|
||||
// Decode the base64-encoded encrypted data
|
||||
const encryptedArray = new Uint8Array(atob(encryptedData).split("").map(char => char.charCodeAt(0)));
|
||||
|
||||
// Extract salt, IV, and encrypted message from the encrypted data
|
||||
const salt = encryptedArray.slice(0, 16);
|
||||
const iv = encryptedArray.slice(16, 28);
|
||||
const encryptedMessage = encryptedArray.slice(28);
|
||||
|
||||
// Derive the key from the password and salt
|
||||
const key = await deriveKey(password, salt);
|
||||
|
||||
// Decrypt the message using AES-GCM
|
||||
const decryptedMessage = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv: iv },
|
||||
key,
|
||||
encryptedMessage
|
||||
);
|
||||
|
||||
// Decode the decrypted message to text
|
||||
const decoder = new TextDecoder();
|
||||
return decoder.decode(decryptedMessage);
|
||||
}
|
||||
|
||||
// ===== UI Toggles =====
|
||||
function toggleEncryptionOptions() {
|
||||
const type = document.getElementById("encryption-type").value;
|
||||
const pwdContainer = document.getElementById("password-input");
|
||||
pwdContainer.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";
|
||||
}
|
||||
|
||||
// ===== Remove File Button =====
|
||||
function removeFile() {
|
||||
document.getElementById("file-input").value = ""; // Clear the file input
|
||||
document.getElementById("remove-file-btn").style.display = 'none'; // Hide the remove file button
|
||||
toggleInputMode(); // Reapply the input mode logic
|
||||
document.getElementById("file-password-input").style.display = 'none'; // Hide the file password input
|
||||
}
|
||||
|
||||
// ===== Input vs. File Toggle =====
|
||||
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';
|
||||
|
||||
// Show/hide text area based on file selection
|
||||
document.getElementById("text-section").style.display =
|
||||
fileSelected ? 'none' : 'flex';
|
||||
|
||||
// Show/hide file input section when in advanced mode and no text input is given
|
||||
document.getElementById("file-section").style.display =
|
||||
(isAdvanced && !textValue) ? 'flex' : 'none';
|
||||
|
||||
// Show/hide the remove file button
|
||||
document.getElementById("remove-file-btn").style.display =
|
||||
fileSelected ? 'inline-block' : 'none';
|
||||
|
||||
// ALWAYS show the password input in advanced mode
|
||||
if (isAdvanced) {
|
||||
document.getElementById("password-input").style.display = 'flex';
|
||||
} else {
|
||||
document.getElementById("password-input").style.display = 'none';
|
||||
}
|
||||
|
||||
// Show the dedicated password input for file encryption if a file is selected
|
||||
if (fileSelected) {
|
||||
document.getElementById("file-password-input").style.display = 'flex'; // Show password input for files
|
||||
} else {
|
||||
document.getElementById("file-password-input").style.display = 'none'; // Hide when no file is selected
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Validate and Submit Form =====
|
||||
async function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
// If the encryption type is advanced, ensure password is provided
|
||||
const password = document.getElementById("password").value;
|
||||
const filePassword = document.getElementById("file-password") ? document.getElementById("file-password").value : '';
|
||||
const encryptionType = document.getElementById("encryption-type").value;
|
||||
|
||||
if (encryptionType === 'advanced' && !password && !filePassword) {
|
||||
alert("Password is required for advanced encryption.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare the form data
|
||||
const payload = {
|
||||
"encryption-type": encryptionType,
|
||||
operation: document.querySelector('input[name="operation"]:checked').value,
|
||||
message: document.getElementById("input-text").value,
|
||||
password: password,
|
||||
"file-password": filePassword
|
||||
};
|
||||
|
||||
// Handle file upload encryption/decryption
|
||||
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;
|
||||
}
|
||||
|
||||
// Handle text encryption/decryption
|
||||
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("file-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 = e.target.result;
|
||||
let encryptedMessage = await encryptAdvanced(raw, pwd);
|
||||
downloadFile(encryptedMessage, f.files[0].name + ".enc");
|
||||
};
|
||||
reader.readAsText(f.files[0]);
|
||||
}
|
||||
|
||||
function decryptFile() {
|
||||
const f = document.getElementById("file-input");
|
||||
const pwd = document.getElementById("file-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 enc = e.target.result;
|
||||
const decryptedMessage = await decryptAdvanced(enc, pwd);
|
||||
downloadFile(decryptedMessage, f.files[0].name.replace(/\.enc$/, ''));
|
||||
} catch {
|
||||
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);
|
||||
}
|
||||
|
||||
// ===== 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); // For mobile devices
|
||||
document.execCommand("copy");
|
||||
|
||||
// Show toast notification
|
||||
const toast = document.getElementById(toastId);
|
||||
toast.classList.add("show");
|
||||
setTimeout(() => toast.classList.remove("show"), 2000); // Remove toast after 2 seconds
|
||||
}
|
||||
|
||||
// ===== 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();
|
||||
}
|
||||
|
||||
// ===== 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() {
|
||||
dots = dots.filter(d=>{
|
||||
const dx = d.c*cellSize+cellSize/2, dy = d.r*cellSize+cellSize/2;
|
||||
if (Math.abs(pacman.x-dx)<pacman.size && Math.abs(pacman.y-dy)<pacman.size) {
|
||||
score++;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
ctx.fillStyle="white";
|
||||
dots.forEach(d=>{
|
||||
ctx.beginPath();
|
||||
ctx.arc(d.c*cellSize+cellSize/2, d.r*cellSize+cellSize/2, dotSize,0,Math.PI*2);
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Clear All Functionality =====
|
||||
function clearAll() {
|
||||
document.getElementById("input-text").value = "";
|
||||
document.getElementById("output-text").value = "";
|
||||
document.getElementById("file-input").value = "";
|
||||
document.getElementById("password").value = "";
|
||||
document.getElementById("file-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);
|
||||
});
|
||||
@@ -0,0 +1,353 @@
|
||||
/**
|
||||
* UI management module.
|
||||
* Handles user interface interactions and form handling.
|
||||
*/
|
||||
|
||||
import { encryptFile, decryptFile } from './fileops.js';
|
||||
|
||||
// ===== UI Initialization =====
|
||||
export function setupUI() {
|
||||
// Set initial state of remove button to hidden
|
||||
const removeBtn = document.getElementById("remove-file-btn");
|
||||
if (removeBtn) {
|
||||
removeBtn.style.display = "none";
|
||||
}
|
||||
|
||||
initializeEventListeners();
|
||||
}
|
||||
|
||||
// ===== Event Listeners =====
|
||||
function initializeEventListeners() {
|
||||
const elements = {
|
||||
encryptionType: document.getElementById("encryption-type"),
|
||||
inputText: document.getElementById("input-text"),
|
||||
form: document.getElementById("crypto-form"),
|
||||
removeFileBtn: document.getElementById("remove-file-btn"),
|
||||
clearAllBtn: document.getElementById("clear-all-btn"),
|
||||
generateBtn: document.getElementById("generate-btn"),
|
||||
copyPasswordBtn: document.getElementById("copy-btn"),
|
||||
copyOutputBtn: document.getElementById("copy-output-btn"),
|
||||
toggleSwitch: document.getElementById("operation-toggle"),
|
||||
copyShareBtn: document.getElementById("copy-share-btn"),
|
||||
shareLink: document.getElementById("share-link")
|
||||
};
|
||||
|
||||
if (validateElements(elements)) {
|
||||
setupElementListeners(elements);
|
||||
}
|
||||
}
|
||||
|
||||
function validateElements(elements) {
|
||||
return elements.encryptionType && elements.inputText && elements.form &&
|
||||
elements.removeFileBtn && elements.clearAllBtn && elements.generateBtn &&
|
||||
elements.copyPasswordBtn && elements.toggleSwitch;
|
||||
}
|
||||
|
||||
function setupElementListeners(elements) {
|
||||
elements.encryptionType.addEventListener("change", toggleEncryptionOptions);
|
||||
elements.inputText.addEventListener("input", handleInputChange);
|
||||
elements.form.addEventListener("submit", handleSubmit);
|
||||
elements.removeFileBtn.addEventListener("click", removeFile);
|
||||
elements.clearAllBtn.addEventListener("click", clearAll);
|
||||
elements.generateBtn.addEventListener("click", generateRandomPassword);
|
||||
elements.copyPasswordBtn.addEventListener("click", () => copyToClipboard("generated-password", "password-copy-feedback"));
|
||||
elements.copyOutputBtn?.addEventListener("click", () => copyToClipboard("output-text", "output-copy-feedback"));
|
||||
elements.toggleSwitch.addEventListener("change", () => {
|
||||
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() {
|
||||
const type = document.getElementById("encryption-type").value.trim().toLowerCase();
|
||||
const passwordInputWrapper = document.getElementById("password-input");
|
||||
const fileSection = document.querySelector("#encoding-section #file-section");
|
||||
const isAdvanced = type.includes("advanced");
|
||||
|
||||
if (passwordInputWrapper) {
|
||||
if (isAdvanced) {
|
||||
passwordInputWrapper.classList.remove("hidden");
|
||||
} else {
|
||||
passwordInputWrapper.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
if (fileSection) {
|
||||
if (isAdvanced) {
|
||||
fileSection.classList.remove("hidden");
|
||||
} else {
|
||||
fileSection.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
updateToggleLabels();
|
||||
toggleInputMode();
|
||||
}
|
||||
|
||||
function updateToggleLabels() {
|
||||
const type = document.getElementById("encryption-type")?.value;
|
||||
const leftLabel = document.getElementById("toggle-left-label");
|
||||
const rightLabel = document.getElementById("toggle-right-label");
|
||||
|
||||
if (!type || !leftLabel || !rightLabel) return;
|
||||
|
||||
const isAdvanced = type.toLowerCase().includes("advanced");
|
||||
leftLabel.textContent = isAdvanced ? "Encrypt" : "Encode";
|
||||
rightLabel.textContent = isAdvanced ? "Decrypt" : "Decode";
|
||||
}
|
||||
|
||||
function toggleInputMode() {
|
||||
const fileInput = document.getElementById("file-input");
|
||||
const textValue = document.getElementById("input-text")?.value.trim();
|
||||
const isAdvanced = document.getElementById("encryption-type")?.value === "advanced";
|
||||
|
||||
const textSection = document.getElementById("text-section");
|
||||
const fileSection = document.getElementById("file-section");
|
||||
const removeBtn = document.getElementById("remove-file-btn");
|
||||
|
||||
if (!fileInput || !textSection || !fileSection || !removeBtn) return;
|
||||
|
||||
const fileSelected = fileInput.files.length > 0;
|
||||
|
||||
textSection.style.display = fileSelected ? "none" : "flex";
|
||||
fileSection.style.display = (isAdvanced && !textValue) ? "flex" : "none";
|
||||
removeBtn.style.display = fileSelected ? "inline-block" : "none";
|
||||
}
|
||||
|
||||
// ===== Form Handling =====
|
||||
async function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const encryptionType = document.getElementById("encryption-type")?.value;
|
||||
const password = document.getElementById("password")?.value;
|
||||
const fileInput = document.getElementById("file-input");
|
||||
const isDecrypt = document.getElementById("operation-toggle").checked;
|
||||
const operation = isDecrypt ? "decrypt" : "encrypt";
|
||||
|
||||
if (!encryptionType || !fileInput) return;
|
||||
|
||||
if (encryptionType === "advanced" && !password) {
|
||||
return alert("Password is required for advanced encryption.");
|
||||
}
|
||||
|
||||
if (fileInput.files.length > 0) {
|
||||
return (operation === "encrypt")
|
||||
? encryptFile(fileInput, password)
|
||||
: decryptFile(fileInput, password);
|
||||
}
|
||||
|
||||
await handleTextOperation(encryptionType, operation, password);
|
||||
}
|
||||
|
||||
async function handleTextOperation(encryptionType, operation, password) {
|
||||
const payload = {
|
||||
"encryption-type": encryptionType,
|
||||
operation: operation,
|
||||
message: document.getElementById("input-text")?.value,
|
||||
password: password
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch("/", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await response.json();
|
||||
document.getElementById("output-text").value = data.result;
|
||||
} catch (err) {
|
||||
alert("Error processing request: " + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Utility Functions =====
|
||||
function removeFile() {
|
||||
const fileInput = document.getElementById("file-input");
|
||||
if (fileInput) fileInput.value = "";
|
||||
const removeBtn = document.getElementById("remove-file-btn");
|
||||
if (removeBtn) removeBtn.style.display = 'none';
|
||||
toggleInputMode();
|
||||
}
|
||||
|
||||
function generateRandomPassword() {
|
||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+[]{}|;:,.<>?/~";
|
||||
const length = 30;
|
||||
const password = Array.from({ length }, () =>
|
||||
charset.charAt(Math.floor(Math.random() * charset.length))
|
||||
).join("");
|
||||
const passwordField = document.getElementById("generated-password");
|
||||
if (passwordField) {
|
||||
passwordField.value = password;
|
||||
// Check if we should start Pacman
|
||||
checkForPacman();
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard(elementId, feedbackId) {
|
||||
const el = document.getElementById(elementId);
|
||||
const feedback = document.getElementById(feedbackId);
|
||||
|
||||
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");
|
||||
setTimeout(() => {
|
||||
feedback.classList.remove("show");
|
||||
setTimeout(() => {
|
||||
feedback.style.display = "none";
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
const fields = ["input-text", "output-text", "file-input", "password"];
|
||||
fields.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.value = "";
|
||||
});
|
||||
removeFile();
|
||||
toggleInputMode();
|
||||
document.getElementById("pacman-section")?.style.setProperty("display", "none");
|
||||
document.getElementById("encoding-section")?.style.setProperty("display", "block");
|
||||
}
|
||||
|
||||
function handleInputChange() {
|
||||
toggleInputMode();
|
||||
checkForPacman();
|
||||
}
|
||||
|
||||
function checkForPacman() {
|
||||
const val = document.getElementById("input-text").value.trim().toLowerCase();
|
||||
const pacSection = document.getElementById("pacman-section");
|
||||
const encSection = document.getElementById("encoding-section");
|
||||
|
||||
if (val.includes("pacman") && pacSection.style.display !== "block") {
|
||||
pacSection.style.display = "block";
|
||||
encSection.style.display = "none";
|
||||
window.startPacman();
|
||||
} else if (pacSection.style.display === "block" && !val.includes("pacman")) {
|
||||
window.exitGame();
|
||||
}
|
||||
}
|
||||
function copyShareLink() {
|
||||
const linkEl = document.getElementById("share-link");
|
||||
const feedback = document.getElementById("shared-link-feedback");
|
||||
|
||||
if (!linkEl) return;
|
||||
|
||||
const linkText = linkEl.href || linkEl.textContent.trim();
|
||||
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(linkText).then(() => {
|
||||
showCopyFeedback(feedback);
|
||||
}).catch(() => {
|
||||
fallbackCopy(linkText, feedback);
|
||||
});
|
||||
} else {
|
||||
fallbackCopy(linkText, feedback);
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackCopy(text, feedbackEl) {
|
||||
const tempInput = document.createElement("input");
|
||||
tempInput.value = text;
|
||||
document.body.appendChild(tempInput);
|
||||
tempInput.select();
|
||||
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
showCopyFeedback(feedbackEl);
|
||||
} catch (err) {
|
||||
alert("Copy failed. Please copy manually.");
|
||||
}
|
||||
|
||||
document.body.removeChild(tempInput);
|
||||
}
|
||||
|
||||
function showCopyFeedback(feedbackEl) {
|
||||
if (!feedbackEl) return;
|
||||
feedbackEl.style.display = "block";
|
||||
feedbackEl.classList.add("show");
|
||||
|
||||
setTimeout(() => {
|
||||
feedbackEl.classList.remove("show");
|
||||
setTimeout(() => {
|
||||
feedbackEl.style.display = "none";
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
|
||||
function startPacman() { }
|
||||
function exitGame() { }
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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') }}" />
|
||||
|
||||
<!-- 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>
|
||||
<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>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
<section class="card form-group" style="padding: 50px 30px;">
|
||||
<h2 style="color: #00ff99; font-size: 2.5em;">403 - Forbidden</h2>
|
||||
<p class="mt-4" style="font-size: 1.2em; color: #cccccc;">
|
||||
Looks like this area is locked behind a secret ghost door!
|
||||
</p>
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="button-group mt-4">
|
||||
<a href="{{ url_for('index') }}">
|
||||
<button type="button">Return Home</button>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<p>© 2025 UnNaturalll-Dev. All rights reserved.</p>
|
||||
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
|
||||
<img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
|
||||
</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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') }}" />
|
||||
|
||||
<!-- 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>
|
||||
<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>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
<section class="card form-group" style="padding: 50px 30px;">
|
||||
<h2 style="color: #ff0066; font-size: 2.5em;">404 - Not Found</h2>
|
||||
<p style="font-size: 1.2em; color: #cccccc;">
|
||||
Whoops! That page doesn't seem to exist. Maybe it got encrypted?
|
||||
</p>
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="button-group">
|
||||
<a href="{{ url_for('index') }}">
|
||||
<button type="button">Return Home</button>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<p>© 2025 UnNaturalll-Dev. All rights reserved.</p>
|
||||
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
|
||||
<img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
|
||||
</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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') }}" />
|
||||
|
||||
<!-- 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>
|
||||
<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>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
<section class="card form-group" style="padding: 50px 30px;">
|
||||
<h2 style="color: #ff3300; font-size: 2.5em;">500 - Server Error</h2>
|
||||
<p class="mt-4" style="font-size: 1.2em; color: #cccccc;">
|
||||
Uh oh! The ghosts chomped the server wires.
|
||||
We're working on patching it up.
|
||||
</p>
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="button-group mt-4">
|
||||
<a href="{{ url_for('index') }}">
|
||||
<button type="button">Return Home</button>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<p>© 2025 UnNaturalll-Dev. All rights reserved.</p>
|
||||
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
|
||||
<img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
|
||||
</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,257 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="PacCrypt - Admin Panel" />
|
||||
<title>Admin Panel - PacCrypt</title>
|
||||
|
||||
<!-- 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>
|
||||
</head>
|
||||
<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 PANEL</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
<!-- Site Map Section -->
|
||||
<section id="sitemap-section" class="card form-group">
|
||||
<h2>Server Management</h2>
|
||||
|
||||
<div class="sitemap-header">
|
||||
<button onclick="toggleSitemap()" style="margin-bottom: 10px;">Show Site Map</button>
|
||||
</div>
|
||||
|
||||
<div id="sitemap-list" class="sitemap-content" style="display: none;">
|
||||
<ul style="list-style: none; padding-left: 0;">
|
||||
{% for route in routes %}
|
||||
<li style="margin-bottom: 5px;"><code>{{ route }}</code></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Server Management Buttons -->
|
||||
<div class="admin-button-grid">
|
||||
<button onclick="restartServer()">Restart Server</button>
|
||||
<form action="{{ url_for('admin_logout') }}" method="GET" style="display: inline;">
|
||||
<button type="submit">Log Out</button>
|
||||
</form>
|
||||
<button onclick="updateServer()">Update Server</button>
|
||||
<form action="{{ url_for('admin_settings') }}" method="GET" style="display: inline;">
|
||||
<button type="submit">Settings</button>
|
||||
</form>
|
||||
<button onclick="resetAdmin()" class="danger-button">Reset Admin</button>
|
||||
<button onclick="clearUploads()" class="danger-button">Clear PacShare</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Flash Messages -->
|
||||
<div id="admin-feedback" class="copy-feedback" style="display: none;"></div>
|
||||
</section>
|
||||
|
||||
<!-- Password Change Section -->
|
||||
<section id="password-change-section" class="card form-group">
|
||||
<h2>Change Admin Password</h2>
|
||||
|
||||
<!-- Password Feedback -->
|
||||
{% with messages = get_flashed_messages(with_categories=true, category_filter=['password-feedback']) %}
|
||||
{% for category, message in messages %}
|
||||
<div class="copy-feedback show">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- Password Change Form -->
|
||||
<form method="POST" action="{{ url_for('admin_change_password') }}">
|
||||
<input type="password" name="current_password" placeholder="Current Password" required />
|
||||
<input type="password" name="new_password" placeholder="New Password" required />
|
||||
<button type="submit">Update Password</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Server Status Section -->
|
||||
<section id="server-status-section" class="card form-group">
|
||||
<h2>Server Status</h2>
|
||||
<ul class="status-list">
|
||||
<li>Uptime: <code>{{ server_info.uptime }}</code></li>
|
||||
<li>Server Time: <code>{{ server_info.server_time }}</code></li>
|
||||
<li>Python Version: <code>{{ server_info.python_version }}</code></li>
|
||||
<li>Flask Debug Mode: <code>{{ server_info.debug_mode }}</code></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- Server Logs Section -->
|
||||
<section id="server-logs-section" class="card form-group">
|
||||
<h2>Server Logs</h2>
|
||||
<button onclick="toggleLogs()" style="margin-bottom: 10px;">Show/Hide Logs</button>
|
||||
<div id="logLoader" style="display: none; margin-bottom: 10px;">Loading logs...</div>
|
||||
<pre id="logContainer" style="display: none;"></pre>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<p>© 2025 UnNaturalll-Dev. All rights reserved.</p>
|
||||
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
|
||||
<img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
|
||||
</a>
|
||||
<a href="{{ url_for('sitemap') }}">
|
||||
<img src="\static\img\sitemap.png" alt="Sitemap Png" width="55" />
|
||||
</a>
|
||||
</footer>
|
||||
|
||||
<!-- Log Viewer Script -->
|
||||
<script>
|
||||
async function toggleLogs() {
|
||||
const logContainer = document.getElementById('logContainer');
|
||||
const logLoader = document.getElementById('logLoader');
|
||||
if (logContainer.style.display === 'none') {
|
||||
logLoader.style.display = 'block';
|
||||
const response = await fetch("{{ url_for('admin_logs') }}");
|
||||
const data = await response.json();
|
||||
logLoader.style.display = 'none';
|
||||
logContainer.innerText = data.logs.join('\n');
|
||||
logContainer.style.display = 'block';
|
||||
} else {
|
||||
logContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
</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>
|
||||
</html>
|
||||
@@ -0,0 +1,62 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="PacCrypt - Admin Login" />
|
||||
<title>Admin Login - PacCrypt</title>
|
||||
|
||||
<!-- 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>
|
||||
</head>
|
||||
<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 Login</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
<!-- Login Form Section -->
|
||||
<section class="card form-group">
|
||||
<h2>Admin Login</h2>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<p style="color: red;">{{ messages[0] }}</p>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- Login Form -->
|
||||
<form method="POST">
|
||||
<input type="text" name="username" placeholder="Username" required />
|
||||
<input type="password" name="password" placeholder="Password" required />
|
||||
<div class="button-group mt-3">
|
||||
<button type="submit">Log In</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<p>© 2025 UnNaturalll-Dev. All rights reserved.</p>
|
||||
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
|
||||
<img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
|
||||
</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="PacCrypt - Admin Settings" />
|
||||
<title>Admin Settings - PacCrypt</title>
|
||||
|
||||
<!-- 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">
|
||||
|
||||
</head>
|
||||
<body class="dark">
|
||||
<!-- Header -->
|
||||
<!-- Header -->
|
||||
<header class="card logo-header">
|
||||
<div class="logo-container">
|
||||
<img src="{{ url_for('static', filename='img/PacCrypt.png') }}" alt="PacCrypt Logo" />
|
||||
<div class="logo-text">
|
||||
<h1>PACCRYPT</h1>
|
||||
<p>Server Settings</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
<!-- Settings Form Section -->
|
||||
<section class="card form-group">
|
||||
<h2>Upload Settings</h2>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<ul style="color: lime;">
|
||||
{% for message in messages %}
|
||||
<li>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- Settings Form -->
|
||||
<form method="POST">
|
||||
<label for="upload_folder">Upload Folder Path:</label>
|
||||
<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>
|
||||
<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>
|
||||
<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">
|
||||
<button type="submit">Save Settings</button>
|
||||
<a href="{{ url_for('admin_page') }}">
|
||||
<button type="button">Back to Admin Panel</button>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<p>© 2025 UnNaturalll-Dev. All rights reserved.</p>
|
||||
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
|
||||
<img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
|
||||
</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,63 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="PacCrypt - Admin Setup" />
|
||||
<title>PacCrypt - Admin Setup</title>
|
||||
|
||||
<!-- 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>
|
||||
</head>
|
||||
<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>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
<!-- Setup Form Section -->
|
||||
<section class="card form-group">
|
||||
<h2>Create Admin Account</h2>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<p style="color: red;">{{ messages[0] }}</p>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- Setup Form -->
|
||||
<form method="POST">
|
||||
<input type="text" name="username" placeholder="Username" required />
|
||||
<input type="password" name="password" placeholder="Password" required />
|
||||
<div class="button-group mt-3">
|
||||
<button type="submit">Set Credentials</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<p>© 2025 UnNaturalll-Dev. All rights reserved.</p>
|
||||
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
|
||||
<img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
|
||||
</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,109 +3,208 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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>
|
||||
<!-- Favicon Link -->
|
||||
<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="icon" href="{{ url_for('static', filename='img/PacCrypt.png') }}" type="image/png" />
|
||||
|
||||
<!-- Stylesheets -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
|
||||
<script defer src="{{ url_for('static', filename='js/script.js') }}"></script>
|
||||
<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>
|
||||
</head>
|
||||
<body class="dark">
|
||||
<header>
|
||||
<h1>PacCrypt</h1>
|
||||
<p>Secure Encoding, Encryption and Password Generation</p>
|
||||
<!-- Header -->
|
||||
<header class="card logo-header">
|
||||
<div class="logo-container">
|
||||
<img src="{{ url_for('static', filename='img/PacCrypt.png') }}" alt="PacCrypt Logo" />
|
||||
<div class="logo-text">
|
||||
<h1>PACCRYPT</h1>
|
||||
<p>Securely Share Text and Files</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
<section id="password-section" class="card">
|
||||
<!-- Password Generator Section -->
|
||||
<section id="password-generator-section" class="card form-group">
|
||||
<h2>Password Generator</h2>
|
||||
<div class="form-group">
|
||||
<input
|
||||
type="text"
|
||||
id="password-field"
|
||||
placeholder="Generated password will appear here"
|
||||
readonly
|
||||
/>
|
||||
<input type="text" id="generated-password" readonly />
|
||||
<div class="button-group">
|
||||
<button type="button" onclick="generateRandomPassword()">Generate</button>
|
||||
<button type="button" onclick="copyToClipboard('password-field', 'password-toast')">Copy</button>
|
||||
<button type="button" id="generate-btn">Generate</button>
|
||||
<button type="button" id="copy-btn">Copy Password</button>
|
||||
</div>
|
||||
<div id="password-toast" class="toast">Copied to Clipboard!</div>
|
||||
<div id="password-copy-feedback" class="copy-feedback">Password copied to clipboard!</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Pacman Game Section -->
|
||||
<section id="pacman-section" class="card" style="display: none;">
|
||||
<div class="pacman-wrapper">
|
||||
<canvas id="pacmanCanvas" width="800" height="600"></canvas>
|
||||
</div>
|
||||
<audio id="chomp-sound" src="{{ url_for('static', filename='audio/chomp.mp3') }}"></audio>
|
||||
<div class="button-group">
|
||||
<div class="button-group" style="margin-top: 6px;">
|
||||
<button type="button" onclick="resetGame()">Restart Game</button>
|
||||
<button type="button" onclick="exitGame()">Exit Game</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="encoding-section" class="card">
|
||||
<h2>Text Encoder / Decoder & File Encryption</h2>
|
||||
<form id="main-form" class="form-group" method="POST" onsubmit="handleSubmit(event)">
|
||||
<label for="encryption-type">Select Encryption Type:</label>
|
||||
<select id="encryption-type" name="encryption-type" onchange="toggleEncryptionOptions()">
|
||||
<option value="basic">Basic (Less Secure)</option>
|
||||
<option value="advanced" selected>Advanced (More Secure)</option>
|
||||
<!-- Encryption/Decryption Section -->
|
||||
<section id="encoding-section" class="card form-group">
|
||||
<h2>Encrypt & Decrypt</h2>
|
||||
<form id="crypto-form" class="form-group">
|
||||
<!-- Encryption Type Selection -->
|
||||
<div class="form-group">
|
||||
<label for="encryption-type">Encryption Type:</label>
|
||||
<select id="encryption-type">
|
||||
<option value="basic">Basic Cipher</option>
|
||||
<option value="advanced" selected>Advanced AES</option>
|
||||
</select>
|
||||
|
||||
<div id="encryption-options" class="radio-group">
|
||||
<label class="radio-button">
|
||||
<input type="radio" name="operation" value="encrypt" id="encrypt-radio" checked />
|
||||
<span id="encrypt-label">Encrypt</span>
|
||||
</label>
|
||||
<label class="radio-button">
|
||||
<input type="radio" name="operation" value="decrypt" id="decrypt-radio" />
|
||||
<span id="decrypt-label">Decrypt</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Operation Toggle -->
|
||||
<div class="toggle-container">
|
||||
<span class="toggle-label">Encrypt</span>
|
||||
<label class="material-switch">
|
||||
<input type="checkbox" id="operation-toggle">
|
||||
<span class="material-slider"></span>
|
||||
</label>
|
||||
<span class="toggle-label">Decrypt</span>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Text Input Section -->
|
||||
<div id="text-section" class="form-group">
|
||||
<textarea
|
||||
id="input-text"
|
||||
name="message"
|
||||
placeholder="Enter text here..."
|
||||
oninput="toggleInputMode()"
|
||||
></textarea>
|
||||
<div id="password-input">
|
||||
<input type="password" id="password" name="password" placeholder="Enter Password" />
|
||||
</div>
|
||||
<textarea id="input-text" placeholder="Enter your message..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Password Input -->
|
||||
<div id="password-input" class="form-group">
|
||||
<input type="password" id="password" placeholder="Encryption/Decryption Password" />
|
||||
</div>
|
||||
|
||||
<!-- File Input Section -->
|
||||
<div id="file-section" class="form-group" style="display: none;">
|
||||
<input type="file" id="file-input" onchange="toggleInputMode()" />
|
||||
<button type="button" id="remove-file-btn" onclick="removeFile()">Remove File</button>
|
||||
<input type="file" id="file-input" />
|
||||
<button type="button" id="remove-file-btn">Remove File</button>
|
||||
</div>
|
||||
|
||||
<div id="file-password-input" style="display: none;">
|
||||
<input type="password" id="file-password" name="file-password" placeholder="Enter Password for File" />
|
||||
</div>
|
||||
|
||||
<button type="button" class="submit-button" onclick="handleSubmit(event)">Submit</button>
|
||||
</form>
|
||||
|
||||
<div style="height: 20px;"></div>
|
||||
|
||||
<textarea id="output-text" readonly placeholder="Result will appear here">{{ result }}</textarea>
|
||||
<!-- Action Buttons -->
|
||||
<div class="button-group">
|
||||
<button type="button" onclick="copyToClipboard('output-text', 'output-toast')">Copy Output</button>
|
||||
<button type="button" onclick="clearAll()">Clear All</button>
|
||||
<button type="submit">Execute</button>
|
||||
<button type="button" id="copy-output-btn">Copy Output</button>
|
||||
</div>
|
||||
<div id="output-toast" class="toast">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>
|
||||
</section>
|
||||
|
||||
<!-- File Sharing Section -->
|
||||
<section id="sharing-section" class="card form-group">
|
||||
<h2 style="margin-bottom: unset;">PacShare</h2>
|
||||
<p style="margin-top: unset;">Securely share encrypted files.</p>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<ul style="color: lime; list-style: none; padding-left: 0;">
|
||||
{% for message in messages %}
|
||||
<li>
|
||||
{{ message | safe }}
|
||||
{% if "pickup" in message %}
|
||||
<div class="share-link-container">
|
||||
<a id="share-link" href="{{ message.split(' at ')[1] }}" target="_blank">{{ message.split(" at ")[1] }}</a>
|
||||
<button type="button" onclick="copyShareLink()">Copy Link</button>
|
||||
<div id="shared-link-feedback" class="copy-feedback">Link copied to clipboard!</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<script>window.onload = () => window.scrollTo(0, document.body.scrollHeight);</script>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
|
||||
<!-- 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="password" name="enc_password" placeholder="Encryption/Decryption Password" required />
|
||||
<input type="password" name="pickup_password" placeholder="Pickup Password" required />
|
||||
<div class="button-group">
|
||||
<button type="submit">Upload and Generate Link</button>
|
||||
</div>
|
||||
</form>
|
||||
<p style="color: #9c0000;">BOTH PASSWORDS ARE REQUIRED FOR PICKUP</p>
|
||||
<script>
|
||||
document.getElementById('upload-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
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;">
|
||||
Files expire after {{ settings.max_file_age_days }} days.<br />
|
||||
Max file size: {{ settings.max_file_size_bytes // (1024 * 1024 * 1024) }} GB.
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<p>© 2025 UnNaturalll-Dev. All rights reserved.</p>
|
||||
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
|
||||
<img src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Flogos-world.net%2Fwp-content%2Fuploads%2F2020%2F11%2FGitHub-Logo.png&f=1&nofb=1&ipt=b9d67651e313b2cdbeae8a7ec9320dadb278a21a2e7217810b839c233c04f265"
|
||||
alt="GitHub Logo" width="100" />
|
||||
<img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
|
||||
</a>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="PacCrypt - Secure file pickup and decryption" />
|
||||
<title>PacCrypt - Secure File Pickup</title>
|
||||
|
||||
<!-- 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>
|
||||
<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>Encrypted File Pickup</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
<!-- File Pickup Section -->
|
||||
<section id="pickup-section" class="card form-group">
|
||||
<h2>File Pickup</h2>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<ul style="color: lime; list-style: none; padding-left: 0;">
|
||||
{% for message in messages %}
|
||||
<li>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- File Info -->
|
||||
<div class="form-group">
|
||||
<p style="color: #00ff99; margin-bottom: 15px;">File ID: <code>{{ file_id }}</code></p>
|
||||
</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>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Security Notice Section -->
|
||||
<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>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<p>© 2025 UnNaturalll-Dev. All rights reserved.</p>
|
||||
<a href="https://github.com/TySP-Dev" target="_blank" id="github-link">
|
||||
<img src="\static\img\Github_logo.png" alt="GitHub Logo" width="100" />
|
||||
</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||