| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| <title>TorProxy VPN Console</title> |
| <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet"> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script> |
| <style> |
| :root { |
| --bg-base: #030303; |
| --panel-bg: rgba(9, 9, 12, 0.7); |
| --panel-border: rgba(255, 90, 31, 0.08); |
| --primary: #FF5A1F; |
| --primary-glow: rgba(255, 90, 31, 0.2); |
| --accent: #FF9E00; |
| --success: #00E676; |
| --danger: #FF1744; |
| --text-primary: #F4F4F5; |
| --text-secondary: #A1A1AA; |
| --text-muted: #52525B; |
| --font-mono: 'JetBrains Mono', monospace; |
| } |
| |
| * { |
| box-sizing: border-box; |
| margin: 0; |
| padding: 0; |
| -webkit-tap-highlight-color: transparent; |
| } |
| |
| body { |
| font-family: 'Plus Jakarta Sans', sans-serif; |
| background-color: var(--bg-base); |
| background-image: |
| radial-gradient(circle at 50% 0%, rgba(255, 90, 31, 0.12) 0%, transparent 60%), |
| radial-gradient(circle at 0% 100%, rgba(255, 158, 0, 0.04) 0%, transparent 40%); |
| background-attachment: fixed; |
| color: var(--text-primary); |
| min-height: 100vh; |
| display: flex; |
| justify-content: center; |
| align-items: flex-start; |
| padding: 20px 20px 40px; |
| } |
| |
| .container { |
| width: 100%; |
| max-width: 520px; |
| display: flex; |
| flex-direction: column; |
| gap: 20px; |
| } |
| |
| |
| .glass-panel { |
| background: var(--panel-bg); |
| border: 1px solid var(--panel-border); |
| border-radius: 24px; |
| padding: 24px; |
| backdrop-filter: blur(24px); |
| -webkit-backdrop-filter: blur(24px); |
| box-shadow: 0 10px 40px 0 rgba(0, 0, 0, 0.6); |
| transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1); |
| } |
| |
| .glass-panel:hover { |
| border-color: rgba(255, 90, 31, 0.16); |
| box-shadow: 0 14px 45px 0 rgba(255, 90, 31, 0.05); |
| } |
| |
| |
| .header { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| text-align: center; |
| padding: 12px 0 4px; |
| } |
| |
| .brand-pill { |
| background: rgba(255, 90, 31, 0.08); |
| border: 1px solid rgba(255, 90, 31, 0.2); |
| padding: 6px 14px; |
| border-radius: 100px; |
| font-size: 11px; |
| font-weight: 700; |
| letter-spacing: 1px; |
| color: var(--primary); |
| text-transform: uppercase; |
| margin-bottom: 12px; |
| display: inline-flex; |
| align-items: center; |
| gap: 8px; |
| } |
| |
| .brand-pill .pulse-dot { |
| width: 8px; |
| height: 8px; |
| background-color: var(--success); |
| border-radius: 50%; |
| animation: pulse-glow 1.5s infinite alternate; |
| } |
| |
| @keyframes pulse-glow { |
| from { box-shadow: 0 0 2px var(--success), 0 0 4px var(--success); } |
| to { box-shadow: 0 0 8px var(--success), 0 0 12px var(--success); } |
| } |
| |
| .header-title { |
| font-size: 28px; |
| font-weight: 800; |
| letter-spacing: -0.8px; |
| background: linear-gradient(135deg, #ffffff 40%, var(--text-secondary) 100%); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| } |
| |
| .header-subtitle { |
| font-size: 13px; |
| color: var(--text-secondary); |
| margin-top: 6px; |
| } |
| |
| |
| .stats-grid { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 12px; |
| } |
| |
| .stat-card { |
| background: rgba(0, 0, 0, 0.3); |
| border: 1px solid rgba(255, 255, 255, 0.03); |
| border-radius: 16px; |
| padding: 16px; |
| display: flex; |
| flex-direction: column; |
| gap: 8px; |
| } |
| |
| .stat-label { |
| font-size: 12px; |
| color: var(--text-secondary); |
| font-weight: 600; |
| text-transform: uppercase; |
| letter-spacing: 0.5px; |
| } |
| |
| .stat-value { |
| font-size: 24px; |
| font-weight: 800; |
| color: #fff; |
| } |
| |
| |
| .config-card { |
| display: flex; |
| flex-direction: column; |
| gap: 16px; |
| } |
| |
| .section-title { |
| font-size: 14px; |
| font-weight: 700; |
| color: #fff; |
| text-transform: uppercase; |
| letter-spacing: 0.5px; |
| border-bottom: 1px solid rgba(255, 255, 255, 0.05); |
| padding-bottom: 8px; |
| margin-bottom: 4px; |
| } |
| |
| .link-container { |
| background: rgba(0, 0, 0, 0.4); |
| border: 1px solid rgba(255, 90, 31, 0.15); |
| border-radius: 16px; |
| padding: 14px; |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| gap: 12px; |
| } |
| |
| .vless-link-text { |
| font-family: var(--font-mono); |
| font-size: 13px; |
| color: var(--accent); |
| overflow: hidden; |
| text-overflow: ellipsis; |
| white-space: nowrap; |
| flex-grow: 1; |
| } |
| |
| .copy-btn { |
| background: var(--primary); |
| border: none; |
| color: #fff; |
| padding: 8px 16px; |
| border-radius: 10px; |
| font-size: 12px; |
| font-weight: 700; |
| cursor: pointer; |
| transition: all 0.2s; |
| flex-shrink: 0; |
| } |
| |
| .copy-btn:hover { |
| opacity: 0.9; |
| box-shadow: 0 0 12px rgba(255, 90, 31, 0.3); |
| } |
| |
| .actions-row { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 10px; |
| } |
| |
| .btn-secondary { |
| background: rgba(255, 255, 255, 0.05); |
| border: 1px solid rgba(255, 255, 255, 0.08); |
| color: #fff; |
| padding: 12px; |
| border-radius: 12px; |
| font-size: 13px; |
| font-weight: 600; |
| cursor: pointer; |
| transition: all 0.2s; |
| text-align: center; |
| } |
| |
| .btn-secondary:hover { |
| background: rgba(255, 255, 255, 0.08); |
| border-color: rgba(255, 255, 255, 0.15); |
| } |
| |
| |
| .qr-wrapper { |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| margin: 10px 0; |
| } |
| #qrcode { |
| background: #fff; |
| padding: 16px; |
| border-radius: 16px; |
| display: inline-block; |
| } |
| |
| |
| .console-panel { |
| display: flex; |
| flex-direction: column; |
| gap: 12px; |
| } |
| |
| .console-screen { |
| background: rgba(0, 0, 0, 0.6); |
| border: 1px solid rgba(255, 255, 255, 0.04); |
| border-radius: 16px; |
| padding: 16px; |
| height: 180px; |
| overflow-y: auto; |
| font-family: var(--font-mono); |
| font-size: 11px; |
| color: #a3e635; |
| white-space: pre-wrap; |
| line-height: 1.5; |
| } |
| |
| |
| .guide-tabs { |
| display: flex; |
| gap: 8px; |
| margin-bottom: 12px; |
| border-bottom: 1px solid rgba(255, 255, 255, 0.05); |
| padding-bottom: 8px; |
| } |
| |
| .tab-btn { |
| background: transparent; |
| border: none; |
| color: var(--text-secondary); |
| font-size: 13px; |
| font-weight: 600; |
| cursor: pointer; |
| padding: 4px 8px; |
| transition: color 0.2s; |
| } |
| |
| .tab-btn.active { |
| color: var(--primary); |
| border-bottom: 2px solid var(--primary); |
| } |
| |
| .tab-content { |
| display: none; |
| font-size: 13px; |
| color: var(--text-secondary); |
| line-height: 1.6; |
| } |
| |
| .tab-content.active { |
| display: block; |
| } |
| |
| .tab-content ol { |
| padding-left: 20px; |
| margin-top: 8px; |
| } |
| |
| .tab-content li { |
| margin-bottom: 6px; |
| } |
| |
| |
| .config-form { |
| display: flex; |
| flex-direction: column; |
| gap: 10px; |
| margin-top: 10px; |
| } |
| |
| .input-group { |
| display: flex; |
| gap: 8px; |
| } |
| |
| .input-group input { |
| flex-grow: 1; |
| background: rgba(0, 0, 0, 0.4); |
| border: 1px solid rgba(255, 255, 255, 0.08); |
| border-radius: 10px; |
| padding: 10px; |
| color: #fff; |
| font-size: 13px; |
| } |
| |
| .input-group input:focus { |
| outline: none; |
| border-color: var(--primary); |
| } |
| |
| .alert-popup { |
| position: fixed; |
| bottom: 20px; |
| left: 50%; |
| transform: translateX(-50%) translateY(100px); |
| background: rgba(255, 90, 31, 0.95); |
| color: #fff; |
| padding: 12px 24px; |
| border-radius: 100px; |
| font-size: 13px; |
| font-weight: 700; |
| box-shadow: 0 10px 30px rgba(255, 90, 31, 0.3); |
| transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); |
| z-index: 1000; |
| } |
| |
| .alert-popup.show { |
| transform: translateX(-50%) translateY(0); |
| } |
| |
| |
| #login-overlay { |
| position: fixed; |
| top: 0; |
| left: 0; |
| width: 100vw; |
| height: 100vh; |
| background: #030303; |
| background-image: radial-gradient(circle at 50% 30%, rgba(255, 90, 31, 0.1) 0%, transparent 60%); |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| z-index: 9999; |
| } |
| .login-card { |
| background: rgba(9, 9, 12, 0.8); |
| border: 1px solid rgba(255, 90, 31, 0.1); |
| border-radius: 24px; |
| padding: 40px; |
| width: 100%; |
| max-width: 360px; |
| box-shadow: 0 20px 40px rgba(0,0,0,0.5); |
| backdrop-filter: blur(20px); |
| text-align: center; |
| } |
| .login-card h2 { |
| margin-bottom: 8px; |
| font-weight: 800; |
| color: #fff; |
| letter-spacing: -0.5px; |
| } |
| .login-card p { |
| font-size: 13px; |
| color: #a1a1aa; |
| margin-bottom: 24px; |
| } |
| .login-card input[type="password"] { |
| width: 100%; |
| background: rgba(0, 0, 0, 0.4); |
| border: 1px solid rgba(255, 90, 31, 0.2); |
| border-radius: 12px; |
| padding: 14px; |
| color: #fff; |
| box-sizing: border-box; |
| margin-bottom: 16px; |
| font-size: 14px; |
| transition: all 0.2s; |
| } |
| .login-card input[type="password"]:focus { |
| outline: none; |
| border-color: #FF5A1F; |
| box-shadow: 0 0 10px rgba(255, 90, 31, 0.2); |
| } |
| .login-card button { |
| width: 100%; |
| background: #FF5A1F; |
| border: none; |
| border-radius: 12px; |
| padding: 14px; |
| color: #fff; |
| font-weight: 600; |
| cursor: pointer; |
| font-size: 14px; |
| transition: opacity 0.2s; |
| } |
| .login-card button:hover { |
| opacity: 0.9; |
| } |
| .login-card .error { |
| color: #ff1744; |
| font-size: 12px; |
| margin-bottom: 16px; |
| font-weight: 600; |
| } |
| </style> |
| </head> |
| <body> |
| |
| <div id="login-overlay" style="display: none;"> |
| <div class="login-card"> |
| <h2>TorProxy VPN</h2> |
| <p>Enter your administration password</p> |
| <div class="error" id="login-error"></div> |
| <form onsubmit="handleLogin(event)"> |
| <input type="password" id="login-password" placeholder="Password" required autofocus> |
| <button type="submit">Unlock Console</button> |
| </form> |
| </div> |
| </div> |
|
|
| <div class="container"> |
| |
| <div class="header"> |
| <div class="brand-pill"> |
| <span class="pulse-dot"></span> |
| VLESS VPN Service |
| </div> |
| <div class="header-title">TorProxy Console</div> |
| <div class="header-subtitle">Secure, fast, and permanent WebSockets-over-TLS</div> |
| </div> |
|
|
| |
| <div class="stats-grid"> |
| <div class="stat-card"> |
| <span class="stat-label">CPU Usage</span> |
| <span class="stat-value" id="cpu-val">0.0%</span> |
| </div> |
| <div class="stat-card"> |
| <span class="stat-label">RAM Usage</span> |
| <span class="stat-value" id="ram-val">0.0%</span> |
| </div> |
| </div> |
|
|
| |
| <div class="glass-panel config-card"> |
| <h3 class="section-title">VLESS Connection Profile</h3> |
| |
| <div class="link-container"> |
| <div class="vless-link-text" id="vless-link">vless://loading...</div> |
| <button class="copy-btn" onclick="copyLink()">Copy Link</button> |
| </div> |
|
|
| <div class="actions-row"> |
| <button class="btn-secondary" onclick="toggleQRCode()">Show QR Code</button> |
| <button class="btn-secondary" onclick="resetUUID()">Regen VLESS UUID</button> |
| </div> |
|
|
| <div class="qr-wrapper" id="qr-wrapper" style="display: none;"> |
| <div id="qrcode"></div> |
| </div> |
| </div> |
|
|
| |
| <div class="glass-panel"> |
| <h3 class="section-title">Server Routing Settings</h3> |
| <p style="font-size: 12px; color: var(--text-secondary); margin-bottom: 8px;"> |
| Specify a custom SNI host if you are using a custom domain. Leave blank to use default. |
| </p> |
| <form class="config-form" onsubmit="saveConfig(event)"> |
| <div class="input-group"> |
| <input type="text" id="custom-sni" placeholder="e.g. yourdomain.com"> |
| <button type="submit" class="copy-btn">Save SNI</button> |
| </div> |
| </form> |
| </div> |
|
|
| |
| <div class="glass-panel"> |
| <h3 class="section-title">Client Setup Guides</h3> |
| <div class="guide-tabs"> |
| <button class="tab-btn active" onclick="switchTab('hiddify')">Hiddify</button> |
| <button class="tab-btn" onclick="switchTab('v2rayng')">v2rayNG</button> |
| <button class="tab-btn" onclick="switchTab('shadowrocket')">Shadowrocket</button> |
| </div> |
| |
| <div class="tab-content active" id="guide-hiddify"> |
| <p><strong>Universal (Android, iOS, Windows, Mac):</strong></p> |
| <ol> |
| <li>Install the <strong>Hiddify</strong> app from the Google Play Store or App Store.</li> |
| <li>Copy the VLESS link above to your clipboard.</li> |
| <li>Open Hiddify and click <strong>Add Profile</strong> or the <strong>+</strong> button.</li> |
| <li>Select <strong>Import from Clipboard</strong>.</li> |
| <li>Tap the big circle button to connect!</li> |
| </ol> |
| </div> |
| <div class="tab-content" id="guide-v2rayng"> |
| <p><strong>Android Client:</strong></p> |
| <ol> |
| <li>Install <strong>v2rayNG</strong> from the Play Store.</li> |
| <li>Copy the VLESS link above.</li> |
| <li>Open the app, tap the <strong>+</strong> icon at the top, and select <strong>Import config from Clipboard</strong>.</li> |
| <li>Tap the imported config to select it, then tap the V button at the bottom right to connect.</li> |
| </ol> |
| </div> |
| <div class="tab-content" id="guide-shadowrocket"> |
| <p><strong>iOS/macOS Client:</strong></p> |
| <ol> |
| <li>Open <strong>Shadowrocket</strong> on your iOS device.</li> |
| <li>Copy the VLESS link or tap "Show QR Code" and scan it using the scanner icon at the top left.</li> |
| <li>The app will automatically import the profile.</li> |
| <li>Turn on the connection switch at the top.</li> |
| </ol> |
| </div> |
| </div> |
|
|
| |
| <div class="glass-panel console-panel"> |
| <h3 class="section-title">Live Sing-box Engine Logs</h3> |
| <div class="console-screen" id="log-console">Initializing log stream...</div> |
| </div> |
| </div> |
|
|
| |
| <div class="alert-popup" id="alert-popup">Copied to Clipboard!</div> |
|
|
| <script> |
| let currentLink = ""; |
| |
| function getAuthToken() { |
| return localStorage.getItem("torproxy_token") || ""; |
| } |
| |
| function setAuthToken(token) { |
| localStorage.setItem("torproxy_token", token); |
| } |
| |
| function clearAuthToken() { |
| localStorage.removeItem("torproxy_token"); |
| } |
| |
| function checkAuth() { |
| const token = getAuthToken(); |
| const overlay = document.getElementById("login-overlay"); |
| if (!token) { |
| overlay.style.display = "flex"; |
| } else { |
| overlay.style.display = "none"; |
| updateStats(); |
| } |
| } |
| |
| function handleLogin(e) { |
| e.preventDefault(); |
| const password = document.getElementById("login-password").value; |
| const errorDiv = document.getElementById("login-error"); |
| errorDiv.innerText = ""; |
| |
| fetch("/login", { |
| method: "POST", |
| body: "password=" + encodeURIComponent(password), |
| headers: { "Content-Type": "application/x-www-form-urlencoded" } |
| }) |
| .then(r => r.json()) |
| .then(data => { |
| if (data.success && data.token) { |
| setAuthToken(data.token); |
| checkAuth(); |
| } else { |
| errorDiv.innerText = data.error || "Login failed"; |
| } |
| }) |
| .catch(() => { |
| errorDiv.innerText = "Connection error"; |
| }); |
| } |
| |
| function showAlert(msg) { |
| const popup = document.getElementById("alert-popup"); |
| popup.innerText = msg; |
| popup.classList.add("show"); |
| setTimeout(() => { |
| popup.classList.remove("show"); |
| }, 2000); |
| } |
| |
| function copyLink() { |
| navigator.clipboard.writeText(currentLink).then(() => { |
| showAlert("VLESS Link Copied!"); |
| }).catch(() => { |
| showAlert("Failed to copy link"); |
| }); |
| } |
| |
| function switchTab(client) { |
| document.querySelectorAll(".tab-btn").forEach(btn => btn.classList.remove("active")); |
| document.querySelectorAll(".tab-content").forEach(c => c.classList.remove("active")); |
| |
| event.target.classList.add("active"); |
| document.getElementById("guide-" + client).classList.add("active"); |
| } |
| |
| function toggleQRCode() { |
| const wrapper = document.getElementById("qr-wrapper"); |
| if (wrapper.style.display === "none") { |
| wrapper.style.display = "flex"; |
| generateQR(); |
| } else { |
| wrapper.style.display = "none"; |
| } |
| } |
| |
| function generateQR() { |
| const qrContainer = document.getElementById("qrcode"); |
| qrContainer.innerHTML = ""; |
| if (currentLink) { |
| new QRCode(qrContainer, { |
| text: currentLink, |
| width: 200, |
| height: 200, |
| colorDark : "#000000", |
| colorLight : "#ffffff", |
| correctLevel : QRCode.CorrectLevel.M |
| }); |
| } |
| } |
| |
| function resetUUID() { |
| if (!confirm("Are you sure you want to regenerate the VLESS UUID? This will disconnect all current users.")) return; |
| fetch("/api/uuid/reset?token=" + encodeURIComponent(getAuthToken()), { method: "POST" }) |
| .then(r => { |
| if (r.status === 401) { clearAuthToken(); checkAuth(); return; } |
| return r.json(); |
| }) |
| .then(data => { |
| if (data && data.success) { |
| showAlert("UUID Regenerated successfully!"); |
| updateStats(); |
| } |
| }); |
| } |
| |
| function saveConfig(e) { |
| e.preventDefault(); |
| const customSni = document.getElementById("custom-sni").value; |
| fetch("/api/config/update?token=" + encodeURIComponent(getAuthToken()), { |
| method: "POST", |
| body: "custom_sni=" + encodeURIComponent(customSni), |
| headers: { "Content-Type": "application/x-www-form-urlencoded" } |
| }) |
| .then(r => { |
| if (r.status === 401) { clearAuthToken(); checkAuth(); return; } |
| return r.json(); |
| }) |
| .then(data => { |
| if (data && data.success) { |
| showAlert("SNI Config Saved!"); |
| updateStats(); |
| } |
| }); |
| } |
| |
| function updateStats() { |
| const token = getAuthToken(); |
| if (!token) return; |
| fetch("/api/status?token=" + encodeURIComponent(token)) |
| .then(r => { |
| if (r.status === 401) { |
| clearAuthToken(); |
| checkAuth(); |
| throw new Error("Unauthorized"); |
| } |
| return r.json(); |
| }) |
| .then(data => { |
| document.getElementById("cpu-val").innerText = data.cpu + "%"; |
| document.getElementById("ram-val").innerText = data.ram + "%"; |
| document.getElementById("vless-link").innerText = data.vless_link; |
| document.getElementById("custom-sni").value = data.custom_sni || ""; |
| currentLink = data.vless_link; |
| |
| const consoleScreen = document.getElementById("log-console"); |
| const isScrolledToBottom = consoleScreen.scrollHeight - consoleScreen.clientHeight <= consoleScreen.scrollTop + 20; |
| |
| consoleScreen.innerText = data.logs || "No logs available."; |
| |
| if (isScrolledToBottom) { |
| consoleScreen.scrollTop = consoleScreen.scrollHeight; |
| } |
| |
| const wrapper = document.getElementById("qr-wrapper"); |
| if (wrapper.style.display !== "none") { |
| generateQR(); |
| } |
| }) |
| .catch(err => console.log("Fetch error:", err.message)); |
| } |
| |
| |
| checkAuth(); |
| |
| setInterval(updateStats, 3000); |
| </script> |
| </body> |
| </html> |
|
|