Added all changes
parent
8c7bef73af
commit
3f98c368a3
@ -0,0 +1,326 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
TorGuard WireGuard VPN Manager
|
||||||
|
A secure web interface for managing WireGuard VPN configurations
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import secrets
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from flask import Flask, request, render_template, redirect, url_for, session, flash, jsonify
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
from flask_wtf.csrf import CSRFProtect
|
||||||
|
import bcrypt
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
import netifaces
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.secret_key = secrets.token_hex(32)
|
||||||
|
csrf = CSRFProtect(app)
|
||||||
|
|
||||||
|
CONF_DIR = Path('/etc/wireguard-manager')
|
||||||
|
WG_CONF_PATH = Path('/etc/wireguard/wg0.conf')
|
||||||
|
KEY_FILE = CONF_DIR / 'key.enc'
|
||||||
|
CREDS_FILE = CONF_DIR / 'credentials.enc'
|
||||||
|
|
||||||
|
def initialize_crypto():
|
||||||
|
"""Initialize encryption key for storing sensitive data"""
|
||||||
|
if not CONF_DIR.exists():
|
||||||
|
CONF_DIR.mkdir(mode=0o700)
|
||||||
|
|
||||||
|
if not KEY_FILE.exists():
|
||||||
|
key = Fernet.generate_key()
|
||||||
|
KEY_FILE.write_bytes(key)
|
||||||
|
KEY_FILE.chmod(0o600)
|
||||||
|
|
||||||
|
return Fernet(KEY_FILE.read_bytes())
|
||||||
|
|
||||||
|
crypto = initialize_crypto()
|
||||||
|
|
||||||
|
def requires_auth(f):
|
||||||
|
"""Decorator to require authentication for routes"""
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
if not CREDS_FILE.exists():
|
||||||
|
return redirect(url_for('register'))
|
||||||
|
if 'authenticated' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
def check_system_config():
|
||||||
|
"""Verify system configuration for WireGuard"""
|
||||||
|
issues = []
|
||||||
|
|
||||||
|
# Check IP forwarding
|
||||||
|
try:
|
||||||
|
with open('/proc/sys/net/ipv4/ip_forward', 'r') as f:
|
||||||
|
if f.read().strip() != '1':
|
||||||
|
issues.append("IP forwarding is not enabled")
|
||||||
|
except Exception as e:
|
||||||
|
issues.append(f"Failed to check IP forwarding: {str(e)}")
|
||||||
|
|
||||||
|
# Check NAT rules
|
||||||
|
try:
|
||||||
|
output = subprocess.check_output(['iptables', '-t', 'nat', '-L', 'POSTROUTING', '-n'], text=True)
|
||||||
|
if "MASQUERADE" not in output:
|
||||||
|
issues.append("NAT masquerade rule is missing")
|
||||||
|
except Exception as e:
|
||||||
|
issues.append(f"Failed to check NAT rules: {str(e)}")
|
||||||
|
|
||||||
|
# Check WireGuard module
|
||||||
|
try:
|
||||||
|
subprocess.check_output(['lsmod'], text=True)
|
||||||
|
if 'wireguard' not in subprocess.check_output(['lsmod'], text=True):
|
||||||
|
issues.append("WireGuard kernel module is not loaded")
|
||||||
|
except Exception as e:
|
||||||
|
issues.append(f"Failed to check WireGuard module: {str(e)}")
|
||||||
|
|
||||||
|
return issues
|
||||||
|
|
||||||
|
def get_wg_status():
|
||||||
|
"""Get WireGuard interface status and statistics"""
|
||||||
|
try:
|
||||||
|
# Check system configuration
|
||||||
|
system_issues = check_system_config()
|
||||||
|
if system_issues:
|
||||||
|
return {
|
||||||
|
'status': 'error',
|
||||||
|
'issues': system_issues
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check WireGuard interface
|
||||||
|
if not WG_CONF_PATH.exists():
|
||||||
|
return {'status': 'disconnected', 'message': 'No configuration file found'}
|
||||||
|
|
||||||
|
try:
|
||||||
|
output = subprocess.check_output(['wg', 'show', 'wg0'], text=True)
|
||||||
|
if 'peer' not in output:
|
||||||
|
return {'status': 'disconnected'}
|
||||||
|
|
||||||
|
# Parse WireGuard stats
|
||||||
|
stats = {'status': 'connected'}
|
||||||
|
for line in output.split('\n'):
|
||||||
|
if 'transfer:' in line:
|
||||||
|
tx, rx = line.split('transfer:')[1].split(',')
|
||||||
|
stats['tx'] = tx.strip()
|
||||||
|
stats['rx'] = rx.strip()
|
||||||
|
elif 'latest handshake:' in line:
|
||||||
|
time_str = line.split('latest handshake:')[1].strip()
|
||||||
|
stats['connected_since'] = time_str
|
||||||
|
|
||||||
|
# Add interface information
|
||||||
|
try:
|
||||||
|
interface_stats = psutil.net_io_counters(pernic=True).get('wg0', None)
|
||||||
|
if interface_stats:
|
||||||
|
stats['total_tx'] = f"{interface_stats.bytes_sent / (1024*1024):.2f} MB"
|
||||||
|
stats['total_rx'] = f"{interface_stats.bytes_recv / (1024*1024):.2f} MB"
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return stats
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return {'status': 'disconnected'}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
'status': 'error',
|
||||||
|
'error': str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate_wireguard_config(config):
|
||||||
|
"""Validate WireGuard configuration format"""
|
||||||
|
required_fields = {
|
||||||
|
'Interface': ['PrivateKey', 'Address'],
|
||||||
|
'Peer': ['PublicKey', 'AllowedIPs', 'Endpoint']
|
||||||
|
}
|
||||||
|
|
||||||
|
current_section = None
|
||||||
|
found_fields = {'Interface': set(), 'Peer': set()}
|
||||||
|
|
||||||
|
for line in config.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith('#'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.startswith('[') and line.endswith(']'):
|
||||||
|
current_section = line[1:-1]
|
||||||
|
continue
|
||||||
|
|
||||||
|
if current_section and '=' in line:
|
||||||
|
key = line.split('=')[0].strip()
|
||||||
|
if current_section in found_fields and key in required_fields[current_section]:
|
||||||
|
found_fields[current_section].add(key)
|
||||||
|
|
||||||
|
# Check if all required fields are present
|
||||||
|
missing_fields = []
|
||||||
|
for section, fields in required_fields.items():
|
||||||
|
for field in fields:
|
||||||
|
if field not in found_fields[section]:
|
||||||
|
missing_fields.append(f"{section}/{field}")
|
||||||
|
|
||||||
|
return len(missing_fields) == 0, missing_fields
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
if not CREDS_FILE.exists():
|
||||||
|
return redirect(url_for('register'))
|
||||||
|
if 'authenticated' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
status = get_wg_status()
|
||||||
|
return render_template('index.html', status=status, WG_CONF_PATH=WG_CONF_PATH)
|
||||||
|
|
||||||
|
@app.route('/status')
|
||||||
|
@requires_auth
|
||||||
|
def get_status_route():
|
||||||
|
"""API endpoint for getting VPN status"""
|
||||||
|
return jsonify(get_wg_status())
|
||||||
|
|
||||||
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
if not CREDS_FILE.exists():
|
||||||
|
return redirect(url_for('register'))
|
||||||
|
|
||||||
|
if 'authenticated' in session:
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
username = request.form['username']
|
||||||
|
password = request.form['password']
|
||||||
|
|
||||||
|
try:
|
||||||
|
stored_creds = json.loads(crypto.decrypt(CREDS_FILE.read_bytes()))
|
||||||
|
if username == stored_creds['username'] and \
|
||||||
|
check_password_hash(stored_creds['password'], password):
|
||||||
|
session['authenticated'] = True
|
||||||
|
session['username'] = username
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"Login error: {str(e)}")
|
||||||
|
flash('Error accessing credentials')
|
||||||
|
return render_template('login.html')
|
||||||
|
|
||||||
|
flash('Invalid credentials')
|
||||||
|
return render_template('login.html')
|
||||||
|
|
||||||
|
@app.route('/register', methods=['GET', 'POST'])
|
||||||
|
def register():
|
||||||
|
if CREDS_FILE.exists():
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
username = request.form['username']
|
||||||
|
password = request.form['password']
|
||||||
|
|
||||||
|
# Validate input
|
||||||
|
if not username or not password:
|
||||||
|
flash('Username and password are required')
|
||||||
|
return render_template('register.html')
|
||||||
|
|
||||||
|
if len(password) < 8:
|
||||||
|
flash('Password must be at least 8 characters long')
|
||||||
|
return render_template('register.html')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Store encrypted credentials
|
||||||
|
creds = {
|
||||||
|
'username': username,
|
||||||
|
'password': generate_password_hash(password)
|
||||||
|
}
|
||||||
|
CREDS_FILE.write_bytes(crypto.encrypt(json.dumps(creds).encode()))
|
||||||
|
CREDS_FILE.chmod(0o600)
|
||||||
|
flash('Account created successfully. Please login.')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"Registration error: {str(e)}")
|
||||||
|
flash('Error creating account')
|
||||||
|
|
||||||
|
return render_template('register.html')
|
||||||
|
|
||||||
|
@app.route('/logout')
|
||||||
|
def logout():
|
||||||
|
session.clear()
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
@app.route('/config', methods=['POST'])
|
||||||
|
@requires_auth
|
||||||
|
def save_config():
|
||||||
|
try:
|
||||||
|
if 'config_file' in request.files:
|
||||||
|
config = request.files['config_file'].read().decode()
|
||||||
|
else:
|
||||||
|
config = request.form['config_text']
|
||||||
|
|
||||||
|
# Validate WireGuard config format
|
||||||
|
is_valid, missing_fields = validate_wireguard_config(config)
|
||||||
|
if not is_valid:
|
||||||
|
flash(f'Invalid WireGuard configuration. Missing fields: {", ".join(missing_fields)}')
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
# Save config securely
|
||||||
|
WG_CONF_PATH.write_text(config)
|
||||||
|
WG_CONF_PATH.chmod(0o600)
|
||||||
|
|
||||||
|
flash('Configuration saved successfully')
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"Config save error: {str(e)}")
|
||||||
|
flash('Error saving configuration')
|
||||||
|
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@app.route('/toggle', methods=['POST'])
|
||||||
|
@requires_auth
|
||||||
|
def toggle_vpn():
|
||||||
|
status = get_wg_status()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if status['status'] == 'connected':
|
||||||
|
subprocess.run(['wg-quick', 'down', 'wg0'], check=True)
|
||||||
|
flash('VPN disconnected successfully')
|
||||||
|
else:
|
||||||
|
# Check system configuration before connecting
|
||||||
|
issues = check_system_config()
|
||||||
|
if issues:
|
||||||
|
flash(f'System configuration issues found: {", ".join(issues)}')
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
subprocess.run(['wg-quick', 'up', 'wg0'], check=True)
|
||||||
|
flash('VPN connected successfully')
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
app.logger.error(f"VPN toggle error: {e.stderr.decode() if e.stderr else str(e)}")
|
||||||
|
flash(f'Error toggling VPN: {e.stderr.decode() if e.stderr else str(e)}')
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"VPN toggle error: {str(e)}")
|
||||||
|
flash(f'Error toggling VPN: {str(e)}')
|
||||||
|
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point"""
|
||||||
|
# Check if running as root
|
||||||
|
if os.geteuid() != 0:
|
||||||
|
print("This program must be run as root")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Initialize directories
|
||||||
|
CONF_DIR.mkdir(mode=0o700, exist_ok=True)
|
||||||
|
|
||||||
|
# Check system configuration
|
||||||
|
issues = check_system_config()
|
||||||
|
if issues:
|
||||||
|
print("Warning: System configuration issues found:")
|
||||||
|
for issue in issues:
|
||||||
|
print(f" - {issue}")
|
||||||
|
|
||||||
|
# Start Flask server
|
||||||
|
app.run(host='0.0.0.0', port=1337)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
@ -0,0 +1,243 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
<meta name="referrer" content="no-referrer">
|
||||||
|
|
||||||
|
<!-- Security headers -->
|
||||||
|
<meta http-equiv="Content-Security-Policy"
|
||||||
|
content="default-src 'self';
|
||||||
|
img-src 'self' data:;
|
||||||
|
style-src 'self' 'unsafe-inline';
|
||||||
|
script-src 'self' 'unsafe-inline';
|
||||||
|
font-src 'self' data:;
|
||||||
|
connect-src 'self';">
|
||||||
|
|
||||||
|
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
||||||
|
<meta http-equiv="X-Frame-Options" content="DENY">
|
||||||
|
|
||||||
|
<title>TorGuard WireGuard Manager</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS and Icons (Local) -->
|
||||||
|
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
|
||||||
|
<link href="{{ url_for('static', filename='css/bootstrap-icons.css') }}" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom Styles -->
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: #0d6efd;
|
||||||
|
--danger-color: #dc3545;
|
||||||
|
--success-color: #198754;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
max-width: 300px;
|
||||||
|
margin: 2rem 0;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 0 20px rgba(0,0,0,0.1);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background-color: #cff4fc;
|
||||||
|
border-color: #b6effb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
border-color: #f5c2c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background-color: #d1e7dd;
|
||||||
|
border-color: #badbcc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading spinner */
|
||||||
|
.spinner-overlay {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 9999;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-container {
|
||||||
|
background: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.form-container {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
max-width: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.form-container {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.footer {
|
||||||
|
margin-top: auto;
|
||||||
|
padding: 1rem 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- Loading Spinner -->
|
||||||
|
<div class="spinner-overlay" id="loadingSpinner">
|
||||||
|
<div class="spinner-container">
|
||||||
|
<div class="spinner-border text-primary mb-2" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<div>Please wait...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container py-4">
|
||||||
|
<!-- Header Section -->
|
||||||
|
{% if session.authenticated %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<span class="text-muted">Welcome, {{ session.username }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="{{ url_for('logout') }}" class="btn btn-outline-danger">
|
||||||
|
<i class="bi bi-box-arrow-right"></i> Logout
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Logo Section -->
|
||||||
|
<div class="text-center">
|
||||||
|
<img src="{{ url_for('static', filename='logo.png') }}"
|
||||||
|
alt="TorGuard Logo"
|
||||||
|
class="logo"
|
||||||
|
onerror="this.onerror=null; this.src='data:image/svg+xml,<svg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 100 40\'><rect width=\'100\' height=\'40\' fill=\'%23f8f9fa\'/><text x=\'50\' y=\'20\' text-anchor=\'middle\' alignment-baseline=\'middle\' font-family=\'Arial\' font-size=\'16\'>TorGuard</text></svg>';">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Flash Messages -->
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category if category != 'message' else 'info' }} alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<p class="mb-0">TorGuard WireGuard Manager</p>
|
||||||
|
{% if session.authenticated %}
|
||||||
|
<small>Connected to: {{ request.host }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- JavaScript Dependencies (Local) -->
|
||||||
|
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
|
||||||
|
|
||||||
|
<!-- Common JavaScript -->
|
||||||
|
<script>
|
||||||
|
// Show loading spinner on form submissions
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const forms = document.querySelectorAll('form');
|
||||||
|
const spinner = document.getElementById('loadingSpinner');
|
||||||
|
|
||||||
|
forms.forEach(form => {
|
||||||
|
form.addEventListener('submit', function() {
|
||||||
|
if (this.checkValidity()) {
|
||||||
|
spinner.style.display = 'flex';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide alerts after 5 seconds
|
||||||
|
const alerts = document.querySelectorAll('.alert');
|
||||||
|
alerts.forEach(alert => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const bsAlert = new bootstrap.Alert(alert);
|
||||||
|
bsAlert.close();
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent form resubmission on refresh
|
||||||
|
if (window.history.replaceState) {
|
||||||
|
window.history.replaceState(null, null, window.location.href);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,264 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="form-container">
|
||||||
|
<!-- Status Display -->
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<div class="status-container">
|
||||||
|
<span class="badge status-badge {% if status.status == 'connected' %}bg-success{% elif status.status == 'error' %}bg-warning{% else %}bg-danger{% endif %}">
|
||||||
|
{{ status.status|title }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{% if status.status == 'connected' %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<div class="text-success mb-2">
|
||||||
|
<i class="bi bi-shield-check me-1"></i>VPN Connection Active
|
||||||
|
</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
<small>
|
||||||
|
<i class="bi bi-clock me-1"></i>Connected since: {{ status.connected_since }}<br>
|
||||||
|
<i class="bi bi-arrow-up me-1"></i>Upload: {{ status.tx }}
|
||||||
|
<i class="bi bi-arrow-down ms-2 me-1"></i>Download: {{ status.rx }}<br>
|
||||||
|
{% if status.total_tx and status.total_rx %}
|
||||||
|
<i class="bi bi-graph-up me-1"></i>Total Transfer: ↑{{ status.total_tx }} ↓{{ status.total_rx }}
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% elif status.status == 'error' %}
|
||||||
|
<div class="alert alert-warning mt-3">
|
||||||
|
<i class="bi bi-exclamation-triangle me-2"></i>System Issues Detected:
|
||||||
|
<ul class="mb-0 mt-2 text-start">
|
||||||
|
{% for issue in status.issues %}
|
||||||
|
<li>{{ issue }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-muted mt-2">
|
||||||
|
<small>
|
||||||
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
|
{% if status.message %}
|
||||||
|
{{ status.message }}
|
||||||
|
{% else %}
|
||||||
|
VPN is currently disconnected
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Connection Toggle -->
|
||||||
|
<form method="POST" action="{{ url_for('toggle_vpn') }}" class="mb-4" id="vpnToggleForm">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit"
|
||||||
|
class="btn {% if status.status == 'connected' %}btn-danger{% else %}btn-success{% endif %} btn-lg"
|
||||||
|
{% if status.status == 'error' or not WG_CONF_PATH.exists() %}disabled{% endif %}>
|
||||||
|
{% if status.status == 'connected' %}
|
||||||
|
<i class="bi bi-power"></i> Disconnect VPN
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-power"></i> Connect VPN
|
||||||
|
{% endif %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% if not WG_CONF_PATH.exists() %}
|
||||||
|
<div class="text-center mt-2">
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="bi bi-info-circle me-1"></i>Please add a WireGuard configuration first
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Configuration Section -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header bg-primary text-white d-flex align-items-center">
|
||||||
|
<i class="bi bi-gear-fill me-2"></i>WireGuard Configuration
|
||||||
|
{% if WG_CONF_PATH.exists() %}
|
||||||
|
<span class="badge bg-light text-primary ms-auto">
|
||||||
|
<i class="bi bi-check-circle me-1"></i>Config Present
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="{{ url_for('save_config') }}" enctype="multipart/form-data" id="configForm">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="config_text" class="form-label">
|
||||||
|
<i class="bi bi-file-text me-2"></i>Paste Configuration
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
class="form-control font-monospace"
|
||||||
|
id="config_text"
|
||||||
|
name="config_text"
|
||||||
|
rows="10"
|
||||||
|
placeholder="[Interface] PrivateKey = ... Address = ... [Peer] PublicKey = ... AllowedIPs = ... Endpoint = ..."
|
||||||
|
spellcheck="false"
|
||||||
|
></textarea>
|
||||||
|
<div class="form-text">
|
||||||
|
<i class="bi bi-info-circle me-1"></i>Paste your TorGuard WireGuard configuration here
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="config_file" class="form-label">
|
||||||
|
<i class="bi bi-upload me-2"></i>Or Upload Configuration File
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
class="form-control"
|
||||||
|
id="config_file"
|
||||||
|
name="config_file"
|
||||||
|
accept=".conf,.txt"
|
||||||
|
>
|
||||||
|
<div class="form-text">
|
||||||
|
<i class="bi bi-shield-lock me-1"></i>File will be stored securely with restricted permissions
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary" id="saveConfigBtn">
|
||||||
|
<i class="bi bi-save me-2"></i>Save Configuration
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Links -->
|
||||||
|
<div class="text-center">
|
||||||
|
<a href="https://torguard.net" target="_blank" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-box-arrow-up-right me-2"></i>Login to TorGuard Portal
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Version Info -->
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="bi bi-info-circle me-1"></i>TorGuard WireGuard Manager v1.0
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status Update Script -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Function to update status via API
|
||||||
|
function updateStatus() {
|
||||||
|
fetch('{{ url_for("get_status_route") }}')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
const statusContainer = document.querySelector('.status-container');
|
||||||
|
const statusBadge = statusContainer.querySelector('.status-badge');
|
||||||
|
|
||||||
|
// Update badge class
|
||||||
|
statusBadge.className = 'badge status-badge';
|
||||||
|
if (data.status === 'connected') {
|
||||||
|
statusBadge.classList.add('bg-success');
|
||||||
|
} else if (data.status === 'error') {
|
||||||
|
statusBadge.classList.add('bg-warning');
|
||||||
|
} else {
|
||||||
|
statusBadge.classList.add('bg-danger');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update badge text
|
||||||
|
statusBadge.textContent = data.status.charAt(0).toUpperCase() + data.status.slice(1);
|
||||||
|
|
||||||
|
// Update status details
|
||||||
|
const details = document.createElement('div');
|
||||||
|
if (data.status === 'connected') {
|
||||||
|
details.innerHTML = `
|
||||||
|
<div class="mt-2">
|
||||||
|
<div class="text-success mb-2">
|
||||||
|
<i class="bi bi-shield-check me-1"></i>VPN Connection Active
|
||||||
|
</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
<small>
|
||||||
|
<i class="bi bi-clock me-1"></i>Connected since: ${data.connected_since}<br>
|
||||||
|
<i class="bi bi-arrow-up me-1"></i>Upload: ${data.tx}
|
||||||
|
<i class="bi bi-arrow-down ms-2 me-1"></i>Download: ${data.rx}<br>
|
||||||
|
${data.total_tx && data.total_rx ? `<i class="bi bi-graph-up me-1"></i>Total Transfer: ↑${data.total_tx} ↓${data.total_rx}` : ''}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
} else if (data.status === 'error') {
|
||||||
|
const issues = data.issues.map(issue => `<li>${issue}</li>`).join('');
|
||||||
|
details.innerHTML = `
|
||||||
|
<div class="alert alert-warning mt-3">
|
||||||
|
<i class="bi bi-exclamation-triangle me-2"></i>System Issues Detected:
|
||||||
|
<ul class="mb-0 mt-2 text-start">${issues}</ul>
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
details.innerHTML = `
|
||||||
|
<div class="text-muted mt-2">
|
||||||
|
<small>
|
||||||
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
|
${data.message || 'VPN is currently disconnected'}
|
||||||
|
</small>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace existing content
|
||||||
|
while (statusContainer.childNodes.length > 1) {
|
||||||
|
statusContainer.removeChild(statusContainer.lastChild);
|
||||||
|
}
|
||||||
|
statusContainer.appendChild(details);
|
||||||
|
|
||||||
|
// Update toggle button
|
||||||
|
const toggleBtn = document.querySelector('#vpnToggleForm button');
|
||||||
|
toggleBtn.className = `btn ${data.status === 'connected' ? 'btn-danger' : 'btn-success'} btn-lg`;
|
||||||
|
toggleBtn.disabled = data.status === 'error' || !data.config_exists;
|
||||||
|
toggleBtn.innerHTML = `
|
||||||
|
<i class="bi bi-power"></i> ${data.status === 'connected' ? 'Disconnect' : 'Connect'} VPN`;
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error updating status:', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status every 30 seconds if page is visible
|
||||||
|
setInterval(() => {
|
||||||
|
if (!document.hidden) {
|
||||||
|
updateStatus();
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
// Update when page becomes visible
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (!document.hidden) {
|
||||||
|
updateStatus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// File upload handling
|
||||||
|
const configFile = document.getElementById('config_file');
|
||||||
|
const configText = document.getElementById('config_text');
|
||||||
|
|
||||||
|
configFile.addEventListener('change', function() {
|
||||||
|
if (this.files.length > 0) {
|
||||||
|
const file = this.files[0];
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = function(e) {
|
||||||
|
configText.value = e.target.result;
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Form submission loading state
|
||||||
|
document.querySelectorAll('form').forEach(form => {
|
||||||
|
form.addEventListener('submit', function() {
|
||||||
|
const btn = this.querySelector('button[type="submit"]');
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = `
|
||||||
|
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||||
|
${btn.textContent}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,167 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-container">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<h2 class="h3 mb-3">Welcome Back</h2>
|
||||||
|
<p class="text-muted">Please login to manage your VPN connection</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" class="needs-validation" novalidate autocomplete="off">
|
||||||
|
<!-- CSRF Protection -->
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
|
||||||
|
<!-- Username Field -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">
|
||||||
|
<i class="bi bi-person-fill me-2"></i>Username
|
||||||
|
</label>
|
||||||
|
<div class="input-group has-validation">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<i class="bi bi-person"></i>
|
||||||
|
</span>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
required
|
||||||
|
autofocus
|
||||||
|
autocomplete="username"
|
||||||
|
pattern="[a-zA-Z0-9_-]{3,20}"
|
||||||
|
title="Username must be 3-20 characters, using only letters, numbers, underscore, or hyphen"
|
||||||
|
maxlength="20">
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Please enter a valid username (3-20 characters, letters, numbers, _ or -)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password Field -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="password" class="form-label">
|
||||||
|
<i class="bi bi-key-fill me-2"></i>Password
|
||||||
|
</label>
|
||||||
|
<div class="input-group has-validation">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<i class="bi bi-key"></i>
|
||||||
|
</span>
|
||||||
|
<input type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
autocomplete="current-password"
|
||||||
|
minlength="8"
|
||||||
|
maxlength="128">
|
||||||
|
<button class="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
id="togglePassword"
|
||||||
|
aria-label="Toggle password visibility">
|
||||||
|
<i class="bi bi-eye-fill"></i>
|
||||||
|
</button>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Password must be at least 8 characters
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
<i class="bi bi-shield-lock me-1"></i>Password is securely encrypted
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rate Limiting Notice -->
|
||||||
|
{% if attempts_remaining is defined %}
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||||
|
{{ attempts_remaining }} login attempts remaining
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<div class="d-grid mb-4">
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-primary btn-lg"
|
||||||
|
id="loginButton">
|
||||||
|
<i class="bi bi-box-arrow-in-right me-2"></i>Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- TorGuard Portal Link -->
|
||||||
|
<div class="text-center">
|
||||||
|
<hr class="my-4">
|
||||||
|
<a href="https://torguard.net"
|
||||||
|
target="_blank"
|
||||||
|
class="btn btn-outline-secondary btn-sm">
|
||||||
|
<i class="bi bi-box-arrow-up-right me-2"></i>Login to TorGuard Portal
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Security Notice -->
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="bi bi-shield-lock me-1"></i>
|
||||||
|
Secure, encrypted connection
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Login Scripts -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const form = document.querySelector('form');
|
||||||
|
const loginButton = document.getElementById('loginButton');
|
||||||
|
const togglePassword = document.getElementById('togglePassword');
|
||||||
|
const password = document.getElementById('password');
|
||||||
|
const username = document.getElementById('username');
|
||||||
|
|
||||||
|
// Password visibility toggle
|
||||||
|
togglePassword.addEventListener('click', function() {
|
||||||
|
const icon = this.querySelector('i');
|
||||||
|
|
||||||
|
if (password.type === 'password') {
|
||||||
|
password.type = 'text';
|
||||||
|
icon.classList.remove('bi-eye-fill');
|
||||||
|
icon.classList.add('bi-eye-slash-fill');
|
||||||
|
} else {
|
||||||
|
password.type = 'password';
|
||||||
|
icon.classList.remove('bi-eye-slash-fill');
|
||||||
|
icon.classList.add('bi-eye-fill');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Form validation
|
||||||
|
form.addEventListener('submit', function(event) {
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
} else {
|
||||||
|
loginButton.disabled = true;
|
||||||
|
loginButton.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Logging in...';
|
||||||
|
}
|
||||||
|
form.classList.add('was-validated');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset validation on input
|
||||||
|
username.addEventListener('input', function() {
|
||||||
|
form.classList.remove('was-validated');
|
||||||
|
});
|
||||||
|
|
||||||
|
password.addEventListener('input', function() {
|
||||||
|
form.classList.remove('was-validated');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent pasting into username field
|
||||||
|
username.addEventListener('paste', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent form resubmission
|
||||||
|
if (window.history.replaceState) {
|
||||||
|
window.history.replaceState(null, null, window.location.href);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,243 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-container">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<h2 class="h3 mb-3">Create Administrator Account</h2>
|
||||||
|
<p class="text-muted">Set up your secure access to TorGuard WireGuard Manager</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" class="needs-validation" novalidate autocomplete="off">
|
||||||
|
<!-- CSRF Protection -->
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
|
||||||
|
<!-- Username Field -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">
|
||||||
|
<i class="bi bi-person-fill me-2"></i>Username
|
||||||
|
</label>
|
||||||
|
<div class="input-group has-validation">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<i class="bi bi-person"></i>
|
||||||
|
</span>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
required
|
||||||
|
autofocus
|
||||||
|
autocomplete="username"
|
||||||
|
pattern="[a-zA-Z0-9_-]{3,20}"
|
||||||
|
title="Username must be 3-20 characters, using only letters, numbers, underscore, or hyphen"
|
||||||
|
maxlength="20">
|
||||||
|
<div class="valid-feedback">
|
||||||
|
<i class="bi bi-check-circle me-1"></i>Username available
|
||||||
|
</div>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Username must be 3-20 characters, using only letters, numbers, underscore, or hyphen
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password Field -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">
|
||||||
|
<i class="bi bi-key-fill me-2"></i>Password
|
||||||
|
</label>
|
||||||
|
<div class="input-group has-validation">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<i class="bi bi-key"></i>
|
||||||
|
</span>
|
||||||
|
<input type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
autocomplete="new-password"
|
||||||
|
minlength="8"
|
||||||
|
maxlength="128"
|
||||||
|
pattern="^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$"
|
||||||
|
title="Password must be at least 8 characters and include letters, numbers, and special characters">
|
||||||
|
<button class="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
id="togglePassword"
|
||||||
|
aria-label="Toggle password visibility">
|
||||||
|
<i class="bi bi-eye-fill"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Password Strength Meter -->
|
||||||
|
<div class="progress mt-2" style="height: 5px;">
|
||||||
|
<div class="progress-bar" id="passwordStrength" role="progressbar" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-text mt-2">
|
||||||
|
<div class="password-requirements">
|
||||||
|
<div id="req-length" class="requirement">
|
||||||
|
<i class="bi bi-x-circle text-danger"></i> At least 8 characters
|
||||||
|
</div>
|
||||||
|
<div id="req-letter" class="requirement">
|
||||||
|
<i class="bi bi-x-circle text-danger"></i> Contains letters
|
||||||
|
</div>
|
||||||
|
<div id="req-number" class="requirement">
|
||||||
|
<i class="bi bi-x-circle text-danger"></i> Contains numbers
|
||||||
|
</div>
|
||||||
|
<div id="req-special" class="requirement">
|
||||||
|
<i class="bi bi-x-circle text-danger"></i> Contains special characters
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirm Password Field -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="confirm_password" class="form-label">
|
||||||
|
<i class="bi bi-key-fill me-2"></i>Confirm Password
|
||||||
|
</label>
|
||||||
|
<div class="input-group has-validation">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<i class="bi bi-key"></i>
|
||||||
|
</span>
|
||||||
|
<input type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="confirm_password"
|
||||||
|
required
|
||||||
|
autocomplete="new-password"
|
||||||
|
maxlength="128">
|
||||||
|
<div class="valid-feedback">
|
||||||
|
<i class="bi bi-check-circle me-1"></i>Passwords match
|
||||||
|
</div>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Passwords do not match
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<div class="d-grid mb-4">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg" id="submitBtn">
|
||||||
|
<i class="bi bi-person-plus-fill me-2"></i>Create Administrator Account
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Security Notices -->
|
||||||
|
<div class="text-center">
|
||||||
|
<small class="text-muted d-block mb-2">
|
||||||
|
<i class="bi bi-shield-lock me-1"></i>
|
||||||
|
Your credentials will be stored securely using industry-standard encryption
|
||||||
|
</small>
|
||||||
|
<small class="text-muted d-block">
|
||||||
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
|
This account will have full administrative access to the WireGuard Manager
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Registration Scripts -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const form = document.querySelector('form');
|
||||||
|
const password = document.getElementById('password');
|
||||||
|
const confirmPassword = document.getElementById('confirm_password');
|
||||||
|
const togglePassword = document.getElementById('togglePassword');
|
||||||
|
const submitBtn = document.getElementById('submitBtn');
|
||||||
|
const passwordStrength = document.getElementById('passwordStrength');
|
||||||
|
|
||||||
|
// Password visibility toggle
|
||||||
|
togglePassword.addEventListener('click', function() {
|
||||||
|
const icon = this.querySelector('i');
|
||||||
|
|
||||||
|
if (password.type === 'password') {
|
||||||
|
password.type = 'text';
|
||||||
|
icon.classList.remove('bi-eye-fill');
|
||||||
|
icon.classList.add('bi-eye-slash-fill');
|
||||||
|
} else {
|
||||||
|
password.type = 'password';
|
||||||
|
icon.classList.remove('bi-eye-slash-fill');
|
||||||
|
icon.classList.add('bi-eye-fill');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Password strength checker
|
||||||
|
function checkPasswordStrength(value) {
|
||||||
|
let strength = 0;
|
||||||
|
const requirements = {
|
||||||
|
length: value.length >= 8,
|
||||||
|
letter: /[A-Za-z]/.test(value),
|
||||||
|
number: /\d/.test(value),
|
||||||
|
special: /[@$!%*#?&]/.test(value)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update requirement indicators
|
||||||
|
document.getElementById('req-length').innerHTML = `
|
||||||
|
<i class="bi bi-${requirements.length ? 'check-circle text-success' : 'x-circle text-danger'}"></i>
|
||||||
|
At least 8 characters
|
||||||
|
`;
|
||||||
|
document.getElementById('req-letter').innerHTML = `
|
||||||
|
<i class="bi bi-${requirements.letter ? 'check-circle text-success' : 'x-circle text-danger'}"></i>
|
||||||
|
Contains letters
|
||||||
|
`;
|
||||||
|
document.getElementById('req-number').innerHTML = `
|
||||||
|
<i class="bi bi-${requirements.number ? 'check-circle text-success' : 'x-circle text-danger'}"></i>
|
||||||
|
Contains numbers
|
||||||
|
`;
|
||||||
|
document.getElementById('req-special').innerHTML = `
|
||||||
|
<i class="bi bi-${requirements.special ? 'check-circle text-success' : 'x-circle text-danger'}"></i>
|
||||||
|
Contains special characters
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Calculate strength
|
||||||
|
strength += requirements.length ? 25 : 0;
|
||||||
|
strength += requirements.letter ? 25 : 0;
|
||||||
|
strength += requirements.number ? 25 : 0;
|
||||||
|
strength += requirements.special ? 25 : 0;
|
||||||
|
|
||||||
|
// Update strength bar
|
||||||
|
passwordStrength.style.width = strength + '%';
|
||||||
|
if (strength < 50) {
|
||||||
|
passwordStrength.className = 'progress-bar bg-danger';
|
||||||
|
} else if (strength < 75) {
|
||||||
|
passwordStrength.className = 'progress-bar bg-warning';
|
||||||
|
} else {
|
||||||
|
passwordStrength.className = 'progress-bar bg-success';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password input handler
|
||||||
|
password.addEventListener('input', function() {
|
||||||
|
checkPasswordStrength(this.value);
|
||||||
|
if (confirmPassword.value) {
|
||||||
|
confirmPassword.dispatchEvent(new Event('input'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Confirm password validation
|
||||||
|
confirmPassword.addEventListener('input', function() {
|
||||||
|
if (this.value !== password.value) {
|
||||||
|
this.setCustomValidity('Passwords do not match');
|
||||||
|
} else {
|
||||||
|
this.setCustomValidity('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Form submission
|
||||||
|
form.addEventListener('submit', function(event) {
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
} else {
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Creating Account...';
|
||||||
|
}
|
||||||
|
form.classList.add('was-validated');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent form resubmission
|
||||||
|
if (window.history.replaceState) {
|
||||||
|
window.history.replaceState(null, null, window.location.href);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,153 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
TorGuard WireGuard Manager Uninstaller
|
||||||
|
Completely removes the WireGuard Manager, its configurations, services, and dependencies.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def print_step(emoji, message):
|
||||||
|
"""Print a step with emoji and message"""
|
||||||
|
print(f"\n{emoji} {message}")
|
||||||
|
|
||||||
|
def run_command(command, error_message, shell=False):
|
||||||
|
"""Run a shell command and handle errors"""
|
||||||
|
try:
|
||||||
|
if shell:
|
||||||
|
subprocess.run(command, check=True, shell=True)
|
||||||
|
else:
|
||||||
|
subprocess.run(command, check=True)
|
||||||
|
print("✅ Done!")
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"❌ Error: {error_message}")
|
||||||
|
if hasattr(e, 'stderr') and e.stderr:
|
||||||
|
print(f"Details: {e.stderr.decode()}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def remove_firewall_rules():
|
||||||
|
"""Remove firewall rules related to WireGuard Manager"""
|
||||||
|
print_step("🛑", "Removing firewall rules...")
|
||||||
|
rules = [
|
||||||
|
"ufw delete allow 1337/tcp",
|
||||||
|
"ufw --force disable"
|
||||||
|
]
|
||||||
|
for rule in rules:
|
||||||
|
run_command(rule, "Failed to remove firewall rule", shell=True)
|
||||||
|
|
||||||
|
def stop_and_disable_services():
|
||||||
|
"""Stops and removes the WireGuard Manager system service"""
|
||||||
|
print_step("🛑", "Stopping and disabling WireGuard Manager service...")
|
||||||
|
service_name = "wireguard-manager"
|
||||||
|
run_command(f"systemctl stop {service_name}", "Failed to stop WireGuard Manager service", shell=True)
|
||||||
|
run_command(f"systemctl disable {service_name}", "Failed to disable WireGuard Manager service", shell=True)
|
||||||
|
run_command(f"rm -f /etc/systemd/system/{service_name}.service", "Failed to remove service file", shell=True)
|
||||||
|
run_command("systemctl daemon-reload", "Failed to reload systemd", shell=True)
|
||||||
|
|
||||||
|
def remove_wireguard():
|
||||||
|
"""Removes WireGuard and its configurations"""
|
||||||
|
print_step("🗑️", "Removing WireGuard and its configurations...")
|
||||||
|
|
||||||
|
# Remove WireGuard kernel module
|
||||||
|
run_command("modprobe -r wireguard", "Failed to remove WireGuard kernel module", shell=True)
|
||||||
|
|
||||||
|
# Uninstall WireGuard packages
|
||||||
|
run_command("apt-get remove --purge -y wireguard wireguard-tools", "Failed to uninstall WireGuard", shell=True)
|
||||||
|
|
||||||
|
# Remove WireGuard configurations
|
||||||
|
run_command("rm -rf /etc/wireguard", "Failed to remove WireGuard configurations", shell=True)
|
||||||
|
|
||||||
|
def reset_network_config():
|
||||||
|
"""Resets networking configurations made by the installer"""
|
||||||
|
print_step("🔄", "Resetting network configurations...")
|
||||||
|
|
||||||
|
# Reset IP forwarding
|
||||||
|
run_command("sed -i 's/net.ipv4.ip_forward=1/#net.ipv4.ip_forward=1/' /etc/sysctl.conf", "Failed to reset sysctl.conf", shell=True)
|
||||||
|
run_command("sysctl -p", "Failed to apply sysctl changes", shell=True)
|
||||||
|
|
||||||
|
# Reset NAT rules
|
||||||
|
run_command("iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE", "Failed to remove NAT rule", shell=True)
|
||||||
|
run_command("iptables -D FORWARD -i wg0 -j ACCEPT", "Failed to remove iptables FORWARD rule", shell=True)
|
||||||
|
run_command("iptables -D FORWARD -o wg0 -j ACCEPT", "Failed to remove iptables FORWARD rule", shell=True)
|
||||||
|
|
||||||
|
# Reset iptables rules to default
|
||||||
|
run_command("iptables -F", "Failed to flush iptables rules", shell=True)
|
||||||
|
run_command("iptables -X", "Failed to delete custom chains", shell=True)
|
||||||
|
run_command("iptables -t nat -F", "Failed to flush NAT rules", shell=True)
|
||||||
|
run_command("iptables -t nat -X", "Failed to delete custom NAT chains", shell=True)
|
||||||
|
|
||||||
|
# Remove iptables-persistent package
|
||||||
|
run_command("apt-get remove --purge -y iptables-persistent", "Failed to remove iptables-persistent", shell=True)
|
||||||
|
|
||||||
|
def remove_wireguard_manager():
|
||||||
|
"""Deletes all files and directories related to the WireGuard Manager"""
|
||||||
|
print_step("🗑️", "Removing WireGuard Manager files...")
|
||||||
|
|
||||||
|
install_dir = Path("/opt/wireguard-manager")
|
||||||
|
config_dir = Path("/etc/wireguard-manager")
|
||||||
|
|
||||||
|
# Delete directories
|
||||||
|
if install_dir.exists():
|
||||||
|
shutil.rmtree(install_dir, ignore_errors=True)
|
||||||
|
if config_dir.exists():
|
||||||
|
shutil.rmtree(config_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
# Remove WireGuard kernel module auto-load setting
|
||||||
|
run_command("rm -f /etc/modules-load.d/wireguard.conf", "Failed to remove WireGuard module auto-load config", shell=True)
|
||||||
|
|
||||||
|
def remove_python_env():
|
||||||
|
"""Removes the Python virtual environment"""
|
||||||
|
print_step("🐍", "Removing Python virtual environment...")
|
||||||
|
venv_path = Path("/opt/wireguard-manager/venv")
|
||||||
|
if venv_path.exists():
|
||||||
|
shutil.rmtree(venv_path, ignore_errors=True)
|
||||||
|
|
||||||
|
# Remove Python dependencies
|
||||||
|
run_command("apt-get remove --purge -y python3-pip python3-venv", "Failed to remove Python dependencies", shell=True)
|
||||||
|
|
||||||
|
def final_cleanup():
|
||||||
|
"""Performs final cleanup and system reset"""
|
||||||
|
print_step("🧹", "Performing final cleanup...")
|
||||||
|
|
||||||
|
# Clear APT cache
|
||||||
|
run_command("apt-get autoremove -y", "Failed to remove unnecessary packages", shell=True)
|
||||||
|
run_command("apt-get clean", "Failed to clean package cache", shell=True)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if os.geteuid() != 0:
|
||||||
|
print("❌ This script must be run as root (sudo)")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("""
|
||||||
|
🛑 TorGuard WireGuard Manager Uninstaller
|
||||||
|
========================================
|
||||||
|
This will completely remove WireGuard Manager, its configurations, services, and dependencies.
|
||||||
|
""")
|
||||||
|
|
||||||
|
confirmation = input("⚠️ Are you sure you want to proceed? This action is irreversible! (yes/no): ").strip().lower()
|
||||||
|
if confirmation != "yes":
|
||||||
|
print("❌ Uninstallation aborted.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
stop_and_disable_services()
|
||||||
|
remove_firewall_rules()
|
||||||
|
remove_wireguard()
|
||||||
|
reset_network_config()
|
||||||
|
remove_wireguard_manager()
|
||||||
|
remove_python_env()
|
||||||
|
final_cleanup()
|
||||||
|
|
||||||
|
print("""
|
||||||
|
✅ Uninstallation Complete!
|
||||||
|
==========================
|
||||||
|
TorGuard WireGuard Manager and all associated configurations have been successfully removed.
|
||||||
|
|
||||||
|
If you need to reinstall, run the installer script again.
|
||||||
|
""")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
Loading…
Reference in New Issue