mvpg/frontend/index.html
2025-08-10 15:34:34 +02:00

975 lines
34 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VPN Gateway Control - Multi Provider</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: center;
color: white;
margin-bottom: 30px;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
.provider-selector {
background: white;
border-radius: 15px;
padding: 30px;
margin-bottom: 30px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.provider-tabs {
display: flex;
gap: 10px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.provider-tab {
flex: 1;
min-width: 150px;
padding: 15px 25px;
border: 2px solid #e0e0e0;
border-radius: 10px;
background: white;
cursor: pointer;
transition: all 0.3s;
text-align: center;
font-weight: 600;
}
.provider-tab:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.provider-tab.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-color: transparent;
}
.main-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
}
@media (max-width: 768px) {
.main-grid {
grid-template-columns: 1fr;
}
}
.card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 30px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
}
.status-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.status-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
}
.status-indicator {
display: flex;
align-items: center;
gap: 10px;
font-size: 1.3em;
font-weight: bold;
}
.status-dot {
width: 15px;
height: 15px;
border-radius: 50%;
animation: pulse 2s infinite;
}
.status-dot.connected {
background: #4ade80;
box-shadow: 0 0 10px #4ade80;
}
.status-dot.disconnected {
background: #f87171;
box-shadow: 0 0 10px #f87171;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.2); }
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.info-item {
background: rgba(255, 255, 255, 0.1);
padding: 15px;
border-radius: 10px;
backdrop-filter: blur(5px);
}
.info-label {
font-size: 0.9em;
opacity: 0.9;
margin-bottom: 5px;
}
.info-value {
font-size: 1.2em;
font-weight: bold;
}
.security-notice {
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
color: white;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
}
select, input, textarea {
width: 100%;
padding: 15px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 1em;
margin-bottom: 15px;
transition: all 0.3s;
}
select:focus, input:focus, textarea:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
button {
padding: 15px 30px;
border: none;
border-radius: 10px;
font-size: 1.1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
width: 100%;
margin-bottom: 10px;
}
.btn-primary {
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(74, 222, 128, 0.3);
}
.btn-danger {
background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
color: white;
}
.btn-secondary {
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
color: white;
}
.custom-server-form {
display: none;
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
margin-top: 20px;
}
.custom-server-form.active {
display: block;
}
.server-list {
max-height: 300px;
overflow-y: auto;
margin-top: 20px;
}
.server-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: white;
border-radius: 10px;
margin-bottom: 10px;
transition: all 0.3s;
}
.server-item:hover {
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.import-section {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
margin-top: 20px;
}
.file-upload {
position: relative;
display: inline-block;
cursor: pointer;
width: 100%;
}
.file-upload input[type=file] {
position: absolute;
left: -9999px;
}
.file-upload label {
display: block;
padding: 15px;
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
color: white;
border-radius: 10px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.file-upload label:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(96, 165, 250, 0.3);
}
.tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.tab {
padding: 10px 20px;
background: #e0e0e0;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
}
.tab.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.spinner {
display: none;
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading .spinner {
display: block;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 15px;
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.keypair-display {
background: #f8f9fa;
padding: 15px;
border-radius: 10px;
margin: 10px 0;
font-family: monospace;
word-break: break-all;
font-size: 0.9em;
}
.copy-btn {
padding: 5px 10px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 0.9em;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔐 VPN Gateway Control Center</h1>
<p>Multi-Provider Support with Permanent Killswitch</p>
</div>
<div class="provider-selector">
<h2>Select VPN Provider</h2>
<div class="provider-tabs">
<div class="provider-tab active" data-provider="mullvad" onclick="switchProvider('mullvad')">
<div>🌍 Mullvad</div>
<small>Commercial VPN</small>
</div>
<div class="provider-tab" data-provider="custom" onclick="switchProvider('custom')">
<div>🔧 Custom Server</div>
<small>Own VPS/Server</small>
</div>
<div class="provider-tab" data-provider="imported" onclick="switchProvider('imported')">
<div>📁 Import Config</div>
<small>Existing WireGuard</small>
</div>
</div>
</div>
<div class="main-grid">
<div class="card status-card">
<div class="status-header">
<div class="status-indicator">
<span class="status-dot disconnected" id="statusDot"></span>
<span id="statusText">Disconnected</span>
</div>
<button class="btn-secondary" onclick="refreshStatus()" style="width: auto; padding: 10px 20px;">
🔄 Refresh
</button>
</div>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Provider</div>
<div class="info-value" id="currentProvider">-</div>
</div>
<div class="info-item">
<div class="info-label">Server</div>
<div class="info-value" id="currentServer">-</div>
</div>
<div class="info-item">
<div class="info-label">Public IP</div>
<div class="info-value" id="publicIP">-</div>
</div>
<div class="info-item">
<div class="info-label">Location</div>
<div class="info-value" id="location">-</div>
</div>
</div>
<div class="security-notice" style="margin-top: 20px;">
<strong>🛡️ Security Status: PROTECTED</strong><br>
✓ Killswitch permanently active<br>
✓ No internet without VPN<br>
✓ DNS leak protection enabled
</div>
</div>
<div class="card">
<!-- Mullvad Provider Section -->
<div id="mullvad-section" class="provider-section">
<h3>Mullvad Server Selection</h3>
<select id="countrySelect" onchange="updateCities()">
<option value="">Loading countries...</option>
</select>
<select id="citySelect" onchange="updateServers()">
<option value="">Select a country first</option>
</select>
<select id="serverSelect">
<option value="">Select a city first</option>
</select>
</div>
<!-- Custom Provider Section -->
<div id="custom-section" class="provider-section" style="display: none;">
<h3>Custom WireGuard Servers</h3>
<button class="btn-secondary" onclick="toggleCustomForm()">
Add New Server
</button>
<div id="customServerForm" class="custom-server-form">
<h4>Add Custom Server</h4>
<input type="text" id="customName" placeholder="Server Name (e.g., my-vps)">
<input type="text" id="customEndpoint" placeholder="Endpoint (IP:Port or domain:port)">
<input type="text" id="customPublicKey" placeholder="Server Public Key">
<input type="text" id="customPrivateKey" placeholder="Client Private Key (optional)">
<input type="text" id="customAddress" placeholder="Client IP (default: 10.0.0.2/32)">
<input type="text" id="customDNS" placeholder="DNS Servers (default: 1.1.1.1,1.0.0.1)">
<input type="text" id="customLocation" placeholder="Location (e.g., Germany)">
<button class="btn-secondary" onclick="generateKeypair()">
🔑 Generate Keypair
</button>
<button class="btn-primary" onclick="addCustomServer()">
Add Server
</button>
</div>
<div class="server-list" id="customServerList">
<!-- Custom servers will be listed here -->
</div>
<select id="customServerSelect">
<option value="">Select a custom server</option>
</select>
</div>
<!-- Import Provider Section -->
<div id="import-section" class="provider-section" style="display: none;">
<h3>Import WireGuard Configuration</h3>
<div class="import-section">
<input type="text" id="importName" placeholder="Configuration Name">
<div class="file-upload">
<input type="file" id="configFile" accept=".conf" onchange="handleFileSelect(event)">
<label for="configFile">📁 Choose WireGuard Config File</label>
</div>
<textarea id="configContent" placeholder="Or paste your WireGuard config here..." rows="10"></textarea>
<button class="btn-primary" onclick="importConfig()">
Import Configuration
</button>
</div>
<div class="server-list" id="importedConfigList">
<!-- Imported configs will be listed here -->
</div>
<select id="importedServerSelect">
<option value="">Select an imported config</option>
</select>
</div>
<div style="margin-top: 30px;">
<button class="btn-primary" onclick="connectVPN()">
<span>🔗 Connect</span>
<span class="spinner"></span>
</button>
<button class="btn-danger" onclick="disconnectVPN()">
<span>⛔ Disconnect</span>
<span class="spinner"></span>
</button>
</div>
</div>
</div>
</div>
<!-- Keypair Modal -->
<div id="keypairModal" class="modal">
<div class="modal-content">
<h3>Generated WireGuard Keypair</h3>
<p style="margin: 15px 0;">Save these keys securely!</p>
<label>Private Key (Keep Secret!):</label>
<div class="keypair-display" id="generatedPrivateKey"></div>
<button class="copy-btn" onclick="copyToClipboard('generatedPrivateKey')">Copy</button>
<label style="margin-top: 15px; display: block;">Public Key (Share with Server):</label>
<div class="keypair-display" id="generatedPublicKey"></div>
<button class="copy-btn" onclick="copyToClipboard('generatedPublicKey')">Copy</button>
<button class="btn-secondary" style="margin-top: 20px;" onclick="closeModal()">
Close
</button>
</div>
</div>
<script>
let currentProvider = 'mullvad';
let servers = {};
let customServers = [];
let importedConfigs = [];
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadProviders();
refreshStatus();
setInterval(refreshStatus, 10000);
});
async function loadProviders() {
try {
const response = await fetch('/api/providers');
const data = await response.json();
if (data.current) {
currentProvider = data.current;
switchProvider(currentProvider);
} else {
loadServers();
}
} catch (error) {
console.error('Error loading providers:', error);
loadServers();
}
}
async function switchProvider(provider) {
// Update UI
document.querySelectorAll('.provider-tab').forEach(tab => {
tab.classList.remove('active');
});
document.querySelector(`[data-provider="${provider}"]`).classList.add('active');
// Hide all sections
document.querySelectorAll('.provider-section').forEach(section => {
section.style.display = 'none';
});
// Show selected section
document.getElementById(`${provider}-section`).style.display = 'block';
currentProvider = provider;
// Switch backend provider
try {
await fetch(`/api/provider/${provider}`, { method: 'POST' });
} catch (error) {
console.error('Error switching provider:', error);
}
// Load servers for new provider
loadServers();
}
async function loadServers() {
try {
const response = await fetch('/api/servers');
const data = await response.json();
servers = data.servers;
if (currentProvider === 'mullvad') {
updateMullvadSelects();
} else if (currentProvider === 'custom') {
updateCustomServers();
} else if (currentProvider === 'imported') {
updateImportedConfigs();
}
} catch (error) {
console.error('Error loading servers:', error);
}
}
function updateMullvadSelects() {
const countrySelect = document.getElementById('countrySelect');
countrySelect.innerHTML = '<option value="">Select a country</option>';
Object.keys(servers).forEach(country => {
const option = document.createElement('option');
option.value = country;
option.textContent = country;
countrySelect.appendChild(option);
});
}
function updateCities() {
const country = document.getElementById('countrySelect').value;
const citySelect = document.getElementById('citySelect');
const serverSelect = document.getElementById('serverSelect');
citySelect.innerHTML = '<option value="">Select a city</option>';
serverSelect.innerHTML = '<option value="">Select a city first</option>';
if (country && servers[country]) {
Object.keys(servers[country]).forEach(city => {
const option = document.createElement('option');
option.value = city;
option.textContent = city;
citySelect.appendChild(option);
});
}
}
function updateServers() {
const country = document.getElementById('countrySelect').value;
const city = document.getElementById('citySelect').value;
const serverSelect = document.getElementById('serverSelect');
serverSelect.innerHTML = '<option value="">Select a server</option>';
if (country && city && servers[country][city]) {
servers[country][city].forEach(server => {
const option = document.createElement('option');
option.value = server.hostname;
option.textContent = `${server.hostname} (${server.type || 'WireGuard'})`;
serverSelect.appendChild(option);
});
}
}
function updateCustomServers() {
const customSelect = document.getElementById('customServerSelect');
const serverList = document.getElementById('customServerList');
customSelect.innerHTML = '<option value="">Select a custom server</option>';
serverList.innerHTML = '';
// Flatten servers structure for custom
for (const location in servers) {
for (const city in servers[location]) {
servers[location][city].forEach(server => {
// Add to select
const option = document.createElement('option');
option.value = server.hostname;
option.textContent = `${server.hostname} (${location})`;
customSelect.appendChild(option);
// Add to list
const item = document.createElement('div');
item.className = 'server-item';
item.innerHTML = `
<div>
<strong>${server.hostname}</strong><br>
<small>${server.endpoint} - ${location}</small>
</div>
<button class="btn-danger" style="width: auto; padding: 5px 15px;"
onclick="removeCustomServer('${server.hostname}')">
Remove
</button>
`;
serverList.appendChild(item);
});
}
}
}
function updateImportedConfigs() {
const importedSelect = document.getElementById('importedServerSelect');
const configList = document.getElementById('importedConfigList');
importedSelect.innerHTML = '<option value="">Select an imported config</option>';
configList.innerHTML = '';
// Check for imported configs in servers
if (servers['Imported'] && servers['Imported']['Configs']) {
servers['Imported']['Configs'].forEach(config => {
// Add to select
const option = document.createElement('option');
option.value = config.hostname;
option.textContent = config.hostname;
importedSelect.appendChild(option);
// Add to list
const item = document.createElement('div');
item.className = 'server-item';
item.innerHTML = `
<div>
<strong>${config.hostname}</strong><br>
<small>Imported Configuration</small>
</div>
`;
configList.appendChild(item);
});
}
}
function toggleCustomForm() {
const form = document.getElementById('customServerForm');
form.classList.toggle('active');
}
async function generateKeypair() {
try {
const response = await fetch('/api/keypair');
const data = await response.json();
document.getElementById('customPrivateKey').value = data.private_key;
document.getElementById('generatedPrivateKey').textContent = data.private_key;
document.getElementById('generatedPublicKey').textContent = data.public_key;
document.getElementById('keypairModal').classList.add('active');
} catch (error) {
alert('Error generating keypair: ' + error);
}
}
async function addCustomServer() {
const name = document.getElementById('customName').value;
const endpoint = document.getElementById('customEndpoint').value;
const publicKey = document.getElementById('customPublicKey').value;
if (!name || !endpoint || !publicKey) {
alert('Please fill in required fields');
return;
}
const config = {
name: name,
endpoint: endpoint,
public_key: publicKey,
private_key: document.getElementById('customPrivateKey').value,
address: document.getElementById('customAddress').value,
dns: document.getElementById('customDNS').value,
location: document.getElementById('customLocation').value || 'Custom'
};
try {
const response = await fetch('/api/custom/add', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(config)
});
if (response.ok) {
alert('Server added successfully');
toggleCustomForm();
// Clear form
document.querySelectorAll('#customServerForm input').forEach(input => input.value = '');
loadServers();
} else {
alert('Failed to add server');
}
} catch (error) {
alert('Error adding server: ' + error);
}
}
async function removeCustomServer(name) {
if (!confirm(`Remove server ${name}?`)) return;
try {
const response = await fetch(`/api/custom/remove/${name}`, {
method: 'DELETE'
});
if (response.ok) {
loadServers();
} else {
alert('Failed to remove server');
}
} catch (error) {
alert('Error removing server: ' + error);
}
}
function handleFileSelect(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
document.getElementById('configContent').value = e.target.result;
// Try to extract name from filename
const name = file.name.replace('.conf', '');
document.getElementById('importName').value = name;
};
reader.readAsText(file);
}
}
async function importConfig() {
const name = document.getElementById('importName').value;
const config = document.getElementById('configContent').value;
if (!name || !config) {
alert('Please provide a name and configuration');
return;
}
try {
const response = await fetch('/api/import', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: name, config: config})
});
if (response.ok) {
alert('Configuration imported successfully');
document.getElementById('importName').value = '';
document.getElementById('configContent').value = '';
loadServers();
} else {
alert('Failed to import configuration');
}
} catch (error) {
alert('Error importing config: ' + error);
}
}
async function refreshStatus() {
try {
const response = await fetch('/api/status');
const data = await response.json();
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
if (data.connected) {
statusDot.className = 'status-dot connected';
statusText.textContent = 'Connected';
document.getElementById('currentProvider').textContent = data.provider || '-';
document.getElementById('currentServer').textContent = data.server || '-';
document.getElementById('publicIP').textContent = data.ip || 'Checking...';
document.getElementById('location').textContent = data.location || '-';
} else {
statusDot.className = 'status-dot disconnected';
statusText.textContent = 'Disconnected';
document.getElementById('currentProvider').textContent = '-';
document.getElementById('currentServer').textContent = '-';
document.getElementById('publicIP').textContent = '-';
document.getElementById('location').textContent = '-';
}
} catch (error) {
console.error('Error refreshing status:', error);
}
}
async function connectVPN() {
let server = null;
if (currentProvider === 'mullvad') {
server = document.getElementById('serverSelect').value;
} else if (currentProvider === 'custom') {
server = document.getElementById('customServerSelect').value;
} else if (currentProvider === 'imported') {
server = document.getElementById('importedServerSelect').value;
}
if (!server) {
alert('Please select a server');
return;
}
const button = event.target.closest('button');
button.classList.add('loading');
try {
const response = await fetch('/api/connect', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({server: server})
});
const data = await response.json();
if (data.success) {
alert('Connected successfully!');
setTimeout(refreshStatus, 2000);
} else {
alert('Connection failed: ' + (data.error || 'Unknown error'));
}
} catch (error) {
alert('Connection error: ' + error);
} finally {
button.classList.remove('loading');
}
}
async function disconnectVPN() {
const button = event.target.closest('button');
button.classList.add('loading');
try {
const response = await fetch('/api/disconnect', {method: 'POST'});
const data = await response.json();
if (data.success) {
alert('Disconnected - WARNING: No internet access (killswitch active)');
setTimeout(refreshStatus, 1000);
} else {
alert('Disconnect failed: ' + (data.error || 'Unknown error'));
}
} catch (error) {
alert('Disconnect error: ' + error);
} finally {
button.classList.remove('loading');
}
}
function copyToClipboard(elementId) {
const text = document.getElementById(elementId).textContent;
navigator.clipboard.writeText(text).then(() => {
alert('Copied to clipboard!');
});
}
function closeModal() {
document.getElementById('keypairModal').classList.remove('active');
}
</script>
</body>
</html>