diff --git a/API/utils/xui.py b/API/utils/xui.py index 77628ec..71d80a5 100644 --- a/API/utils/xui.py +++ b/API/utils/xui.py @@ -12,7 +12,7 @@ class Configs: self.client_email = client_email self.display_name = display_name - async def legacy_payload(self, data: dict,) -> dict: + def legacy_payload(self, data: dict,) -> dict: inbounds = data.get("obj") inbound = next((i for i in inbounds if i.get("id") == self.inbound_id), None) if not inbound: @@ -44,7 +44,7 @@ class Configs: }) } - async def legacy_config(self, data: dict) -> str: + def legacy_config(self, data: dict) -> str: inbounds = data.get("obj") inbound = next((i for i in inbounds if i.get("id") == self.inbound_id), None) if not inbound: diff --git a/Frontend/Nginx/spectralvpn_api.nginx b/Frontend/Nginx/spectralvpn_api.nginx index 3abfedb..9f7c97f 100644 --- a/Frontend/Nginx/spectralvpn_api.nginx +++ b/Frontend/Nginx/spectralvpn_api.nginx @@ -5,24 +5,30 @@ server { ssl_certificate /etc/letsencrypt/live/spectralvpn.ru/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/spectralvpn.ru/privkey.pem; - + location / { if ($request_method = 'OPTIONS') { - add_header 'Access-Control-Allow-Origin' 'https://spectralvpn.ru'; - add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, OPTIONS'; - add_header 'Access-Control-Allow-Headers' 'Content-Type'; - add_header 'Access-Control-Max-Age' 86400; + add_header 'Access-Control-Allow-Origin' 'https://spectralvpn.ru' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'X-API-KEY, Content-Type, Authorization' always; + add_header 'Access-Control-Max-Age' 86400 always; + add_header 'Access-Control-Allow-Credentials' 'true' always; return 204; } - proxy_pass http://api:8000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Host $host; - proxy_set_header X-Forwarded-Port $server_port; + add_header 'Access-Control-Allow-Origin' 'https://spectralvpn.ru' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Expose-Headers' '*' always; # если нужно читать заголовки ответа - add_header Access-Control-Allow-Origin "https://spectralvpn.ru" always; + proxy_pass http://api:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + + proxy_hide_header Access-Control-Allow-Origin; + proxy_hide_header Access-Control-Allow-Credentials; } } \ No newline at end of file diff --git a/Frontend/Web/JS/control-panel.js b/Frontend/Web/JS/control-panel.js index 4f072f4..e4776c2 100644 --- a/Frontend/Web/JS/control-panel.js +++ b/Frontend/Web/JS/control-panel.js @@ -4,46 +4,77 @@ const getToken = () => localStorage.getItem("access_token"); const showNotification = (message, type = "success") => { const color = type === "success" ? "#00ffff" : "#ff5555"; - const notification = document.createElement("div"); - notification.style.cssText = ` - position: fixed; top: 20px; right: 20px; padding: 15px 25px; border-radius: 12px; - background: rgba(0,0,0,0.95); border: 1px solid ${color}; color: ${color}; - z-index: 10000; font-weight: 500; box-shadow: 0 4px 15px rgba(0,0,0,0.5); + const notif = document.createElement("div"); + notif.style.cssText = ` + position: fixed; top: 20px; right: 20px; padding: 14px 24px; border-radius: 12px; + background: rgba(0,0,0,0.96); border: 1px solid ${color}; color: ${color}; + z-index: 10000; font-weight: 500; box-shadow: 0 4px 20px rgba(0,0,0,0.6); `; - notification.textContent = message; - document.body.appendChild(notification); - setTimeout(() => notification.remove(), 4000); + notif.textContent = message; + document.body.appendChild(notif); + setTimeout(() => notif.remove(), 4500); }; +async function apiRequest(endpoint, options = {}) { + const token = getToken(); + if (!token) { + showLoginModal(); + return null; + } + + const res = await fetch(`${API_BASE}${endpoint}`, { + ...options, + headers: { + "X-API-KEY": token, + "Content-Type": "application/json", + ...options.headers + } + }); + + if (res.status === 401) { + localStorage.removeItem("access_token"); + showNotification("Сессия истекла. Войдите заново.", "error"); + showLoginModal(); + return null; + } + + return res; +} + +// Показать окно логина +function showLoginModal() { + document.getElementById("loginModal").classList.add("active"); + document.getElementById("mainContent").classList.add("hidden"); + document.getElementById("userInfo").style.display = "none"; +} + +// Скрыть окно логина и показать кабинет +function hideLoginModal() { + document.getElementById("loginModal").classList.remove("active"); + document.getElementById("mainContent").classList.remove("hidden"); + document.getElementById("userInfo").style.display = "flex"; +} + async function loadPanel() { const token = getToken(); if (!token) { - window.location.href = "register.html"; + showLoginModal(); return; } - try { - const res = await fetch(`${API_BASE}/config/get_info`, { - method: "GET", - headers: { "X-API-KEY": token } - }); + const res = await apiRequest("/config/get_info", { method: "GET" }); + if (!res) return; - if (res.status === 401) { - localStorage.removeItem("access_token"); - window.location.href = "register.html"; - return; - } - - if (!res.ok) throw new Error("Failed to load configs"); - - const data = await res.json(); - document.getElementById("userEmail").textContent = "Аккаунт активен"; - renderConfigs(data.configs || []); - - } catch (err) { - console.error(err); + if (!res.ok) { showNotification("Ошибка загрузки данных", "error"); + return; } + + const data = await res.json(); + + document.getElementById("userEmail").textContent = data.email || "Аккаунт активен"; + hideLoginModal(); // ← Важно! + renderConfigs(data.configs || []); } function renderConfigs(configs) { @@ -51,7 +82,7 @@ function renderConfigs(configs) { container.innerHTML = ""; if (configs.length === 0) { - container.innerHTML = `

+ container.innerHTML = `

У вас пока нет конфигураций.
Нажмите кнопку ниже, чтобы создать первую.

`; return; @@ -63,22 +94,20 @@ function renderConfigs(configs) { const card = document.createElement("div"); card.className = "url-card"; card.innerHTML = ` -
+
${cfg.name}
-
- Использовано: ${trafficGB} ГБ -
+
Использовано: ${trafficGB} ГБ
- - + +
`; card.querySelector(".btn-copy").addEventListener("click", async () => { try { await navigator.clipboard.writeText(cfg.config); - showNotification(`Конфиг "${cfg.name}" скопирован в буфер`); + showNotification(`Конфиг "${cfg.name}" скопирован`); } catch (e) { showNotification("Не удалось скопировать", "error"); } @@ -87,25 +116,16 @@ function renderConfigs(configs) { card.querySelector(".btn-delete").addEventListener("click", async () => { if (!confirm(`Удалить конфигурацию "${cfg.name}"?`)) return; - const token = getToken(); - try { - const res = await fetch(`${API_BASE}/config/delete`, { - method: "DELETE", - headers: { - "X-API-KEY": token, - "Content-Type": "application/json" - }, - body: JSON.stringify({ name: cfg.name }) - }); + const res = await apiRequest("/config/delete", { + method: "DELETE", + body: JSON.stringify({ name: cfg.name }) + }); - if (res.ok) { - card.remove(); - showNotification(`Конфиг "${cfg.name}" удалён`, "error"); - } else { - showNotification("Не удалось удалить конфиг", "error"); - } - } catch (e) { - showNotification("Ошибка сети", "error"); + if (res && res.ok) { + showNotification(`Конфиг "${cfg.name}" удалён`); + loadPanel(); + } else { + showNotification("Не удалось удалить конфиг", "error"); } }); @@ -113,61 +133,126 @@ function renderConfigs(configs) { }); } -document.getElementById("addConfigBtn").addEventListener("click", async () => { - const name = prompt("Введите название конфигурации (например: Телефон, Ноутбук, Рабочий):"); - if (!name || !name.trim()) return; +// ==================== ЛОГИН ==================== +document.getElementById("loginSubmit").addEventListener("click", async () => { + const email = document.getElementById("loginEmail").value.trim(); + const password = document.getElementById("loginPassword").value; + const errorEl = document.getElementById("loginError"); - const token = getToken(); + errorEl.textContent = ""; + + if (!email || !password) { + errorEl.textContent = "Введите email и пароль"; + return; + } try { - const res = await fetch(`${API_BASE}/config/add`, { + const res = await fetch(`${API_BASE}/user/login`, { method: "POST", - headers: { - "X-API-KEY": token, - "Content-Type": "application/json" - }, - body: JSON.stringify({ name: name.trim() }) + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }) }); const data = await res.json(); - if (res.ok) { - showNotification(`Конфиг "${name}" успешно создан!`); - loadPanel(); + if (res.ok && data.access_token) { + localStorage.setItem("access_token", data.access_token); + showNotification("Вход выполнен успешно!"); + document.getElementById("loginEmail").value = ""; + document.getElementById("loginPassword").value = ""; + loadPanel(); // ← Главное исправление } else { - showNotification(data.detail || "Ошибка при создании конфига", "error"); + errorEl.textContent = data.detail || "Неверный email или пароль"; } - } catch (e) { - showNotification("Нет связи с сервером", "error"); + } catch (err) { + console.error(err); + errorEl.textContent = "Нет связи с сервером. Попробуйте позже."; } }); -document.getElementById("deleteAccountBtn").addEventListener("click", async () => { - if (!confirm("Вы уверены, что хотите удалить аккаунт? Это действие необратимо!")) return; +// ==================== Создание конфига ==================== +document.getElementById("addConfigBtn").addEventListener("click", () => { + document.getElementById("addModal").classList.add("active"); + document.getElementById("configName").focus(); +}); + +document.getElementById("addCancel").addEventListener("click", () => { + document.getElementById("addModal").classList.remove("active"); + document.getElementById("addError").textContent = ""; +}); + +document.getElementById("addSubmit").addEventListener("click", async () => { + const name = document.getElementById("configName").value.trim(); + const errorEl = document.getElementById("addError"); + + if (!name) { + errorEl.textContent = "Введите название конфигурации"; + return; + } + + const res = await apiRequest("/config/add", { + method: "POST", + body: JSON.stringify({ name }) + }); + + if (res && res.ok) { + showNotification(`Конфигурация "${name}" создана!`); + document.getElementById("addModal").classList.remove("active"); + document.getElementById("configName").value = ""; + loadPanel(); + } else { + const errData = await res.json().catch(() => ({})); + errorEl.textContent = errData.detail || "Ошибка при создании"; + } +}); + +// ==================== Выход ==================== +document.getElementById("logoutBtn").addEventListener("click", async () => { + if (!confirm("Выйти из аккаунта?")) return; const token = getToken(); - try { - const res = await fetch(`${API_BASE}/user/delete`, { - method: "DELETE", - headers: { "X-API-KEY": token } - }); + if (token) { + await fetch(`${API_BASE}/user/logout`, { + method: "POST", + headers: { "X-API-KEY": token, "Content-Type": "application/json" }, + body: JSON.stringify({ token_to_revoke: token }) + }).catch(() => {}); + } - if (res.ok) { - localStorage.removeItem("access_token"); - alert("Аккаунт успешно удалён."); - window.location.href = "index.html"; - } else { - showNotification("Не удалось удалить аккаунт", "error"); - } - } catch (e) { - showNotification("Ошибка сети", "error"); + localStorage.removeItem("access_token"); + showLoginModal(); +}); + +// ==================== Удаление аккаунта ==================== +document.getElementById("deleteAccountBtn").addEventListener("click", async () => { + if (!confirm("Вы уверены? Это действие необратимо!")) return; + + const res = await apiRequest("/user/delete", { method: "DELETE" }); + + if (res && res.ok) { + localStorage.removeItem("access_token"); + alert("Аккаунт успешно удалён."); + showLoginModal(); + } else { + showNotification("Не удалось удалить аккаунт", "error"); } }); -document.getElementById("logoutBtn").addEventListener("click", () => { - if (confirm("Выйти из аккаунта?")) { - localStorage.removeItem("access_token"); - window.location.href = "index.html"; +// Табы "Как подключиться" +document.querySelectorAll(".tab-btn").forEach(btn => { + btn.addEventListener("click", () => { + document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active")); + btn.classList.add("active"); + + document.querySelectorAll(".instructions").forEach(instr => instr.classList.add("hidden")); + document.getElementById(btn.dataset.platform + "-instructions").classList.remove("hidden"); + }); +}); + +// Закрытие модалки создания конфига кликом вне +document.getElementById("addModal").addEventListener("click", (e) => { + if (e.target === document.getElementById("addModal")) { + document.getElementById("addModal").classList.remove("active"); } }); diff --git a/Frontend/Web/Styles/control-panel.css b/Frontend/Web/Styles/control-panel.css index 70d2f66..829b5a7 100644 --- a/Frontend/Web/Styles/control-panel.css +++ b/Frontend/Web/Styles/control-panel.css @@ -1,261 +1,294 @@ -:root{ +:root { --bg: #000; --card: #111; --text: #e0e0e0; - --accent: cyan; + --accent: #00ffff; --border: #333; + --danger: #ff4444; } -*{margin: 0; padding: 0; box-sizing: border-box;} -body{background: var(--bg); color: var(--text); font-family: 'Inter', sans-serif; min-height: 100vh;} +* { margin: 0; padding: 0; box-sizing: border-box; } +body { + background: var(--bg); + color: var(--text); + font-family: 'Inter', sans-serif; + min-height: 100vh; + line-height: 1.5; +} -.container{max-width: 800px; margin: 0 auto; padding: 20px;} +.container { max-width: 820px; margin: 0 auto; padding: 20px; } -header{ +header { display: flex; justify-content: space-between; align-items: center; padding: 20px 0; - border-bottom: 1px solid #333; + border-bottom: 1px solid var(--border); margin-bottom: 40px; flex-wrap: wrap; + gap: 15px; } -header h2 {font-size: 24pt;} -header h2 b {color: var(--accent);} +header h2 { font-size: 28px; } +header h2 b { color: var(--accent); } -.user-info{ +.user-info { display: flex; align-items: center; gap: 15px; - font-size: 14pt; + font-size: 15px; } -#logoutBtn{ +#logoutBtn { background: transparent; border: 1px solid var(--accent); color: var(--accent); - padding: 8px 16px; + padding: 8px 18px; border-radius: 8px; cursor: pointer; transition: 0.3s; } -#logoutBtn:hover{ +#logoutBtn:hover { background: var(--accent); color: #000; } -h1{ - font-size: 28pt; - margin-bottom: 30px; +h1 { + font-size: 26px; + margin-bottom: 25px; text-align: center; } -.urls-list{ +.urls-list { display: grid; - gap: 15px; + gap: 16px; margin-bottom: 30px; } -.url-card{ +.url-card { background: var(--card); - border: 1px solid #333; - border-radius: 12px; + border: 1px solid var(--border); + border-radius: 14px; padding: 20px; display: flex; justify-content: space-between; align-items: center; - transition: 0.3s; + transition: all 0.3s ease; } -.url-card:hover{ +.url-card:hover { border-color: var(--accent); - box-shadow: 0 0 15px rgba(0, 255, 255, 0.2); + box-shadow: 0 0 20px rgba(0, 255, 255, 0.15); } -.url-name{ +.url-info { + flex: 1; +} + +.url-name { font-weight: 600; color: var(--accent); + font-size: 17px; } -.url-actions{ +.traffic { + font-size: 13.5px; + color: #888; + margin-top: 4px; +} + +.url-actions { display: flex; gap: 10px; } -.btn-copy, .btn-delete{ - padding: 8px 16px; +.btn { + padding: 9px 18px; border-radius: 8px; cursor: pointer; font-weight: 600; transition: 0.3s; + border: none; } -.btn-copy{ +.btn-copy { background: transparent; border: 1px solid var(--accent); color: var(--accent); } -.btn-copy:hover{ +.btn-copy:hover { background: var(--accent); color: #000; } -.btn-delete{ - background: #330000; +.btn-delete { + background: #2a0a0a; border: 1px solid #800; - color: #f88; + color: #ff8888; } -.btn-delete:hover{ +.btn-delete:hover { background: #800; color: white; } -.add-btn{ +.add-btn { width: 100%; - padding: 16px; + padding: 18px; background: transparent; border: 2px dashed var(--accent); color: var(--accent); - font-size: 16pt; + font-size: 17px; + font-weight: 600; border-radius: 12px; cursor: pointer; - transition:hover{ - background: rgba(0, 255, 255, 0.1); - } + transition: 0.3s; } -.modal{ - display: none; - position: fixed; - top: 0; left: 0; width: 100%; height: 100%; - background: rgba(0,0,0,0.8); - justify-content: center; - align-items: center; - z-index: 1000; +.add-btn:hover { + background: rgba(0, 255, 255, 0.08); } -.modal.active{display: flex;} - -.modal-content{ - background: #111; - padding: 30px; - border-radius: 16px; - width: 90%; - max-width: 400px; - border: 1px solid var(--accent); +.danger-zone { + margin-top: 60px; text-align: center; } -.modal-content input{ - width: 100%; - padding: 14px; - margin: 10px 0; - background: #222; - border: 1px solid #444; +.delete-account-btn { + background: #3a0a0a; + color: var(--danger); + border: 1px solid #a00; + padding: 12px 28px; border-radius: 10px; + cursor: pointer; + font-size: 15px; +} + +.delete-account-btn:hover { + background: #600; color: white; } -.modal-content .error{ - color: #ff6b6b; - margin: 10px 0; - min-height: 20px; +/* Модальное окно */ +.modal { + display: none; + position: fixed; + inset: 0; + background: rgba(0,0,0,0.85); + justify-content: center; + align-items: center; + z-index: 2000; } -.buttons{ +.modal.active { display: flex; } + +.modal-content { + background: #111; + padding: 32px; + border-radius: 16px; + width: 90%; + max-width: 420px; + border: 1px solid var(--accent); +} + +.modal-content h2 { margin-bottom: 20px; text-align: center; } + +.modal-content input { + width: 100%; + padding: 14px; + margin-bottom: 15px; + background: #222; + border: 1px solid #555; + border-radius: 10px; + color: white; + font-size: 16px; +} + +.error { + color: #ff6b6b; + min-height: 22px; + font-size: 14px; + text-align: center; +} + +.buttons { display: flex; - gap: 10px; + gap: 12px; margin-top: 20px; } -.buttons button{ +.buttons button { flex: 1; - padding: 12px; + padding: 14px; border-radius: 10px; cursor: pointer; -} - -#loginSubmit{ - background: var(--accent); - color: black; - border: none; -} - -#closeModal{ - background: transparent; - border: 1px solid #666; - color: #aaa; -} - -.how-to-connect{ - margin-bottom: 60px; -} - -.platform-tabs{ - display: flex; - justify-content: center; - gap: 12px; - margin-bottom: 32px; - flex-wrap: wrap; -} - -.tab-btn{ - background: #1a1a1a; - border: 1px solid #444; - color: #aaa; - padding: 12px 24px; - border-radius: 50px; - font-size: 15px; - font-weight: 500; - cursor: pointer; - transition: all 0.22s ease; -} - -.tab-btn:hover{ - border-color: #666; - color: #ddd; - background: #222; -} - -.tab-btn.active{ - background: var(--accent); - color: #000; - border-color: var(--accent); - box-shadow: 0 0 20px rgba(0, 255, 255, 0.25); font-weight: 600; } -.instructions{ +#addSubmit { + background: var(--accent); + color: #000; + border: none; +} + +#addCancel { + background: transparent; + border: 1px solid #666; + color: #ccc; +} + +/* Табы и инструкции (оставил твои стили, только немного подчистил) */ +.platform-tabs { + display: flex; + justify-content: center; + gap: 12px; + margin-bottom: 30px; + flex-wrap: wrap; +} + +.tab-btn { + background: #1a1a1a; + border: 1px solid #444; + color: #aaa; + padding: 11px 22px; + border-radius: 50px; + cursor: pointer; + transition: all 0.25s; +} + +.tab-btn.active, +.tab-btn:hover { + background: var(--accent); + color: #000; + border-color: var(--accent); +} + +.instructions { background: var(--card); border: 1px solid #333; border-radius: 16px; - padding: 28px 32px; - transition: all 0.3s ease; + padding: 26px 30px; } -.instructions ol{ +.instructions ol { counter-reset: step; list-style: none; - font-size: 15.5px; - line-height: 1.65; } -.instructions li{ +.instructions li { position: relative; - padding-left: 38px; - margin-bottom: 18px; + padding-left: 42px; + margin-bottom: 16px; } -.instructions li::before{ +.instructions li::before { content: counter(step); counter-increment: step; position: absolute; left: 0; - top: 2px; + top: 0; width: 28px; height: 28px; background: #222; @@ -265,37 +298,7 @@ h1{ display: flex; align-items: center; justify-content: center; - font-size: 14px; - font-weight: 600; + font-weight: 700; } -.instructions a{ - color: var(--accent); - text-decoration: none; - border-bottom: 1px solid rgba(0,255,255,0.3); - transition: all 0.2s; -} - -.instructions a:hover{ - border-bottom-color: var(--accent); - color: #00ffff; -} - -.instructions kbd{ - background: #1e1e1e; - border: 1px solid #444; - border-radius: 5px; - padding: 3px 7px; - font-family: monospace; - color: #aaa; -} - -.instructions ul{ - margin: 12px 0 12px 24px; - list-style: disc; - color: #bbb; -} - -.hidden{ - display: none !important; -} \ No newline at end of file +.hidden { display: none !important; } \ No newline at end of file diff --git a/Frontend/Web/control-panel.html b/Frontend/Web/control-panel.html index 96f23f4..7a97c19 100644 --- a/Frontend/Web/control-panel.html +++ b/Frontend/Web/control-panel.html @@ -11,73 +11,87 @@

SpectralVPN

-
+
+

Как подключиться

-
- +
    -
  1. Создайте и скопируйте свою конфигурацию в личном кабинете.
  2. -
  3. Скачайте и установите приложение v2rayNG.
  4. -
  5. Откройте приложение.
  6. -
  7. Нажмите + в правом верхнем углу → Импорт из буфера обмена.
  8. -
  9. Готово! Включайте/выключайте соединение большой кнопкой внизу справа.
  10. +
  11. Создайте конфигурацию ниже и скопируйте ссылку.
  12. +
  13. Установите v2rayNG.
  14. +
  15. Нажмите + → Импорт из буфера обмена.
  16. +
  17. Включайте соединение большой кнопкой.
+

Ваши конфигурации

- + + +
+ +
-