976 lines
34 KiB
HTML
976 lines
34 KiB
HTML
|
<!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>
|