Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ad2b65aba | |||
| 265dff3329 | |||
| 9e45c34365 |
@@ -1,77 +1,177 @@
|
|||||||
# PacCrypt WebApp
|
# PacCrypt WebApp
|
||||||
|
|
||||||
**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
|
Live demo: [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+
|
||||||
git clone https://github.com/TySP-Dev/PacCrypt.git
|
- Flask 3+
|
||||||
cd paccrypt-webapp
|
- Cryptography 42+
|
||||||
|
- Waitress 2.1+
|
||||||
|
- Git (for update feature)
|
||||||
|
- Nginx (recommended)
|
||||||
|
|
||||||
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:
|
### ⚡ Quick Setup
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
4. Run the Flask app:
|
```bash
|
||||||
python app.py
|
git clone https://github.com/TySP-Dev/PacCrypt.git
|
||||||
|
cd paccrypt-webapp-final
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # or venv\Scripts\activate on Windows
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
5. Open http://127.0.0.1:5000 to access the app locally.
|
Then run:
|
||||||
|
|
||||||
## Usage
|
- Development Mode:
|
||||||
|
```bash
|
||||||
|
./start_dev.sh # or start_dev.bat
|
||||||
|
```
|
||||||
|
|
||||||
### Encryption and Decryption
|
- Production Mode:
|
||||||
|
```bash
|
||||||
|
./start_prod.sh # or start_prod.bat
|
||||||
|
```
|
||||||
|
|
||||||
Select the encryption type (Basic or Advanced).
|
Visit [http://127.0.0.1:5000](http://127.0.0.1:5000)
|
||||||
|
|
||||||
For text encryption/decryption:
|
---
|
||||||
|
|
||||||
Enter text in the Input Text area.
|
## 🧭 Navigation & Usage
|
||||||
|
|
||||||
Choose whether to Encrypt or Decrypt.
|
### 🔐 Encrypt & Decrypt
|
||||||
|
|
||||||
Enter a password (if using advanced encryption).
|
- Choose between Basic Cipher or Advanced AES
|
||||||
|
- Type your message or upload a file
|
||||||
|
- Enter password (if AES)
|
||||||
|
- Select mode using toggle (Encrypt/Decrypt)
|
||||||
|
- Hit Execute
|
||||||
|
|
||||||
For file encryption/decryption:
|
### 📤 Share Files
|
||||||
|
|
||||||
Upload a file.
|
- Upload a file with two passwords:
|
||||||
|
- Encryption password
|
||||||
|
- Pickup password
|
||||||
|
- Get a shareable URL and click 📋 Copy Link
|
||||||
|
|
||||||
Enter a password for encryption/decryption.
|
### 🔑 Generate Passwords
|
||||||
|
|
||||||
Click Encrypt or Decrypt.
|
- Click Generate
|
||||||
|
- Then hit 📋 Copy
|
||||||
|
|
||||||
### Password Generation
|
### 🎮 Pac-Man Game
|
||||||
|
|
||||||
Click the Generate button to create a random password, then use the Copy button to copy it to your clipboard.
|
- Type `pacman` in the input box
|
||||||
|
- Game appears with Restart/Exit controls
|
||||||
|
- Classic arrow key controls 🕹️
|
||||||
|
|
||||||
### Pac-Man Game (Easter Egg)
|
---
|
||||||
|
|
||||||
Type the word "pacman" in the input box to unlock the Pac-Man game!
|
## 🛠️ Admin Panel
|
||||||
|
|
||||||
### Contributing
|
Visit `/adminpage` after setting up credentials at `/admin-setup`.
|
||||||
|
|
||||||
Feel free to open an issue or submit a pull request for improvements, bug fixes, or new features!
|
Features:
|
||||||
|
- 🔄 Restart server
|
||||||
|
- 🔃 Update from GitHub (git pull)
|
||||||
|
- 🧽 Clear uploads
|
||||||
|
- 🔐 Change admin password
|
||||||
|
- 📝 View logs
|
||||||
|
- ⚙️ Adjust upload settings
|
||||||
|
|
||||||
### License
|
---
|
||||||
|
|
||||||
This project is open source and available under the MIT License.
|
## 🛡️ Deployment Tips
|
||||||
|
|
||||||
|
Minimal Nginx config:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name yourdomain.com;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use Let's Encrypt to add SSL/TLS support.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗂️ Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
paccrypt-webapp-final/
|
||||||
|
├── app.py
|
||||||
|
├── requirements.txt
|
||||||
|
├── README.md
|
||||||
|
├── templates/
|
||||||
|
│ ├── index.html
|
||||||
|
│ ├── 404.html
|
||||||
|
│ └── 403.html
|
||||||
|
│ └── 500.html
|
||||||
|
│ └── admin.html
|
||||||
|
│ └── admin_login.html
|
||||||
|
│ └── admin_settings.html
|
||||||
|
│ └── admin_setup.html
|
||||||
|
│ └── pickup.html
|
||||||
|
├── static/
|
||||||
|
│ ├── css/
|
||||||
|
│ │ └── styles.css
|
||||||
|
│ ├── js/
|
||||||
|
│ │ └── 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,529 @@
|
|||||||
from flask import Flask, render_template, request, jsonify
|
|
||||||
import html
|
|
||||||
import os
|
import os
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import html
|
||||||
import base64
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import secrets
|
||||||
|
import shutil
|
||||||
|
import datetime
|
||||||
|
import subprocess
|
||||||
|
import platform
|
||||||
|
|
||||||
|
from flask import (
|
||||||
|
Flask, render_template, request, jsonify, session,
|
||||||
|
redirect, url_for, flash, send_file
|
||||||
|
)
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||||
from cryptography.hazmat.primitives.hashes import SHA256
|
from cryptography.hazmat.primitives.hashes import SHA256
|
||||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||||
from waitress import serve
|
from cryptography.fernet import Fernet
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
app.secret_key = os.getenv("FLASK_SECRET", os.urandom(24))
|
||||||
|
|
||||||
# Basic Encoder/Decoder
|
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')
|
ALPHABET = list('abcdefghijklmnopqrstuvwxyz')
|
||||||
|
|
||||||
|
DEFAULT_SETTINGS = {
|
||||||
|
"upload_folder": "uploads",
|
||||||
|
"max_file_age_days": 14,
|
||||||
|
"max_file_size_bytes": 25 * 1024 * 1024 * 1024 # 25GB
|
||||||
|
}
|
||||||
|
|
||||||
|
# === Settings ===
|
||||||
|
def load_settings():
|
||||||
|
if not os.path.exists(SETTINGS_FILE):
|
||||||
|
with open(SETTINGS_FILE, 'w') as f:
|
||||||
|
json.dump(DEFAULT_SETTINGS, f)
|
||||||
|
with open(SETTINGS_FILE, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
settings = load_settings()
|
||||||
|
UPLOAD_FOLDER = settings["upload_folder"]
|
||||||
|
MAX_FILE_AGE_DAYS = settings["max_file_age_days"]
|
||||||
|
MAX_FILE_SIZE_BYTES = settings["max_file_size_bytes"]
|
||||||
|
|
||||||
|
if not os.path.exists(UPLOAD_FOLDER):
|
||||||
|
os.makedirs(UPLOAD_FOLDER)
|
||||||
|
|
||||||
|
# === Crypto ===
|
||||||
|
def derive_key(password: str, salt: bytes) -> bytes:
|
||||||
|
return PBKDF2HMAC(algorithm=SHA256(), length=32, salt=salt, iterations=200_000).derive(password.encode())
|
||||||
|
|
||||||
|
def hash_password(password: str, salt: bytes) -> str:
|
||||||
|
return base64.urlsafe_b64encode(derive_key(password, salt)).decode()
|
||||||
|
|
||||||
def simple_encode(text: str) -> str:
|
def simple_encode(text: str) -> str:
|
||||||
return ''.join(
|
return ''.join(ALPHABET[(ALPHABET.index(c) + 3) % 26] if c in ALPHABET else c for c in text.lower())
|
||||||
ALPHABET[(ALPHABET.index(c) + 3) % 26] if c in ALPHABET else c
|
|
||||||
for c in text.lower()
|
|
||||||
)
|
|
||||||
|
|
||||||
def simple_decode(text: str) -> str:
|
def simple_decode(text: str) -> str:
|
||||||
return ''.join(
|
return ''.join(ALPHABET[(ALPHABET.index(c) - 3) % 26] if c in ALPHABET else c for c in text.lower())
|
||||||
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())
|
|
||||||
|
|
||||||
def advanced_encrypt(plaintext: str, password: str) -> str:
|
def advanced_encrypt(plaintext: str, password: str) -> str:
|
||||||
salt = os.urandom(16)
|
salt = os.urandom(16)
|
||||||
key = derive_key(password, salt)
|
key = derive_key(password, salt)
|
||||||
|
|
||||||
aesgcm = AESGCM(key)
|
|
||||||
nonce = os.urandom(12)
|
nonce = os.urandom(12)
|
||||||
|
ct = AESGCM(key).encrypt(nonce, plaintext.encode(), None)
|
||||||
ct = aesgcm.encrypt(nonce, plaintext.encode(), None)
|
return base64.urlsafe_b64encode(salt + nonce + ct).decode()
|
||||||
encrypted = salt + nonce + ct
|
|
||||||
return base64.urlsafe_b64encode(encrypted).decode()
|
|
||||||
|
|
||||||
def advanced_decrypt(token_b64: str, password: str) -> str:
|
def advanced_decrypt(token_b64: str, password: str) -> str:
|
||||||
try:
|
try:
|
||||||
data = base64.urlsafe_b64decode(token_b64.encode())
|
data = base64.urlsafe_b64decode(token_b64.encode())
|
||||||
salt, nonce, ct = data[:16], data[16:28], data[28:]
|
salt, nonce, ct = data[:16], data[16:28], data[28:]
|
||||||
key = derive_key(password, salt)
|
key = derive_key(password, salt)
|
||||||
aesgcm = AESGCM(key)
|
return AESGCM(key).decrypt(nonce, ct, None).decode()
|
||||||
pt = aesgcm.decrypt(nonce, ct, None)
|
|
||||||
return pt.decode()
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return "[Error] Invalid password or corrupted data!"
|
return "[Error] Invalid password or corrupted data!"
|
||||||
|
|
||||||
# Combined Route for Page & AJAX
|
# === Admin Auth ===
|
||||||
|
def load_admin_key():
|
||||||
|
if not os.path.exists(ADMIN_KEY_FILE):
|
||||||
|
with open(ADMIN_KEY_FILE, 'wb') as f:
|
||||||
|
f.write(Fernet.generate_key())
|
||||||
|
with open(ADMIN_KEY_FILE, 'rb') as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
def encrypt_creds(username, password):
|
||||||
|
key = load_admin_key()
|
||||||
|
cipher = Fernet(key)
|
||||||
|
salt = os.urandom(16)
|
||||||
|
hashed_pw = hash_password(password, salt)
|
||||||
|
data = json.dumps({"u": username, "p": hashed_pw, "s": base64.b64encode(salt).decode()}).encode()
|
||||||
|
with open(ADMIN_CRED_FILE, 'wb') as f:
|
||||||
|
f.write(cipher.encrypt(data))
|
||||||
|
|
||||||
|
def check_creds(username, password):
|
||||||
|
try:
|
||||||
|
key = load_admin_key()
|
||||||
|
cipher = Fernet(key)
|
||||||
|
with open(ADMIN_CRED_FILE, 'rb') as f:
|
||||||
|
decrypted = cipher.decrypt(f.read())
|
||||||
|
creds = json.loads(decrypted)
|
||||||
|
salt = base64.b64decode(creds["s"])
|
||||||
|
return creds["u"] == username and creds["p"] == hash_password(password, salt)
|
||||||
|
except Exception as e:
|
||||||
|
print("[ERROR] check_creds failed:", e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def log_admin_event(message: str):
|
||||||
|
try:
|
||||||
|
key = load_admin_key()
|
||||||
|
cipher = Fernet(key)
|
||||||
|
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
encrypted = cipher.encrypt(f"[{timestamp}] {message}".encode())
|
||||||
|
with open(ADMIN_LOG_FILE, 'ab') as f:
|
||||||
|
f.write(encrypted + b"\n")
|
||||||
|
except Exception as e:
|
||||||
|
print("[ERROR] Failed to write admin log:", e)
|
||||||
|
|
||||||
|
# === Text Encryption Route ===
|
||||||
@app.route("/", methods=["GET", "POST"])
|
@app.route("/", methods=["GET", "POST"])
|
||||||
def index():
|
def index():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
data = request.get_json()
|
if 'file' in request.files: # <-- Handling file upload
|
||||||
encryption_type = data.get("encryption-type", "basic")
|
file = request.files['file']
|
||||||
operation = data.get("operation", "")
|
enc_password = request.form.get('enc_password')
|
||||||
message = data.get("message", "")
|
pickup_password = request.form.get('pickup_password')
|
||||||
password = data.get("password", "")
|
|
||||||
file_password = data.get("file-password", "")
|
|
||||||
|
|
||||||
final_password = file_password if file_password else password
|
if not file or not enc_password or not pickup_password:
|
||||||
|
flash('Missing fields')
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
if encryption_type == "basic":
|
if file.content_length and file.content_length > MAX_FILE_SIZE_BYTES:
|
||||||
result = simple_encode(message) if operation == "encrypt" else simple_decode(message)
|
flash(f"File too large! Limit: {MAX_FILE_SIZE_BYTES / (1024**3):.2f} GB")
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
temp_path = os.path.join(UPLOAD_FOLDER, filename)
|
||||||
|
file.save(temp_path)
|
||||||
|
|
||||||
|
with open(temp_path, 'rb') as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
salt = os.urandom(16)
|
||||||
|
key = derive_key(enc_password, salt)
|
||||||
|
nonce = os.urandom(12)
|
||||||
|
ct = AESGCM(key).encrypt(nonce, data, None)
|
||||||
|
|
||||||
|
random_id = secrets.token_urlsafe(24)
|
||||||
|
|
||||||
|
with open(os.path.join(UPLOAD_FOLDER, f"{random_id}.enc"), 'wb') as f:
|
||||||
|
f.write(salt + nonce + ct)
|
||||||
|
os.remove(temp_path)
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
'pickup_password': base64.urlsafe_b64encode(hashlib.sha256(pickup_password.encode()).digest()).decode(),
|
||||||
|
'original_name': filename,
|
||||||
|
'timestamp': datetime.datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
with open(os.path.join(UPLOAD_FOLDER, f"{random_id}.json"), 'w') as f:
|
||||||
|
json.dump(meta, f)
|
||||||
|
|
||||||
|
pickup_url = request.host_url.rstrip('/') + url_for('pickup_file', file_id=random_id)
|
||||||
|
flash(pickup_url)
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
else: # <-- Handling encryption/decryption
|
||||||
|
data = request.get_json()
|
||||||
|
encryption_type = data.get("encryption-type", "basic")
|
||||||
|
operation = data.get("operation", "")
|
||||||
|
message = data.get("message", "")
|
||||||
|
password = data.get("password", "")
|
||||||
|
|
||||||
|
if encryption_type == "basic":
|
||||||
|
result = simple_encode(message) if operation == "encrypt" else simple_decode(message)
|
||||||
|
else:
|
||||||
|
result = advanced_encrypt(message, password) if operation == "encrypt" else advanced_decrypt(message, password)
|
||||||
|
|
||||||
|
return jsonify(result=html.escape(result))
|
||||||
|
|
||||||
|
return render_template("index.html", result="", password="", encryption_type="advanced", settings=settings)
|
||||||
|
|
||||||
|
# === File Pickup Route ===
|
||||||
|
@app.route("/pickup/<file_id>", methods=["GET", "POST"])
|
||||||
|
def pickup_file(file_id):
|
||||||
|
meta_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.json")
|
||||||
|
enc_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.enc")
|
||||||
|
|
||||||
|
if not os.path.exists(meta_path) or not os.path.exists(enc_path):
|
||||||
|
flash("File not found or expired")
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
pickup_password = request.form.get('pickup_password')
|
||||||
|
enc_password = request.form.get('enc_password')
|
||||||
|
|
||||||
|
if not pickup_password or not enc_password:
|
||||||
|
flash("Missing fields")
|
||||||
|
return redirect(request.url)
|
||||||
|
|
||||||
|
with open(meta_path, 'r') as f:
|
||||||
|
meta = json.load(f)
|
||||||
|
|
||||||
|
expected_hash = base64.urlsafe_b64encode(hashlib.sha256(pickup_password.encode()).digest()).decode()
|
||||||
|
if expected_hash != meta['pickup_password']:
|
||||||
|
flash("Incorrect pickup password")
|
||||||
|
return redirect(request.url)
|
||||||
|
|
||||||
|
with open(enc_path, 'rb') as f:
|
||||||
|
enc_data = f.read()
|
||||||
|
salt, nonce, ct = enc_data[:16], enc_data[16:28], enc_data[28:]
|
||||||
|
key = derive_key(enc_password, salt)
|
||||||
|
|
||||||
|
try:
|
||||||
|
decrypted = AESGCM(key).decrypt(nonce, ct, None)
|
||||||
|
except Exception:
|
||||||
|
flash("Decryption failed")
|
||||||
|
return redirect(request.url)
|
||||||
|
|
||||||
|
os.remove(meta_path)
|
||||||
|
os.remove(enc_path)
|
||||||
|
log_admin_event(f"File {file_id} downloaded and deleted.")
|
||||||
|
|
||||||
|
return send_file(io.BytesIO(decrypted), as_attachment=True, download_name=meta['original_name'])
|
||||||
|
|
||||||
|
return render_template("pickup.html", file_id=file_id)
|
||||||
|
|
||||||
|
def cleanup_expired_files():
|
||||||
|
now = datetime.datetime.utcnow()
|
||||||
|
|
||||||
|
for fname in os.listdir(UPLOAD_FOLDER):
|
||||||
|
if fname.endswith(".enc") or fname.endswith(".json"):
|
||||||
|
path = os.path.join(UPLOAD_FOLDER, fname)
|
||||||
|
try:
|
||||||
|
file_time = datetime.datetime.utcfromtimestamp(os.path.getmtime(path))
|
||||||
|
age = (now - file_time).days
|
||||||
|
if age > MAX_FILE_AGE_DAYS:
|
||||||
|
os.remove(path)
|
||||||
|
print(f"[INFO] Deleted expired file: {fname}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Could not check/delete file {fname}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# === Admin Log Viewer ===
|
||||||
|
@app.route("/admin-logs")
|
||||||
|
def admin_logs():
|
||||||
|
if not session.get("admin_logged_in"):
|
||||||
|
return redirect(url_for("admin_login"))
|
||||||
|
|
||||||
|
logs = []
|
||||||
|
try:
|
||||||
|
key = load_admin_key()
|
||||||
|
cipher = Fernet(key)
|
||||||
|
if os.path.exists(ADMIN_LOG_FILE):
|
||||||
|
with open(ADMIN_LOG_FILE, 'rb') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
for line in lines[-100:]:
|
||||||
|
if line.strip():
|
||||||
|
try:
|
||||||
|
decrypted = cipher.decrypt(line.strip())
|
||||||
|
logs.append(decrypted.decode())
|
||||||
|
except Exception:
|
||||||
|
logs.append("[Error] Corrupted log entry.")
|
||||||
|
except Exception as e:
|
||||||
|
logs.append(f"[Error loading logs] {str(e)}")
|
||||||
|
|
||||||
|
return jsonify(logs=logs)
|
||||||
|
|
||||||
|
# === Admin Settings Editor ===
|
||||||
|
@app.route("/admin-settings", methods=["GET", "POST"])
|
||||||
|
def admin_settings():
|
||||||
|
if not session.get("admin_logged_in"):
|
||||||
|
return redirect(url_for("admin_login"))
|
||||||
|
|
||||||
|
current_settings = load_settings()
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
upload_folder = request.form.get('upload_folder', current_settings.get('upload_folder', 'uploads'))
|
||||||
|
max_file_age_days = int(request.form.get('max_file_age_days', current_settings.get('max_file_age_days', 14)))
|
||||||
|
max_file_size_gb = float(request.form.get('max_file_size_gb', current_settings.get('max_file_size_bytes', 25 * 1024 * 1024 * 1024) / (1024 * 1024 * 1024)))
|
||||||
|
max_file_size_bytes = int(max_file_size_gb * 1024 * 1024 * 1024)
|
||||||
|
|
||||||
|
updated_settings = {
|
||||||
|
"upload_folder": upload_folder,
|
||||||
|
"max_file_age_days": max_file_age_days,
|
||||||
|
"max_file_size_bytes": max_file_size_bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(SETTINGS_FILE, 'w') as f:
|
||||||
|
json.dump(updated_settings, f)
|
||||||
|
|
||||||
|
flash("Settings updated successfully!")
|
||||||
|
|
||||||
|
global settings, UPLOAD_FOLDER, MAX_FILE_AGE_DAYS, MAX_FILE_SIZE_BYTES
|
||||||
|
settings = load_settings()
|
||||||
|
UPLOAD_FOLDER = settings.get('upload_folder', 'uploads')
|
||||||
|
MAX_FILE_AGE_DAYS = settings.get('max_file_age_days', 14)
|
||||||
|
MAX_FILE_SIZE_BYTES = settings.get('max_file_size_bytes', 25 * 1024 * 1024 * 1024)
|
||||||
|
|
||||||
|
if not os.path.exists(UPLOAD_FOLDER):
|
||||||
|
os.makedirs(UPLOAD_FOLDER)
|
||||||
|
|
||||||
|
return redirect(url_for("admin_settings"))
|
||||||
|
|
||||||
|
return render_template("admin_settings.html", settings=current_settings)
|
||||||
|
|
||||||
|
# === Admin Setup ===
|
||||||
|
@app.route("/admin-setup", methods=["GET", "POST"])
|
||||||
|
def admin_setup():
|
||||||
|
if os.path.exists(ADMIN_CRED_FILE):
|
||||||
|
return redirect(url_for("admin_login"))
|
||||||
|
if request.method == "POST":
|
||||||
|
u = request.form.get("username")
|
||||||
|
p = request.form.get("password")
|
||||||
|
if u and p:
|
||||||
|
encrypt_creds(u, p)
|
||||||
|
session["admin_logged_in"] = True
|
||||||
|
return redirect(url_for("admin_page"))
|
||||||
|
flash("Both fields required")
|
||||||
|
return render_template("admin_setup.html")
|
||||||
|
|
||||||
|
# === Admin Login ===
|
||||||
|
@app.route("/admin-login", methods=["GET", "POST"])
|
||||||
|
def admin_login():
|
||||||
|
if request.method == "POST":
|
||||||
|
u = request.form.get("username")
|
||||||
|
p = request.form.get("password")
|
||||||
|
if check_creds(u, p):
|
||||||
|
session["admin_logged_in"] = True
|
||||||
|
log_admin_event("Admin login successful.")
|
||||||
|
return redirect(url_for("admin_page"))
|
||||||
else:
|
else:
|
||||||
result = advanced_encrypt(message, final_password) if operation == "encrypt" else advanced_decrypt(message, final_password)
|
log_admin_event("Admin login failed.")
|
||||||
|
flash("Incorrect credentials")
|
||||||
|
return render_template("admin_login.html")
|
||||||
|
|
||||||
return jsonify(result=html.escape(result))
|
# === Admin Logout ===
|
||||||
|
@app.route("/admin-logout")
|
||||||
|
def admin_logout():
|
||||||
|
session.pop("admin_logged_in", None)
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
return render_template(
|
# === Admin Page ===
|
||||||
"index.html",
|
@app.route("/adminpage")
|
||||||
result="",
|
def admin_page():
|
||||||
password="",
|
if not session.get("admin_logged_in"):
|
||||||
encryption_type="advanced"
|
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']
|
||||||
|
|
||||||
|
try:
|
||||||
|
uptime = subprocess.check_output("uptime -p", shell=True).decode().strip()
|
||||||
|
except Exception:
|
||||||
|
uptime = "Unavailable"
|
||||||
|
|
||||||
|
server_info = {
|
||||||
|
"uptime": uptime,
|
||||||
|
"time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"python": platform.python_version(),
|
||||||
|
"debug": app.debug
|
||||||
|
}
|
||||||
|
|
||||||
|
return render_template("admin.html", routes=routes, server_info=server_info)
|
||||||
|
|
||||||
|
# === Restart Server ===
|
||||||
|
@app.route("/restart-server")
|
||||||
|
def restart_server():
|
||||||
|
if not session.get("admin_logged_in"):
|
||||||
|
return redirect(url_for("admin_login"))
|
||||||
|
subprocess.Popen(["sudo", "systemctl", "restart", "paccrypt.service"])
|
||||||
|
flash("Restart triggered")
|
||||||
|
return redirect(url_for("admin_page"))
|
||||||
|
|
||||||
|
# === Reset Admin Credentials ===
|
||||||
|
@app.route("/admin-reset", methods=["POST"])
|
||||||
|
def admin_reset():
|
||||||
|
if not session.get("admin_logged_in"):
|
||||||
|
return redirect(url_for("admin_login"))
|
||||||
|
try:
|
||||||
|
if os.path.exists(ADMIN_CRED_FILE):
|
||||||
|
os.remove(ADMIN_CRED_FILE)
|
||||||
|
if os.path.exists(ADMIN_KEY_FILE):
|
||||||
|
os.remove(ADMIN_KEY_FILE)
|
||||||
|
session.pop("admin_logged_in", None)
|
||||||
|
flash("Admin credentials reset. Please create new credentials.")
|
||||||
|
except Exception as e:
|
||||||
|
flash("Failed to reset admin credentials.")
|
||||||
|
print("[ERROR] admin_reset failed:", e)
|
||||||
|
return redirect(url_for("admin_setup"))
|
||||||
|
|
||||||
|
# === Change Admin Password ===
|
||||||
|
@app.route("/admin-change-password", methods=["POST"])
|
||||||
|
def admin_change_password():
|
||||||
|
if not session.get("admin_logged_in"):
|
||||||
|
return redirect(url_for("admin_login"))
|
||||||
|
|
||||||
|
current = request.form.get("current_password")
|
||||||
|
new = request.form.get("new_password")
|
||||||
|
|
||||||
|
try:
|
||||||
|
key = load_admin_key()
|
||||||
|
cipher = Fernet(key)
|
||||||
|
with open(ADMIN_CRED_FILE, 'rb') as file:
|
||||||
|
decrypted = cipher.decrypt(file.read())
|
||||||
|
creds = json.loads(decrypted)
|
||||||
|
|
||||||
|
salt = base64.b64decode(creds["s"])
|
||||||
|
if hash_password(current, salt) != creds["p"]:
|
||||||
|
flash("Current password is incorrect")
|
||||||
|
return redirect(url_for("admin_page"))
|
||||||
|
|
||||||
|
creds["p"] = hash_password(new, salt)
|
||||||
|
encrypted = cipher.encrypt(json.dumps(creds).encode())
|
||||||
|
with open(ADMIN_CRED_FILE, 'wb') as file:
|
||||||
|
file.write(encrypted)
|
||||||
|
|
||||||
|
log_admin_event("Admin password changed.")
|
||||||
|
flash("Password updated successfully", "password-feedback")
|
||||||
|
return redirect(url_for("admin_page"))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
flash("Failed to update password")
|
||||||
|
print("[ERROR] Password change failed:", e)
|
||||||
|
return redirect(url_for("admin_page"))
|
||||||
|
|
||||||
|
@app.route("/admin-clear-uploads", methods=["POST"])
|
||||||
|
def admin_clear_uploads():
|
||||||
|
if not session.get("admin_logged_in"):
|
||||||
|
return redirect(url_for("admin_login"))
|
||||||
|
|
||||||
|
deleted = 0
|
||||||
|
for filename in os.listdir(UPLOAD_FOLDER):
|
||||||
|
if filename.endswith(".enc") or filename.endswith(".json"):
|
||||||
|
try:
|
||||||
|
os.remove(os.path.join(UPLOAD_FOLDER, filename))
|
||||||
|
deleted += 1
|
||||||
|
except Exception as e:
|
||||||
|
print("[ERROR] Failed to delete:", filename, e)
|
||||||
|
|
||||||
|
flash(f"Cleared {deleted} uploaded file(s).", "clear-feedback")
|
||||||
|
return redirect(url_for("admin_page"))
|
||||||
|
|
||||||
|
@app.route("/admin-update-server", methods=["POST"])
|
||||||
|
def admin_update_server():
|
||||||
|
if not session.get("admin_logged_in"):
|
||||||
|
return redirect(url_for("admin_login"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
output = subprocess.check_output(["git", "pull", "origin", "main"], cwd=os.getcwd()).decode()
|
||||||
|
flash("✅ Server updated successfully!<br><pre>" + html.escape(output) + "</pre>", "update")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
flash("❌ Failed to update server:<br><pre>" + html.escape(e.output.decode()) + "</pre>", "update")
|
||||||
|
except Exception as ex:
|
||||||
|
flash("❌ An error occurred: " + str(ex), "update")
|
||||||
|
|
||||||
|
return redirect(url_for("admin_page"))
|
||||||
|
|
||||||
|
@app.route("/sitemap")
|
||||||
|
def sitemap():
|
||||||
|
output = ["<h1>PacCrypt Sitemap</h1>", "<ul>"]
|
||||||
|
for rule in app.url_map.iter_rules():
|
||||||
|
if rule.endpoint != 'static':
|
||||||
|
url = url_for(rule.endpoint, **{arg: f"<{arg}>" for arg in rule.arguments})
|
||||||
|
output.append(f"<li><a href='{url}'>{url}</a></li>")
|
||||||
|
output.append("</ul>")
|
||||||
|
return "\n".join(output)
|
||||||
|
|
||||||
|
@app.route("/robots.txt")
|
||||||
|
def robots_txt():
|
||||||
|
lines = [
|
||||||
|
"User-agent: *",
|
||||||
|
"Disallow: /adminpage",
|
||||||
|
"Disallow: /admin-login",
|
||||||
|
"Disallow: /admin-setup",
|
||||||
|
"Disallow: /admin-reset",
|
||||||
|
"Disallow: /admin-settings",
|
||||||
|
"Disallow: /restart-server",
|
||||||
|
"Disallow: /pickup",
|
||||||
|
"Disallow: /admin-change-password",
|
||||||
|
"Allow: /",
|
||||||
|
f"Sitemap: {url_for('sitemap', _external=True)}"
|
||||||
|
]
|
||||||
|
return "\n".join(lines), 200, {"Content-Type": "text/plain"}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# === Error Handlers ===
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def page_not_found(e):
|
||||||
|
return render_template('404.html'), 404
|
||||||
|
|
||||||
|
@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 # re-raise for debugging in development
|
||||||
|
|
||||||
|
|
||||||
|
# === Server Mode Execution ===
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Use Waitress to serve the app in production
|
PRODUCTION = os.getenv("PRODUCTION", "false").lower() == "true"
|
||||||
serve(app, host="0.0.0.0", port=5000)
|
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)
|
||||||
+6
-3
@@ -1,5 +1,8 @@
|
|||||||
### **requirements.txt**
|
### **requirements.txt**
|
||||||
|
|
||||||
Flask==2.1.2
|
flask==3.0.3
|
||||||
cryptography==3.4.8
|
cryptography==42.0.5
|
||||||
nginx==1.21.0 # Only needed for Nginx integration, not installed via pip
|
waitress==2.1.2
|
||||||
|
|
||||||
|
# nginx - Only needed for Nginx integration, not installed via pip
|
||||||
|
# Run pip install -r requirements.txt
|
||||||
@@ -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
|
||||||
Binary file not shown.
+217
-104
@@ -9,23 +9,25 @@
|
|||||||
body {
|
body {
|
||||||
font-family: 'Poppins', sans-serif;
|
font-family: 'Poppins', sans-serif;
|
||||||
background-color: #121212;
|
background-color: #121212;
|
||||||
color: #f0f0f0;
|
color: #00ff99;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
justify-content: center; /* Vertically center content */
|
justify-content: center;
|
||||||
align-items: center; /* Horizontally center content */
|
align-items: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Header ===== */
|
/* ===== Header ===== */
|
||||||
header {
|
header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 30px 20px;
|
padding: 25px;
|
||||||
background-color: #1c1c1c;
|
background-color: #1c1c1c;
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.5);
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 0 15px rgba(0, 255, 153, 0.4);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
header h1 {
|
header h1 {
|
||||||
@@ -35,8 +37,7 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
header p {
|
header p {
|
||||||
font-size: 1.1em;
|
font-size: 1.2em;
|
||||||
color: #00ff99;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Main Layout ===== */
|
/* ===== Main Layout ===== */
|
||||||
@@ -49,29 +50,28 @@ main {
|
|||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
gap: 30px;
|
gap: 30px;
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Section Card Styling ===== */
|
/* ===== Card Styling ===== */
|
||||||
.card {
|
.card {
|
||||||
background-color: #1e1e1e;
|
background-color: #1e1e1e;
|
||||||
padding: 20px 25px;
|
padding: 25px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 800px;
|
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 0 15px rgba(0, 255, 153, 0.4);
|
box-shadow: 0 0 15px rgba(0, 255, 153, 0.4);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Uniform Form Inputs ===== */
|
/* ===== Form Group Styling ===== */
|
||||||
.form-group {
|
.form-group {
|
||||||
display: flex;
|
display: flex !important;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 15px;
|
gap: 0px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Inputs, Textareas, Selects ===== */
|
||||||
input,
|
input,
|
||||||
textarea,
|
textarea,
|
||||||
select,
|
select,
|
||||||
@@ -84,7 +84,9 @@ input[type="file"] {
|
|||||||
background-color: #2c2f33;
|
background-color: #2c2f33;
|
||||||
color: #00ff99;
|
color: #00ff99;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
text-align: center;
|
||||||
transition: 0.3s;
|
transition: 0.3s;
|
||||||
|
margin:10px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
@@ -99,7 +101,6 @@ input[type="password"] {
|
|||||||
input[type="file"] {
|
input[type="file"] {
|
||||||
border: 2px dashed #00ff99;
|
border: 2px dashed #00ff99;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="file"]::file-selector-button {
|
input[type="file"]::file-selector-button {
|
||||||
@@ -116,6 +117,7 @@ input[type="file"] {
|
|||||||
background-color: #00cc77;
|
background-color: #00cc77;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Focus Effects ===== */
|
||||||
input:focus,
|
input:focus,
|
||||||
textarea:focus,
|
textarea:focus,
|
||||||
select:focus {
|
select:focus {
|
||||||
@@ -123,37 +125,36 @@ select:focus {
|
|||||||
box-shadow: 0 0 8px rgba(0, 255, 153, 0.8);
|
box-shadow: 0 0 8px rgba(0, 255, 153, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Match input and output textarea sizes ===== */
|
/* ===== Textareas Specific Widths ===== */
|
||||||
#input-text,
|
#input-text,
|
||||||
#output-text {
|
#output-text {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
height: 140px;
|
height: 140px;
|
||||||
box-sizing: border-box;
|
|
||||||
resize: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Buttons ===== */
|
/* ===== Button Group Styling ===== */
|
||||||
.button-group {
|
.button-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 15px;
|
||||||
margin-top: 8px;
|
margin: 10px auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
border: 2px solid #00ff99;
|
border: 0px solid #00ff99;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: #2c2f33;
|
background-color: #2c2f33;
|
||||||
color: #00ff99;
|
color: #00ff99;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: 0.3s;
|
transition: 0.3s;
|
||||||
width: 100%; /* Makes buttons stretch to fill container */
|
width: 100%;
|
||||||
max-width: 200px; /* Restricts button width */
|
max-width: 200px;
|
||||||
|
/* margin: 10px auto; */
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
@@ -161,79 +162,109 @@ button {
|
|||||||
color: #121212;
|
color: #121212;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Toggle Buttons ===== */
|
/* ===== Toggle Switch Styling ===== */
|
||||||
.radio-group {
|
.toggle-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 12px;
|
||||||
margin-top: 8px;
|
margin-top: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-button {
|
/* Make sure the switch aligns well */
|
||||||
display: inline-flex;
|
.switch {
|
||||||
align-items: center;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center; /* <-- Ensures vertical centering */
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 8px 18px;
|
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;
|
||||||
border: 2px solid #00ff99;
|
border: 2px solid #00ff99;
|
||||||
border-radius: 8px;
|
border-radius: 34px;
|
||||||
background-color: #2c2f33;
|
transition: .4s;
|
||||||
color: #00ff99;
|
display: flex;
|
||||||
cursor: pointer;
|
align-items: center;
|
||||||
transition: 0.3s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-button:hover {
|
/* The circle knob */
|
||||||
|
.slider::before {
|
||||||
|
content: "";
|
||||||
|
height: 26px;
|
||||||
|
width: 26px;
|
||||||
background-color: #00ff99;
|
background-color: #00ff99;
|
||||||
color: #121212;
|
border-radius: 50%;
|
||||||
|
transition: .4s;
|
||||||
|
transform: translateX(4px);
|
||||||
|
position: absolute;
|
||||||
|
left: 0px;
|
||||||
|
bottom: 2.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-button input {
|
input:checked + .slider::before {
|
||||||
display: none;
|
transform: translateX(36px);
|
||||||
}
|
|
||||||
|
|
||||||
.radio-button input:checked + span {
|
|
||||||
background-color: #00ff99;
|
|
||||||
color: #121212;
|
|
||||||
padding: 8px 18px;
|
|
||||||
border-radius: 8px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#remove-file-btn:hover {
|
/* Toggle Labels */
|
||||||
background-color: #ff5555;
|
.labels {
|
||||||
color: #2c2f33;
|
position: relative;
|
||||||
|
width: 100px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #00ff99;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels::before,
|
||||||
|
.labels::after {
|
||||||
|
content: attr(data-on);
|
||||||
|
width: 50%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels::after {
|
||||||
|
content: attr(data-off);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Toast Notifications ===== */
|
/* ===== Toast Notifications ===== */
|
||||||
.toast {
|
.toast {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
min-width: 250px;
|
width: 80%;
|
||||||
|
max-width: 500px;
|
||||||
|
min-height: 50px;
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
color: #00ff99;
|
color: #00ff99;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
padding: 10px;
|
padding: 14px;
|
||||||
margin-top: 8px;
|
margin: 10px auto 0 auto;
|
||||||
font-size: 0.9em;
|
font-size: 1em;
|
||||||
animation: fadein 0.5s, fadeout 0.5s 2.5s;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast.show {
|
.toast.show {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
animation: fadein 0.5s, fadeout 0.5s 2.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadein {
|
@keyframes fadein {
|
||||||
@@ -256,29 +287,17 @@ 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 ===== */
|
||||||
footer {
|
footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 18px;
|
padding: 25px;
|
||||||
background-color: #1c1c1c;
|
background-color: #1c1c1c;
|
||||||
color: #00ff99;
|
color: #00ff99;
|
||||||
margin-top: auto;
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 0 15px rgba(0, 255, 153, 0.4);
|
||||||
|
margin-top: 30px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer a {
|
footer a {
|
||||||
@@ -290,22 +309,116 @@ footer {
|
|||||||
color: #ff0066;
|
color: #ff0066;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Password Input Field ===== */
|
/* ===== Responsive Tweaks ===== */
|
||||||
#password-input {
|
@media (max-width: 600px) {
|
||||||
display: flex; /* Password input is visible by default */
|
input, textarea, select, #input-text, #output-text {
|
||||||
margin-top: 15px;
|
width: 100%;
|
||||||
flex-direction: column;
|
max-width: 90%;
|
||||||
gap: 10px;
|
}
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#password-input input {
|
/* ===== Copy Feedback Message ===== */
|
||||||
padding: 12px;
|
.copy-feedback {
|
||||||
font-size: 1em;
|
background-color: #2a2a2a;
|
||||||
border: 2px solid #00ff99;
|
border: 1px solid #00ff99;
|
||||||
border-radius: 8px;
|
padding: 6px 12px;
|
||||||
background-color: #2c2f33;
|
margin-top: 6px;
|
||||||
color: #00ff99;
|
border-radius: 6px;
|
||||||
width: 100%; /* Ensure the password field takes full width */
|
color: #00ff99;
|
||||||
|
font-size: 0.9em;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 300px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-feedback.show {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
form input,
|
||||||
|
form button {
|
||||||
|
width: 80%;
|
||||||
|
max-width: 500px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-feedback.show {
|
||||||
|
display: block;
|
||||||
|
width: fit-content;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
color: #00ff99;
|
||||||
|
border: 1px solid #00ff99;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#logContainer {
|
||||||
|
white-space: pre-wrap; /* Wrap long lines */
|
||||||
|
word-wrap: break-word; /* Break long words if needed */
|
||||||
|
overflow-wrap: anywhere; /* Ensures long strings don't overflow */
|
||||||
|
background: black;
|
||||||
|
color: lime;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pacmanCanvas {
|
||||||
|
background-color: black;
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
border: 2px solid #00ff99;
|
||||||
|
border-radius: 12px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
#pacman-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
border: 2px solid #00ff99;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pacman-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ===== Utility: Hidden Class ===== */
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,86 @@
|
|||||||
|
// encryption.js
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives an AES-GCM key from a password using PBKDF2.
|
||||||
|
* @param {string} password - User-supplied password.
|
||||||
|
* @param {Uint8Array} salt - Randomly generated salt.
|
||||||
|
* @returns {Promise<CryptoKey>}
|
||||||
|
*/
|
||||||
|
export async function deriveKey(password, salt) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const keyMaterial = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
encoder.encode(password),
|
||||||
|
{ name: 'PBKDF2' },
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
|
||||||
|
return crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: 'PBKDF2',
|
||||||
|
salt,
|
||||||
|
iterations: 200_000,
|
||||||
|
hash: 'SHA-256'
|
||||||
|
},
|
||||||
|
keyMaterial,
|
||||||
|
{ name: 'AES-GCM', length: 256 },
|
||||||
|
false,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts a message using AES-GCM with a derived key.
|
||||||
|
* @param {string} message - Plaintext message to encrypt.
|
||||||
|
* @param {string} password - User password for key derivation.
|
||||||
|
* @returns {Promise<string>} - Base64-encoded encrypted string.
|
||||||
|
*/
|
||||||
|
export async function encryptAdvanced(message, password) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||||
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||||
|
const key = await deriveKey(password, salt);
|
||||||
|
const encoded = encoder.encode(message);
|
||||||
|
|
||||||
|
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded);
|
||||||
|
|
||||||
|
const output = new Uint8Array(salt.length + iv.length + ciphertext.byteLength);
|
||||||
|
output.set(salt);
|
||||||
|
output.set(iv, salt.length);
|
||||||
|
output.set(new Uint8Array(ciphertext), salt.length + iv.length);
|
||||||
|
|
||||||
|
return btoa(String.fromCharCode(...output));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts an AES-GCM encrypted string.
|
||||||
|
* @param {string} encryptedData - Base64-encoded ciphertext.
|
||||||
|
* @param {string} password - Password used to derive the decryption key.
|
||||||
|
* @returns {Promise<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, 16);
|
||||||
|
const iv = encrypted.slice(16, 28);
|
||||||
|
const ciphertext = encrypted.slice(28);
|
||||||
|
const key = await deriveKey(password, salt);
|
||||||
|
|
||||||
|
const decrypted = await crypto.subtle.decrypt(
|
||||||
|
{ name: 'AES-GCM', iv },
|
||||||
|
key,
|
||||||
|
ciphertext
|
||||||
|
);
|
||||||
|
|
||||||
|
return new TextDecoder().decode(decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional init logging for module diagnostics.
|
||||||
|
*/
|
||||||
|
export function setupEncryption() {
|
||||||
|
console.log('[Encryption] Module loaded');
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
// fileops.js
|
||||||
|
|
||||||
|
import { encryptAdvanced, decryptAdvanced } from './encryption.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts the selected file and triggers download of the encrypted version.
|
||||||
|
* @param {HTMLInputElement} fileInput - The input element of type 'file'.
|
||||||
|
* @param {string} password - Password for encryption.
|
||||||
|
*/
|
||||||
|
export function encryptFile(fileInput, password) {
|
||||||
|
if (!fileInput.files.length) {
|
||||||
|
alert("Please select a file!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = async (e) => {
|
||||||
|
const rawBytes = new Uint8Array(e.target.result);
|
||||||
|
const base64 = btoa(String.fromCharCode(...rawBytes));
|
||||||
|
const encrypted = await encryptAdvanced(base64, password);
|
||||||
|
downloadFile(encrypted, file.name + ".enc");
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts the selected encrypted file and triggers download of the original.
|
||||||
|
* @param {HTMLInputElement} fileInput - The input element of type 'file'.
|
||||||
|
* @param {string} password - Password for decryption.
|
||||||
|
*/
|
||||||
|
export function decryptFile(fileInput, password) {
|
||||||
|
if (!fileInput.files.length) {
|
||||||
|
alert("Please select a file!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = async (e) => {
|
||||||
|
try {
|
||||||
|
const encryptedText = e.target.result;
|
||||||
|
const base64Decrypted = await decryptAdvanced(encryptedText, password);
|
||||||
|
const byteArray = new Uint8Array(
|
||||||
|
[...atob(base64Decrypted)].map(c => c.charCodeAt(0))
|
||||||
|
);
|
||||||
|
downloadFileBinary(byteArray, file.name.replace(/\.enc$/, ''));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[Decryption Error]", err);
|
||||||
|
alert("Decryption failed: wrong password or corrupted file.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a text-based file (encrypted string).
|
||||||
|
* @param {string} content - The file content to download.
|
||||||
|
* @param {string} filename - Desired name for the downloaded file.
|
||||||
|
*/
|
||||||
|
function downloadFile(content, filename) {
|
||||||
|
const blob = new Blob([content], { type: "application/octet-stream" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a binary file (Uint8Array).
|
||||||
|
* @param {Uint8Array} byteArray - The binary content.
|
||||||
|
* @param {string} filename - Desired name for the downloaded file.
|
||||||
|
*/
|
||||||
|
function downloadFileBinary(byteArray, filename) {
|
||||||
|
const blob = new Blob([byteArray], { type: "application/octet-stream" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// main.js
|
||||||
|
|
||||||
|
import { setupUI } from './ui.js';
|
||||||
|
import { setupGame } from './pacman.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize UI and game once the DOM is fully loaded.
|
||||||
|
*/
|
||||||
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
|
setupUI();
|
||||||
|
setupGame();
|
||||||
|
});
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
// pacman.js
|
||||||
|
|
||||||
|
export function setupGame() {
|
||||||
|
console.log('[PacMan] Game module loaded.');
|
||||||
|
|
||||||
|
window.startPacman = startPacman;
|
||||||
|
window.exitGame = exitGame;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== Game Constants & State ======
|
||||||
|
let canvas, ctx, pacman, enemy, walls, dots, score;
|
||||||
|
let pacmanSpeed = 40,
|
||||||
|
enemySpeed = 20,
|
||||||
|
cellSize = 40,
|
||||||
|
dotSize = 5,
|
||||||
|
cols, rows, randSeed, gameInterval;
|
||||||
|
|
||||||
|
// ====== Game Initialization ======
|
||||||
|
|
||||||
|
export function startPacman() {
|
||||||
|
canvas = document.getElementById("pacmanCanvas");
|
||||||
|
ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
cols = Math.floor(canvas.width / cellSize);
|
||||||
|
rows = Math.floor(canvas.height / cellSize);
|
||||||
|
walls = [];
|
||||||
|
dots = [];
|
||||||
|
score = 0;
|
||||||
|
|
||||||
|
clearInterval(gameInterval);
|
||||||
|
const seedSource = document.getElementById("password")?.value || "pacman";
|
||||||
|
randSeed = [...seedSource].reduce((s, c) => s + c.charCodeAt(0), 0);
|
||||||
|
|
||||||
|
|
||||||
|
generateWalls();
|
||||||
|
generateDots();
|
||||||
|
|
||||||
|
pacman = spawn();
|
||||||
|
do { enemy = spawn(); } while (enemy.x === pacman.x && enemy.y === pacman.y);
|
||||||
|
|
||||||
|
pacman.dx = pacman.dy = 0;
|
||||||
|
document.addEventListener("keydown", movePacman);
|
||||||
|
|
||||||
|
gameInterval = setInterval(gameLoop, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopPacman() {
|
||||||
|
clearInterval(gameInterval);
|
||||||
|
if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetGame() {
|
||||||
|
stopPacman();
|
||||||
|
startPacman();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exitGame() {
|
||||||
|
stopPacman();
|
||||||
|
document.getElementById("input-text").value = "";
|
||||||
|
document.getElementById("pacman-section").style.display = "none";
|
||||||
|
document.getElementById("encoding-section").style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== Game Setup Helpers ======
|
||||||
|
|
||||||
|
function spawn() {
|
||||||
|
const options = [];
|
||||||
|
for (let c = 1; c < cols - 1; c++) {
|
||||||
|
for (let r = 1; r < rows - 1; r++) {
|
||||||
|
if (!walls.some(w => w.c === c && w.r === r)) {
|
||||||
|
const neighbors = [
|
||||||
|
{ c: c + 1, r }, { c: c - 1, r },
|
||||||
|
{ c, r: r + 1 }, { c, r: r - 1 }
|
||||||
|
];
|
||||||
|
if (neighbors.some(n => !walls.some(w => w.c === n.c && w.r === n.r))) {
|
||||||
|
options.push({ c, r });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const s = options[Math.floor(rand() * options.length)];
|
||||||
|
return {
|
||||||
|
x: s.c * cellSize + cellSize / 2,
|
||||||
|
y: s.r * cellSize + cellSize / 2,
|
||||||
|
size: cellSize / 2 - 5,
|
||||||
|
dx: 0,
|
||||||
|
dy: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rand() {
|
||||||
|
const x = Math.sin(randSeed++) * 10000;
|
||||||
|
return x - Math.floor(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateWalls() {
|
||||||
|
for (let c = 0; c < cols; c++) {
|
||||||
|
for (let r = 0; r < rows; r++) {
|
||||||
|
if (c === 0 || r === 0 || c === cols - 1 || r === rows - 1 || rand() < 0.2) {
|
||||||
|
walls.push({ c, r });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDots() {
|
||||||
|
dots = [];
|
||||||
|
for (let c = 1; c < cols - 1; c++) {
|
||||||
|
for (let r = 1; r < rows - 1; r++) {
|
||||||
|
if (walls.some(w => w.c === c && w.r === r)) continue;
|
||||||
|
|
||||||
|
const isEnclosed =
|
||||||
|
walls.some(w => w.c === c + 1 && w.r === r) &&
|
||||||
|
walls.some(w => w.c === c - 1 && w.r === r) &&
|
||||||
|
walls.some(w => w.c === c && w.r === r + 1) &&
|
||||||
|
walls.some(w => w.c === c && w.r === r - 1);
|
||||||
|
|
||||||
|
if (!isEnclosed) dots.push({ c, r });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== Game Loop & Drawing ======
|
||||||
|
|
||||||
|
function gameLoop() {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
drawWalls();
|
||||||
|
moveChar(pacman);
|
||||||
|
moveEnemy();
|
||||||
|
drawChar(pacman, "yellow");
|
||||||
|
drawChar(enemy, "red");
|
||||||
|
eatDots();
|
||||||
|
drawScore();
|
||||||
|
checkGameOver();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawWalls() {
|
||||||
|
ctx.fillStyle = "blue";
|
||||||
|
walls.forEach(w => {
|
||||||
|
ctx.fillRect(w.c * cellSize, w.r * cellSize, cellSize, cellSize);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawChar(ch, color) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(ch.x, ch.y, ch.size, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawScore() {
|
||||||
|
ctx.fillStyle = "white";
|
||||||
|
ctx.font = "20px Poppins";
|
||||||
|
ctx.fillText("Score: " + score, 10, 25);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkGameOver() {
|
||||||
|
if (
|
||||||
|
Math.abs(pacman.x - enemy.x) < pacman.size &&
|
||||||
|
Math.abs(pacman.y - enemy.y) < pacman.size
|
||||||
|
) {
|
||||||
|
ctx.fillStyle = "#00ff99";
|
||||||
|
ctx.font = "40px Poppins";
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.fillText("Game Over!", canvas.width / 2, canvas.height / 2);
|
||||||
|
clearInterval(gameInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== Movement Logic ======
|
||||||
|
|
||||||
|
function movePacman(e) {
|
||||||
|
const k = e.key;
|
||||||
|
if (!["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(k)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (k === "ArrowUp") { pacman.dx = 0; pacman.dy = -pacmanSpeed; }
|
||||||
|
if (k === "ArrowDown") { pacman.dx = 0; pacman.dy = pacmanSpeed; }
|
||||||
|
if (k === "ArrowLeft") { pacman.dx = -pacmanSpeed; pacman.dy = 0; }
|
||||||
|
if (k === "ArrowRight") { pacman.dx = pacmanSpeed; pacman.dy = 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveChar(ch) {
|
||||||
|
const nx = ch.x + ch.dx;
|
||||||
|
const ny = ch.y + ch.dy;
|
||||||
|
if (!willCollide(nx, ny, ch.size)) {
|
||||||
|
ch.x = nx;
|
||||||
|
ch.y = ny;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveEnemy() {
|
||||||
|
const options = [];
|
||||||
|
const moves = [[enemySpeed, 0], [-enemySpeed, 0], [0, enemySpeed], [0, -enemySpeed]];
|
||||||
|
|
||||||
|
moves.forEach(([dx, dy]) => {
|
||||||
|
const nx = enemy.x + dx;
|
||||||
|
const ny = enemy.y + dy;
|
||||||
|
if (!willCollide(nx, ny, enemy.size)) options.push({ dx, dy });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!options.length) return;
|
||||||
|
|
||||||
|
let best = options[0];
|
||||||
|
let bestDist = dist(enemy.x + best.dx, enemy.y + best.dy, pacman.x, pacman.y);
|
||||||
|
|
||||||
|
for (const opt of options) {
|
||||||
|
const d = dist(enemy.x + opt.dx, enemy.y + opt.dy, pacman.x, pacman.y);
|
||||||
|
if (d < bestDist) {
|
||||||
|
best = opt;
|
||||||
|
bestDist = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enemy.x += best.dx;
|
||||||
|
enemy.y += best.dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dist(x1, y1, x2, y2) {
|
||||||
|
return Math.abs(x1 - x2) + Math.abs(y1 - y2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function willCollide(x, y, size) {
|
||||||
|
const left = x - size, right = x + size;
|
||||||
|
const top = y - size, bottom = y + size;
|
||||||
|
|
||||||
|
return walls.some(w => {
|
||||||
|
const wx1 = w.c * cellSize, wy1 = w.r * cellSize;
|
||||||
|
const wx2 = wx1 + cellSize, wy2 = wy1 + cellSize;
|
||||||
|
return right > wx1 && left < wx2 && bottom > wy1 && top < wy2;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function eatDots() {
|
||||||
|
const chompSound = document.getElementById("chomp-sound");
|
||||||
|
|
||||||
|
dots = dots.filter(d => {
|
||||||
|
const dx = d.c * cellSize + cellSize / 2;
|
||||||
|
const dy = d.r * cellSize + cellSize / 2;
|
||||||
|
|
||||||
|
if (Math.abs(pacman.x - dx) < pacman.size && Math.abs(pacman.y - dy) < pacman.size) {
|
||||||
|
score++;
|
||||||
|
if (chompSound) {
|
||||||
|
chompSound.currentTime = 0;
|
||||||
|
chompSound.volume = 0.4;
|
||||||
|
chompSound.play();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.fillStyle = "white";
|
||||||
|
dots.forEach(d => {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(d.c * cellSize + cellSize / 2, d.r * cellSize + cellSize / 2, dotSize, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.resetGame = resetGame;
|
||||||
|
window.exitGame = exitGame;
|
||||||
+85
-106
@@ -1,41 +1,31 @@
|
|||||||
// ===== AES Encryption =====
|
// ===== AES Advanced Encryption =====
|
||||||
async function encryptAdvanced(message, password) {
|
async function encryptAdvanced(message, password) {
|
||||||
// Create a random salt for key derivation
|
|
||||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||||
|
|
||||||
// Derive a key from the password using PBKDF2 and the salt
|
|
||||||
const key = await deriveKey(password, salt);
|
const key = await deriveKey(password, salt);
|
||||||
|
|
||||||
// Create a random initialization vector (IV)
|
|
||||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||||
|
|
||||||
// Encode the message as a Uint8Array
|
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const encodedMessage = encoder.encode(message);
|
const encodedMessage = encoder.encode(message);
|
||||||
|
|
||||||
// Encrypt the message using AES-GCM
|
|
||||||
const encryptedMessage = await crypto.subtle.encrypt(
|
const encryptedMessage = await crypto.subtle.encrypt(
|
||||||
{ name: 'AES-GCM', iv: iv },
|
{ name: 'AES-GCM', iv: iv },
|
||||||
key,
|
key,
|
||||||
encodedMessage
|
encodedMessage
|
||||||
);
|
);
|
||||||
|
|
||||||
// Combine salt, IV, and encrypted message
|
|
||||||
const encryptedArray = new Uint8Array(salt.length + iv.length + encryptedMessage.byteLength);
|
const encryptedArray = new Uint8Array(salt.length + iv.length + encryptedMessage.byteLength);
|
||||||
encryptedArray.set(salt);
|
encryptedArray.set(salt);
|
||||||
encryptedArray.set(iv, salt.length);
|
encryptedArray.set(iv, salt.length);
|
||||||
encryptedArray.set(new Uint8Array(encryptedMessage), salt.length + iv.length);
|
encryptedArray.set(new Uint8Array(encryptedMessage), salt.length + iv.length);
|
||||||
|
|
||||||
// Convert the result to base64 to send to the server
|
return btoa(String.fromCharCode(...encryptedArray));
|
||||||
return btoa(String.fromCharCode.apply(null, encryptedArray));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Derive a key from the password using PBKDF2
|
|
||||||
async function deriveKey(password, salt) {
|
async function deriveKey(password, salt) {
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const passwordBuffer = encoder.encode(password);
|
const passwordBuffer = encoder.encode(password);
|
||||||
|
|
||||||
const key = await crypto.subtle.importKey(
|
const keyMaterial = await crypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
passwordBuffer,
|
passwordBuffer,
|
||||||
{ name: 'PBKDF2' },
|
{ name: 'PBKDF2' },
|
||||||
@@ -47,37 +37,31 @@ async function deriveKey(password, salt) {
|
|||||||
{
|
{
|
||||||
name: 'PBKDF2',
|
name: 'PBKDF2',
|
||||||
salt: salt,
|
salt: salt,
|
||||||
iterations: 100000,
|
iterations: 200000,
|
||||||
hash: 'SHA-256',
|
hash: 'SHA-256',
|
||||||
},
|
},
|
||||||
key,
|
keyMaterial,
|
||||||
{ name: 'AES-GCM', length: 256 },
|
{ name: 'AES-GCM', length: 256 },
|
||||||
false,
|
false,
|
||||||
['encrypt', 'decrypt']
|
['encrypt', 'decrypt']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== AES Decryption =====
|
|
||||||
async function decryptAdvanced(encryptedData, password) {
|
async function decryptAdvanced(encryptedData, password) {
|
||||||
// Decode the base64-encoded encrypted data
|
const encryptedArray = new Uint8Array(atob(encryptedData).split("").map(c => c.charCodeAt(0)));
|
||||||
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 salt = encryptedArray.slice(0, 16);
|
||||||
const iv = encryptedArray.slice(16, 28);
|
const iv = encryptedArray.slice(16, 28);
|
||||||
const encryptedMessage = encryptedArray.slice(28);
|
const encryptedMessage = encryptedArray.slice(28);
|
||||||
|
|
||||||
// Derive the key from the password and salt
|
|
||||||
const key = await deriveKey(password, salt);
|
const key = await deriveKey(password, salt);
|
||||||
|
|
||||||
// Decrypt the message using AES-GCM
|
|
||||||
const decryptedMessage = await crypto.subtle.decrypt(
|
const decryptedMessage = await crypto.subtle.decrypt(
|
||||||
{ name: 'AES-GCM', iv: iv },
|
{ name: 'AES-GCM', iv: iv },
|
||||||
key,
|
key,
|
||||||
encryptedMessage
|
encryptedMessage
|
||||||
);
|
);
|
||||||
|
|
||||||
// Decode the decrypted message to text
|
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
return decoder.decode(decryptedMessage);
|
return decoder.decode(decryptedMessage);
|
||||||
}
|
}
|
||||||
@@ -85,81 +69,54 @@ async function decryptAdvanced(encryptedData, password) {
|
|||||||
// ===== UI Toggles =====
|
// ===== UI Toggles =====
|
||||||
function toggleEncryptionOptions() {
|
function toggleEncryptionOptions() {
|
||||||
const type = document.getElementById("encryption-type").value;
|
const type = document.getElementById("encryption-type").value;
|
||||||
const pwdContainer = document.getElementById("password-input");
|
document.getElementById("password-input").style.display = (type === 'advanced') ? 'flex' : 'none';
|
||||||
pwdContainer.style.display = (type === 'advanced') ? 'flex' : 'none';
|
|
||||||
if (type === 'basic') removeFile();
|
if (type === 'basic') removeFile();
|
||||||
|
|
||||||
toggleInputMode();
|
toggleInputMode();
|
||||||
document.getElementById("encrypt-label").textContent =
|
document.getElementById("encrypt-label").textContent = (type === 'basic') ? "Encode" : "Encrypt";
|
||||||
(type === 'basic') ? "Encode" : "Encrypt";
|
document.getElementById("decrypt-label").textContent = (type === 'basic') ? "Decode" : "Decrypt";
|
||||||
document.getElementById("decrypt-label").textContent =
|
|
||||||
(type === 'basic') ? "Decode" : "Decrypt";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Remove File Button =====
|
|
||||||
function removeFile() {
|
function removeFile() {
|
||||||
document.getElementById("file-input").value = ""; // Clear the file input
|
document.getElementById("file-input").value = "";
|
||||||
document.getElementById("remove-file-btn").style.display = 'none'; // Hide the remove file button
|
document.getElementById("remove-file-btn").style.display = 'none';
|
||||||
toggleInputMode(); // Reapply the input mode logic
|
toggleInputMode();
|
||||||
document.getElementById("file-password-input").style.display = 'none'; // Hide the file password input
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Input vs. File Toggle =====
|
|
||||||
function toggleInputMode() {
|
function toggleInputMode() {
|
||||||
const textValue = document.getElementById("input-text").value.trim();
|
const textValue = document.getElementById("input-text").value.trim();
|
||||||
const fileSelected = document.getElementById("file-input").files.length > 0;
|
const fileSelected = document.getElementById("file-input").files.length > 0;
|
||||||
const isAdvanced = document.getElementById("encryption-type").value === 'advanced';
|
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';
|
||||||
document.getElementById("text-section").style.display =
|
document.getElementById("file-section").style.display = (isAdvanced && !textValue) ? 'flex' : 'none';
|
||||||
fileSelected ? 'none' : 'flex';
|
document.getElementById("remove-file-btn").style.display = fileSelected ? 'inline-block' : 'none';
|
||||||
|
|
||||||
// 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) {
|
if (isAdvanced) {
|
||||||
document.getElementById("password-input").style.display = 'flex';
|
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 =====
|
// ===== Form Submission =====
|
||||||
async function handleSubmit(event) {
|
async function handleSubmit(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
// If the encryption type is advanced, ensure password is provided
|
|
||||||
const password = document.getElementById("password").value;
|
const password = document.getElementById("password").value;
|
||||||
const filePassword = document.getElementById("file-password") ? document.getElementById("file-password").value : '';
|
|
||||||
const encryptionType = document.getElementById("encryption-type").value;
|
const encryptionType = document.getElementById("encryption-type").value;
|
||||||
|
|
||||||
if (encryptionType === 'advanced' && !password && !filePassword) {
|
if (encryptionType === 'advanced' && !password) {
|
||||||
alert("Password is required for advanced encryption.");
|
alert("Password is required for advanced encryption.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare the form data
|
|
||||||
const payload = {
|
const payload = {
|
||||||
"encryption-type": encryptionType,
|
"encryption-type": encryptionType,
|
||||||
operation: document.querySelector('input[name="operation"]:checked').value,
|
operation: document.querySelector('input[name="operation"]:checked').value,
|
||||||
message: document.getElementById("input-text").value,
|
message: document.getElementById("input-text").value,
|
||||||
password: password,
|
password: password
|
||||||
"file-password": filePassword
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle file upload encryption/decryption
|
|
||||||
const fileInput = document.getElementById("file-input");
|
const fileInput = document.getElementById("file-input");
|
||||||
if (fileInput.files.length > 0) {
|
if (fileInput.files.length > 0) {
|
||||||
const op = document.querySelector('input[name="operation"]:checked').value;
|
const op = document.querySelector('input[name="operation"]:checked').value;
|
||||||
@@ -168,7 +125,6 @@ async function handleSubmit(event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle text encryption/decryption
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("/", {
|
const resp = await fetch("/", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -185,30 +141,35 @@ async function handleSubmit(event) {
|
|||||||
// ===== File Encryption / Decryption =====
|
// ===== File Encryption / Decryption =====
|
||||||
function encryptFile() {
|
function encryptFile() {
|
||||||
const f = document.getElementById("file-input");
|
const f = document.getElementById("file-input");
|
||||||
const pwd = document.getElementById("file-password").value;
|
const pwd = document.getElementById("password").value;
|
||||||
if (!pwd) return alert("Please enter a password!");
|
if (!pwd) return alert("Please enter a password!");
|
||||||
if (!f.files.length) return alert("Please select a file!");
|
if (!f.files.length) return alert("Please select a file!");
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = async (e) => {
|
reader.onload = async (e) => {
|
||||||
const raw = e.target.result;
|
const raw = new Uint8Array(e.target.result);
|
||||||
let encryptedMessage = await encryptAdvanced(raw, pwd);
|
const base64Raw = btoa(String.fromCharCode(...raw));
|
||||||
downloadFile(encryptedMessage, f.files[0].name + ".enc");
|
const encrypted = await encryptAdvanced(base64Raw, pwd);
|
||||||
|
downloadFile(encrypted, f.files[0].name + ".enc");
|
||||||
};
|
};
|
||||||
reader.readAsText(f.files[0]);
|
reader.readAsArrayBuffer(f.files[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function decryptFile() {
|
function decryptFile() {
|
||||||
const f = document.getElementById("file-input");
|
const f = document.getElementById("file-input");
|
||||||
const pwd = document.getElementById("file-password").value;
|
const pwd = document.getElementById("password").value;
|
||||||
if (!pwd) return alert("Please enter a password!");
|
if (!pwd) return alert("Please enter a password!");
|
||||||
if (!f.files.length) return alert("Please select a file!");
|
if (!f.files.length) return alert("Please select a file!");
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = async (e) => {
|
reader.onload = async (e) => {
|
||||||
try {
|
try {
|
||||||
const enc = e.target.result;
|
const encryptedText = e.target.result;
|
||||||
const decryptedMessage = await decryptAdvanced(enc, pwd);
|
const decryptedBase64 = await decryptAdvanced(encryptedText, pwd);
|
||||||
downloadFile(decryptedMessage, f.files[0].name.replace(/\.enc$/, ''));
|
const byteString = atob(decryptedBase64);
|
||||||
} catch {
|
const byteArray = new Uint8Array([...byteString].map(c => c.charCodeAt(0)));
|
||||||
|
downloadFileBinary(byteArray, f.files[0].name.replace(/\.enc$/, ''));
|
||||||
|
} catch (err) {
|
||||||
alert("Decryption failed: wrong password or corrupted file.");
|
alert("Decryption failed: wrong password or corrupted file.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -225,6 +186,16 @@ function downloadFile(content, filename) {
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function downloadFileBinary(byteArray, filename) {
|
||||||
|
const blob = new Blob([byteArray], { type: "application/octet-stream" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
// ===== Password Generator =====
|
// ===== Password Generator =====
|
||||||
function generateRandomPassword() {
|
function generateRandomPassword() {
|
||||||
const length = 30;
|
const length = 30;
|
||||||
@@ -240,13 +211,12 @@ function generateRandomPassword() {
|
|||||||
function copyToClipboard(elementId, toastId) {
|
function copyToClipboard(elementId, toastId) {
|
||||||
const copyText = document.getElementById(elementId);
|
const copyText = document.getElementById(elementId);
|
||||||
copyText.select();
|
copyText.select();
|
||||||
copyText.setSelectionRange(0, 99999); // For mobile devices
|
copyText.setSelectionRange(0, 99999);
|
||||||
document.execCommand("copy");
|
document.execCommand("copy");
|
||||||
|
|
||||||
// Show toast notification
|
|
||||||
const toast = document.getElementById(toastId);
|
const toast = document.getElementById(toastId);
|
||||||
toast.classList.add("show");
|
toast.classList.add("show");
|
||||||
setTimeout(() => toast.classList.remove("show"), 2000); // Remove toast after 2 seconds
|
setTimeout(() => toast.classList.remove("show"), 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Pacman Easter Egg =====
|
// ===== Pacman Easter Egg =====
|
||||||
@@ -277,6 +247,28 @@ function resetGame() {
|
|||||||
startPacman();
|
startPacman();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Clear All =====
|
||||||
|
function clearAll() {
|
||||||
|
document.getElementById("input-text").value = "";
|
||||||
|
document.getElementById("output-text").value = "";
|
||||||
|
document.getElementById("file-input").value = "";
|
||||||
|
document.getElementById("password").value = "";
|
||||||
|
|
||||||
|
document.getElementById("pacman-section").style.display = "none";
|
||||||
|
document.getElementById("encoding-section").style.display = "block";
|
||||||
|
|
||||||
|
removeFile();
|
||||||
|
toggleInputMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Initialize =====
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
toggleEncryptionOptions();
|
||||||
|
toggleInputMode();
|
||||||
|
document.getElementById("input-text").addEventListener("input", checkForPacman);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// ===== Pacman Game Variables & Logic =====
|
// ===== Pacman Game Variables & Logic =====
|
||||||
let canvas, ctx, pacman, enemy, walls, dots, score;
|
let canvas, ctx, pacman, enemy, walls, dots, score;
|
||||||
let pacmanSpeed = 40, enemySpeed = 20, cellSize = 40, dotSize = 5;
|
let pacmanSpeed = 40, enemySpeed = 20, cellSize = 40, dotSize = 5;
|
||||||
@@ -441,18 +433,27 @@ function drawChar(ch,color) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function eatDots() {
|
function eatDots() {
|
||||||
dots = dots.filter(d=>{
|
const chompSound = document.getElementById("chomp-sound");
|
||||||
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) {
|
dots = dots.filter(d => {
|
||||||
|
const dx = d.c * cellSize + cellSize / 2;
|
||||||
|
const dy = d.r * cellSize + cellSize / 2;
|
||||||
|
if (Math.abs(pacman.x - dx) < pacman.size && Math.abs(pacman.y - dy) < pacman.size) {
|
||||||
score++;
|
score++;
|
||||||
return false;
|
if (chompSound) {
|
||||||
|
chompSound.currentTime = 0; // Reset sound
|
||||||
|
chompSound.volume = 0.4;
|
||||||
|
chompSound.play();
|
||||||
|
}
|
||||||
|
return false; // Remove dot
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
ctx.fillStyle="white";
|
|
||||||
dots.forEach(d=>{
|
ctx.fillStyle = "white";
|
||||||
|
dots.forEach(d => {
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(d.c*cellSize+cellSize/2, d.r*cellSize+cellSize/2, dotSize,0,Math.PI*2);
|
ctx.arc(d.c * cellSize + cellSize / 2, d.r * cellSize + cellSize / 2, dotSize, 0, Math.PI * 2);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -472,25 +473,3 @@ function checkGameOver() {
|
|||||||
clearInterval(gameInterval);
|
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);
|
|
||||||
});
|
|
||||||
|
|||||||
+215
@@ -0,0 +1,215 @@
|
|||||||
|
// ui.js
|
||||||
|
import { encryptFile, decryptFile } from './fileops.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize all UI functionality after DOM is loaded
|
||||||
|
*/
|
||||||
|
export function setupUI() {
|
||||||
|
toggleEncryptionOptions();
|
||||||
|
toggleInputMode();
|
||||||
|
|
||||||
|
const encryptionTypeEl = document.getElementById("encryption-type");
|
||||||
|
const inputTextEl = document.getElementById("input-text");
|
||||||
|
const formEl = document.getElementById("crypto-form");
|
||||||
|
const removeFileBtn = document.getElementById("remove-file-btn");
|
||||||
|
const clearAllBtn = document.getElementById("clear-all-btn");
|
||||||
|
const generateBtn = document.getElementById("generate-btn");
|
||||||
|
const copyPasswordBtn = document.getElementById("copy-btn");
|
||||||
|
const copyOutputBtn = document.getElementById("copy-output-btn");
|
||||||
|
const toggleSwitch = document.getElementById("operation-toggle");
|
||||||
|
const copyShareBtn = document.getElementById("copy-share-btn");
|
||||||
|
const shareLink = document.getElementById("share-link");
|
||||||
|
|
||||||
|
if (
|
||||||
|
encryptionTypeEl && inputTextEl && formEl && removeFileBtn &&
|
||||||
|
clearAllBtn && generateBtn && copyPasswordBtn && toggleSwitch
|
||||||
|
) {
|
||||||
|
encryptionTypeEl.addEventListener("change", toggleEncryptionOptions);
|
||||||
|
inputTextEl.addEventListener("input", () => {
|
||||||
|
toggleInputMode();
|
||||||
|
checkForPacman();
|
||||||
|
});
|
||||||
|
formEl.addEventListener("submit", handleSubmit);
|
||||||
|
removeFileBtn.addEventListener("click", removeFile);
|
||||||
|
clearAllBtn.addEventListener("click", clearAll);
|
||||||
|
generateBtn.addEventListener("click", generateRandomPassword);
|
||||||
|
copyPasswordBtn.addEventListener("click", () => copyToClipboard("generated-password", "password-copy-feedback"));
|
||||||
|
copyOutputBtn?.addEventListener("click", () => copyToClipboard("output-text", "output-copy-feedback"));
|
||||||
|
toggleSwitch.addEventListener("change", updateToggleLabels);
|
||||||
|
|
||||||
|
const copySharedLinkBtn = document.getElementById("copy-shared-link");
|
||||||
|
const sharedLinkEl = document.getElementById("shared-link");
|
||||||
|
|
||||||
|
if (copySharedLinkBtn && sharedLinkEl) {
|
||||||
|
copySharedLinkBtn.addEventListener("click", () => {
|
||||||
|
navigator.clipboard.writeText(sharedLinkEl.textContent.trim()).then(() => {
|
||||||
|
const feedback = document.getElementById("shared-link-feedback");
|
||||||
|
if (feedback) {
|
||||||
|
feedback.classList.remove("hidden");
|
||||||
|
feedback.classList.add("show");
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
feedback.classList.remove("show");
|
||||||
|
feedback.classList.add("hidden");
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
sharedLinkEl.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function toggleEncryptionOptions() {
|
||||||
|
const type = document.getElementById("encryption-type").value.trim().toLowerCase();
|
||||||
|
const passwordInputWrapper = document.getElementById("password-input");
|
||||||
|
const isAdvanced = type.includes("advanced");
|
||||||
|
|
||||||
|
if (passwordInputWrapper) {
|
||||||
|
if (isAdvanced) {
|
||||||
|
passwordInputWrapper.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
passwordInputWrapper.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateToggleLabels();
|
||||||
|
toggleInputMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function updateToggleLabels() {
|
||||||
|
const type = document.getElementById("encryption-type")?.value;
|
||||||
|
const leftLabel = document.getElementById("toggle-left-label");
|
||||||
|
const rightLabel = document.getElementById("toggle-right-label");
|
||||||
|
|
||||||
|
if (!type || !leftLabel || !rightLabel) return;
|
||||||
|
|
||||||
|
const isAdvanced = type.toLowerCase().includes("advanced");
|
||||||
|
leftLabel.textContent = isAdvanced ? "Encrypt" : "Encode";
|
||||||
|
rightLabel.textContent = isAdvanced ? "Decrypt" : "Decode";
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleInputMode() {
|
||||||
|
const fileInput = document.getElementById("file-input");
|
||||||
|
const textValue = document.getElementById("input-text")?.value.trim();
|
||||||
|
const isAdvanced = document.getElementById("encryption-type")?.value === "advanced";
|
||||||
|
|
||||||
|
const textSection = document.getElementById("text-section");
|
||||||
|
const fileSection = document.getElementById("file-section");
|
||||||
|
const removeBtn = document.getElementById("remove-file-btn");
|
||||||
|
|
||||||
|
if (!fileInput || !textSection || !fileSection || !removeBtn) return;
|
||||||
|
|
||||||
|
const fileSelected = fileInput.files.length > 0;
|
||||||
|
|
||||||
|
textSection.style.display = fileSelected ? "none" : "flex";
|
||||||
|
fileSection.style.display = (isAdvanced && !textValue) ? "flex" : "none";
|
||||||
|
removeBtn.style.display = fileSelected ? "inline-block" : "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const encryptionType = document.getElementById("encryption-type")?.value;
|
||||||
|
const password = document.getElementById("password")?.value;
|
||||||
|
const fileInput = document.getElementById("file-input");
|
||||||
|
const isDecrypt = document.getElementById("operation-toggle").checked;
|
||||||
|
const operation = isDecrypt ? "decrypt" : "encrypt";
|
||||||
|
|
||||||
|
if (!encryptionType || !fileInput) return;
|
||||||
|
|
||||||
|
if (encryptionType === "advanced" && !password) {
|
||||||
|
return alert("Password is required for advanced encryption.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileInput.files.length > 0) {
|
||||||
|
return (operation === "encrypt")
|
||||||
|
? encryptFile(fileInput, password)
|
||||||
|
: decryptFile(fileInput, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
"encryption-type": encryptionType,
|
||||||
|
operation: operation,
|
||||||
|
message: document.getElementById("input-text")?.value,
|
||||||
|
password: password
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
document.getElementById("output-text").value = data.result;
|
||||||
|
} catch (err) {
|
||||||
|
alert("Error processing request: " + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFile() {
|
||||||
|
const fileInput = document.getElementById("file-input");
|
||||||
|
if (fileInput) fileInput.value = "";
|
||||||
|
const removeBtn = document.getElementById("remove-file-btn");
|
||||||
|
if (removeBtn) removeBtn.style.display = 'none';
|
||||||
|
toggleInputMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateRandomPassword() {
|
||||||
|
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+[]{}|;:,.<>?/~";
|
||||||
|
const length = 30;
|
||||||
|
const password = Array.from({ length }, () =>
|
||||||
|
charset.charAt(Math.floor(Math.random() * charset.length))
|
||||||
|
).join("");
|
||||||
|
const passwordField = document.getElementById("generated-password");
|
||||||
|
if (passwordField) passwordField.value = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToClipboard(elementId, feedbackId) {
|
||||||
|
const el = document.getElementById(elementId);
|
||||||
|
const feedback = document.getElementById(feedbackId);
|
||||||
|
if (!el || !feedback) return;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(el.textContent || el.value || "").then(() => {
|
||||||
|
feedback.classList.add("show");
|
||||||
|
setTimeout(() => {
|
||||||
|
feedback.classList.remove("show");
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAll() {
|
||||||
|
const fields = ["input-text", "output-text", "file-input", "password"];
|
||||||
|
fields.forEach(id => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.value = "";
|
||||||
|
});
|
||||||
|
removeFile();
|
||||||
|
toggleInputMode();
|
||||||
|
document.getElementById("pacman-section")?.style.setProperty("display", "none");
|
||||||
|
document.getElementById("encoding-section")?.style.setProperty("display", "block");
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkForPacman() {
|
||||||
|
const val = document.getElementById("input-text").value.trim().toLowerCase();
|
||||||
|
const pacSection = document.getElementById("pacman-section");
|
||||||
|
const encSection = document.getElementById("encoding-section");
|
||||||
|
|
||||||
|
if (val.includes("pacman") && pacSection.style.display !== "block") {
|
||||||
|
pacSection.style.display = "block";
|
||||||
|
encSection.style.display = "none";
|
||||||
|
window.startPacman();
|
||||||
|
} else if (pacSection.style.display === "block" && !val.includes("pacman")) {
|
||||||
|
window.exitGame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function startPacman() { }
|
||||||
|
function exitGame() { }
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>403 - PacCrypt</title>
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}" />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet" />
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
|
||||||
|
<script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||||
|
</head>
|
||||||
|
<body class="dark">
|
||||||
|
|
||||||
|
<header class="card mb-5">
|
||||||
|
<h1>PacCrypt</h1>
|
||||||
|
<p>Secure Encoding, Encryption and Password Generation</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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,45 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>404 - PacCrypt</title>
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}" />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet" />
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
|
||||||
|
<script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||||
|
</head>
|
||||||
|
<body class="dark">
|
||||||
|
|
||||||
|
<header class="card mb-5">
|
||||||
|
<h1>PacCrypt</h1>
|
||||||
|
<p>Secure Encoding, Encryption and Password Generation</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="card form-group" style="padding: 50px 30px;">
|
||||||
|
<h2 style="color: #ff0066; font-size: 2.5em;">❓ 404 - Not Found</h2>
|
||||||
|
<p class="mt-4" style="font-size: 1.2em; color: #cccccc;">
|
||||||
|
Whoops! That page doesn’t seem to exist. Maybe it got encrypted? 🧩🔐
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<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,46 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>500 - PacCrypt</title>
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}" />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet" />
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
|
||||||
|
<script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||||
|
</head>
|
||||||
|
<body class="dark">
|
||||||
|
|
||||||
|
<header class="card mb-5">
|
||||||
|
<h1>PacCrypt</h1>
|
||||||
|
<p>Secure Encoding, Encryption and Password Generation</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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,155 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Admin Panel - PacCrypt</title>
|
||||||
|
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||||
|
<script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||||
|
</head>
|
||||||
|
<body class="dark">
|
||||||
|
|
||||||
|
<header class="card mb-5">
|
||||||
|
<h1>PacCrypt Admin Panel</h1>
|
||||||
|
<p>Site Overview & Controls</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
|
||||||
|
<!-- Site Map -->
|
||||||
|
<section class="card form-group">
|
||||||
|
<h2>🔍 Site Map</h2>
|
||||||
|
<ul style="list-style: none; padding-left: 0;">
|
||||||
|
{% for route in routes %}
|
||||||
|
<li style="margin-bottom: 5px;">🔗 <code>{{ route }}</code></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="button-group mt-4">
|
||||||
|
<a href="{{ url_for('restart_server') }}">
|
||||||
|
<button type="button">🔁 Restart Server</button>
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('admin_logout') }}">
|
||||||
|
<button type="button">🚪 Log Out</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="{{ url_for('admin_reset') }}" method="POST" onsubmit="return confirm('Are you sure you want to reset admin credentials?');">
|
||||||
|
<button type="submit" class="mt-4" style="background-color: #b90000;">⚠️ Reset Admin</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form action="{{ url_for('admin_clear_uploads') }}" method="POST"
|
||||||
|
onsubmit="return confirm('Are you sure you want to delete ALL uploaded files?');">
|
||||||
|
<button type="submit">🗑 Clear All Uploaded Files</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true, category_filter=['clear-feedback']) %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="copy-feedback show">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Change Admin Password -->
|
||||||
|
<section class="card form-group mt-5">
|
||||||
|
<h2>🔑 Change Admin Password</h2>
|
||||||
|
|
||||||
|
{% 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 %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Update Server -->
|
||||||
|
<section class="card form-group mt-5">
|
||||||
|
<h2>📦 Update Server from GitHub</h2>
|
||||||
|
<form method="POST" action="{{ url_for('admin_update_server') }}">
|
||||||
|
<button type="submit" onclick="return confirm('Are you sure you want to pull the latest changes from GitHub?')">
|
||||||
|
🔁 Pull Latest Changes
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% with update_msgs = get_flashed_messages(category_filter=["update"]) %}
|
||||||
|
{% if update_msgs %}
|
||||||
|
<div class="copy-feedback show" style="margin-top: 12px;">
|
||||||
|
{{ update_msgs[0] | safe }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Server Status -->
|
||||||
|
<section class="card form-group mt-5">
|
||||||
|
<h2>📊 Server Status</h2>
|
||||||
|
<ul style="list-style: none; padding-left: 0;">
|
||||||
|
<li>🕒 Uptime: <code>{{ server_info.uptime }}</code></li>
|
||||||
|
<li>📅 Server Time: <code>{{ server_info.time }}</code></li>
|
||||||
|
<li>🐍 Python Version: <code>{{ server_info.python }}</code></li>
|
||||||
|
<li>⚙️ Flask Debug Mode: <code>{{ server_info.debug }}</code></li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Server Logs -->
|
||||||
|
<section class="card form-group mt-5">
|
||||||
|
<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; max-height: 400px; overflow-y: auto; background: black; color: lime; padding: 10px; border-radius: 8px;"></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- System Settings -->
|
||||||
|
<section class="card form-group mt-5">
|
||||||
|
<h2>🧩 System Configuration</h2>
|
||||||
|
<p>You can manage upload storage, limits, and expiration policies here:</p>
|
||||||
|
<a href="{{ url_for('admin_settings') }}">
|
||||||
|
<button type="button">🛠️ Manage Upload Settings</button>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Admin Login - PacCrypt</title>
|
||||||
|
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||||
|
<script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||||
|
</head>
|
||||||
|
<body class="dark">
|
||||||
|
|
||||||
|
<header class="card mb-5">
|
||||||
|
<h1>PacCrypt Admin</h1>
|
||||||
|
<p>Administrator Login</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="card form-group">
|
||||||
|
<h2>🔑 Admin Login</h2>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<p style="color: red;">{{ messages[0] }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<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,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Admin Settings - PacCrypt</title>
|
||||||
|
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||||
|
</head>
|
||||||
|
<body class="dark">
|
||||||
|
|
||||||
|
<header class="card mb-5">
|
||||||
|
<h1>PacCrypt Admin Settings</h1>
|
||||||
|
<p>Manage upload configuration securely</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="card form-group">
|
||||||
|
<h2>⚙️ Upload Settings</h2>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<ul style="color: lime;">
|
||||||
|
{% for message in messages %}
|
||||||
|
<li>{{ message }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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,49 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Admin Setup - PacCrypt</title>
|
||||||
|
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||||
|
<script type="module" src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||||
|
</head>
|
||||||
|
<body class="dark">
|
||||||
|
|
||||||
|
<header class="card mb-5">
|
||||||
|
<h1>PacCrypt Admin</h1>
|
||||||
|
<p>Secure Admin Setup</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="card form-group">
|
||||||
|
<h2>🛡️ Create Admin Account</h2>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<p style="color: red;">{{ messages[0] }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<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>
|
||||||
+128
-94
@@ -1,112 +1,146 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>PacCrypt</title>
|
<title>PacCrypt</title>
|
||||||
<!-- Favicon Link -->
|
<link rel="icon" href="{{ url_for('static', filename='img/PacCrypt.png') }}" type="image/png" />
|
||||||
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet" />
|
<script type="module" src="{{ url_for('static', filename='js/main.js') }}" defer></script>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
|
|
||||||
<script defer src="{{ url_for('static', filename='js/script.js') }}"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body class="dark">
|
<body class="dark">
|
||||||
<header>
|
|
||||||
<h1>PacCrypt</h1>
|
|
||||||
<p>Secure Encoding, Encryption and Password Generation</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
<header class="card mb-5">
|
||||||
<section id="password-section" class="card">
|
<h1>PacCrypt</h1>
|
||||||
<h2>Password Generator</h2>
|
<p>Encrypt and share your text or files securely</p>
|
||||||
<div class="form-group">
|
</header>
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="password-field"
|
|
||||||
placeholder="Generated password will appear here"
|
|
||||||
readonly
|
|
||||||
/>
|
|
||||||
<div class="button-group">
|
|
||||||
<button type="button" onclick="generateRandomPassword()">Generate</button>
|
|
||||||
<button type="button" onclick="copyToClipboard('password-field', 'password-toast')">Copy</button>
|
|
||||||
</div>
|
|
||||||
<div id="password-toast" class="toast">Copied to Clipboard!</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="pacman-section" class="card" style="display: none;">
|
<main>
|
||||||
<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">
|
|
||||||
<button type="button" onclick="resetGame()">Restart Game</button>
|
|
||||||
<button type="button" onclick="exitGame()">Exit Game</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="encoding-section" class="card">
|
<!-- Password Generator -->
|
||||||
<h2>Text Encoder / Decoder & File Encryption</h2>
|
<section class="card form-group mt-5">
|
||||||
<form id="main-form" class="form-group" method="POST" onsubmit="handleSubmit(event)">
|
<h2>🔑 Password Generator</h2>
|
||||||
<label for="encryption-type">Select Encryption Type:</label>
|
<div class="form-group">
|
||||||
<select id="encryption-type" name="encryption-type" onchange="toggleEncryptionOptions()">
|
<input type="text" id="generated-password" readonly />
|
||||||
<option value="basic">Basic (Less Secure)</option>
|
<div class="button-group">
|
||||||
<option value="advanced" selected>Advanced (More Secure)</option>
|
<button type="button" id="generate-btn">🎲 Generate</button>
|
||||||
</select>
|
<button type="button" id="copy-btn">📋 Copy</button>
|
||||||
|
</div>
|
||||||
|
<div id="password-copy-feedback" class="copy-feedback">Copied to clipboard!</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div id="encryption-options" class="radio-group">
|
<!-- Pacman Game Section -->
|
||||||
<label class="radio-button">
|
<section id="pacman-section" class="card" style="display: none;">
|
||||||
<input type="radio" name="operation" value="encrypt" id="encrypt-radio" checked />
|
<div class="pacman-wrapper">
|
||||||
<span id="encrypt-label">Encrypt</span>
|
<canvas id="pacmanCanvas" width="800" height="600"></canvas>
|
||||||
</label>
|
</div>
|
||||||
<label class="radio-button">
|
<audio id="chomp-sound" src="{{ url_for('static', filename='audio/chomp.mp3') }}"></audio>
|
||||||
<input type="radio" name="operation" value="decrypt" id="decrypt-radio" />
|
<div class="button-group">
|
||||||
<span id="decrypt-label">Decrypt</span>
|
<button type="button" onclick="resetGame()">Restart Game</button>
|
||||||
</label>
|
<button type="button" onclick="exitGame()">Exit Game</button>
|
||||||
</div>
|
</div>
|
||||||
|
</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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="file-section" class="form-group" style="display: none;">
|
<!-- Encrypt & Decrypt Section -->
|
||||||
<input type="file" id="file-input" onchange="toggleInputMode()" />
|
<section class="card form-group" id="encoding-section">
|
||||||
<button type="button" id="remove-file-btn" onclick="removeFile()">Remove File</button>
|
<h2>🔐 Encrypt & Decrypt</h2>
|
||||||
</div>
|
<form id="crypto-form" class="form-group">
|
||||||
|
|
||||||
<div id="file-password-input" style="display: none;">
|
<div class="form-group">
|
||||||
<input type="password" id="file-password" name="file-password" placeholder="Enter Password for File" />
|
<label for="encryption-type">Encryption Type:</label>
|
||||||
</div>
|
<select id="encryption-type">
|
||||||
|
<option value="basic">Basic Cipher</option>
|
||||||
|
<option value="advanced" selected>Advanced AES</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="button" class="submit-button" onclick="handleSubmit(event)">Submit</button>
|
<div class="toggle-container">
|
||||||
</form>
|
<span id="toggle-left-label">Encrypt</span>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" id="operation-toggle" />
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
<span id="toggle-right-label">Decrypt</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="height: 20px;"></div>
|
<div id="text-section" class="form-group">
|
||||||
|
<textarea id="input-text" placeholder="Enter your message..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
<textarea id="output-text" readonly placeholder="Result will appear here">{{ result }}</textarea>
|
<div id="password-input" class="form-group">
|
||||||
<div class="button-group">
|
<input type="password" id="password" placeholder="Password (AES only)" />
|
||||||
<button type="button" onclick="copyToClipboard('output-text', 'output-toast')">Copy Output</button>
|
</div>
|
||||||
<button type="button" onclick="clearAll()">Clear All</button>
|
|
||||||
</div>
|
<div id="file-section" class="form-group" style="display: none;">
|
||||||
<div id="output-toast" class="toast">Copied to Clipboard!</div>
|
<input type="file" id="file-input" />
|
||||||
</section>
|
<button type="button" id="remove-file-btn">🗑 Remove File</button>
|
||||||
</main>
|
</div>
|
||||||
|
|
||||||
|
<div class="button-group mt-3">
|
||||||
|
<button type="submit">⚡ Execute</button>
|
||||||
|
<button type="button" id="clear-all-btn">🧹 Clear All</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea id="output-text" readonly placeholder="Result will appear here..."></textarea>
|
||||||
|
<div class="button-group">
|
||||||
|
<button type="button" id="copy-output-btn">📋 Copy Output</button>
|
||||||
|
</div>
|
||||||
|
<div id="output-copy-feedback" class="copy-feedback">Copied to clipboard!</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- PacCrypt Sharing -->
|
||||||
|
<section class="card form-group mt-5">
|
||||||
|
<h2>📤 PacCrypt Sharing</h2>
|
||||||
|
<p>Securely share a file with encryption and a pickup password.</p>
|
||||||
|
|
||||||
|
{% 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 %}
|
||||||
|
<br />
|
||||||
|
<span id="shared-link">{{ message.split(" at ")[1] }}</span>
|
||||||
|
<button type="button" id="copy-shared-link">📋 Copy Link</button>
|
||||||
|
<div id="shared-link-feedback" class="copy-feedback">Copied to clipboard!</div>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<script>window.onload = () => window.scrollTo(0, document.body.scrollHeight);</script>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form method="POST" enctype="multipart/form-data" class="form-group">
|
||||||
|
<input type="file" name="file" id="upload-file" required />
|
||||||
|
<input type="password" name="enc_password" placeholder="Encryption Password" required />
|
||||||
|
<input type="password" name="pickup_password" placeholder="Pickup Password" required />
|
||||||
|
<div class="button-group mt-3">
|
||||||
|
<button type="submit">🔒 Upload and Generate Link</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<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="\static\img\Github_logo.png"
|
||||||
|
alt="GitHub Logo" width="100" />
|
||||||
|
</a>
|
||||||
|
</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" />
|
|
||||||
</a>
|
|
||||||
</footer>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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">
|
||||||
|
<title>Pickup File - PacCrypt</title>
|
||||||
|
<link rel="icon" type="image/png" href="{{ url_for('static', filename='img/PacCrypt.png') }}">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||||
|
</head>
|
||||||
|
<body class="dark">
|
||||||
|
|
||||||
|
<header class="card mb-5">
|
||||||
|
<h1>PacCrypt Pickup</h1>
|
||||||
|
<p>Enter passwords to retrieve your file securely</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="card form-group">
|
||||||
|
<h2>🔐 Decrypt and Download</h2>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<ul style="color: red;">
|
||||||
|
{% for message in messages %}
|
||||||
|
<li>{{ message }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
<input type="password" name="pickup_password" placeholder="Pickup Password" required>
|
||||||
|
<input type="password" name="enc_password" placeholder="Encryption Password" required>
|
||||||
|
<div class="button-group mt-3">
|
||||||
|
<button type="submit">📥 Decrypt and Download</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card form-group mt-5">
|
||||||
|
<p style="font-size: 0.9em; color: gray;">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>
|
||||||
Reference in New Issue
Block a user