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> |