From 4c4d325245594db96cf702b52472ce24fd37461e Mon Sep 17 00:00:00 2001 From: Lev Date: Sun, 19 Apr 2026 17:46:56 +0300 Subject: [PATCH] Unstable --- API/main.py | 1 + API/utils/xui.py | 3 +- Frontend/Dockerfile | 7 + Frontend/JS/control-panel.js | 225 ------------------ .../Nginx}/spectralvpn.ru.nginx | 2 +- .../Nginx}/spectralvpn.ru_http.nginx | 2 +- .../Nginx}/spectralvpn_api.nginx | 6 +- Frontend/Web/JS/control-panel.js | 174 ++++++++++++++ Frontend/{ => Web}/JS/register.js | 66 ++--- Frontend/{ => Web}/Styles/control-panel.css | 0 Frontend/{ => Web}/Styles/index.css | 0 Frontend/{ => Web}/Styles/offer.css | 0 Frontend/{ => Web}/Styles/register.css | 0 Frontend/{ => Web}/control-panel.html | 0 Frontend/{ => Web}/index.html | 0 Frontend/{ => Web}/offer.html | 0 Frontend/{ => Web}/privacy.html | 0 Frontend/{ => Web}/register.html | 3 + docker-compose.yml | 16 +- 19 files changed, 226 insertions(+), 279 deletions(-) create mode 100644 Frontend/Dockerfile delete mode 100644 Frontend/JS/control-panel.js rename {configs/nginx => Frontend/Nginx}/spectralvpn.ru.nginx (91%) rename {configs/nginx => Frontend/Nginx}/spectralvpn.ru_http.nginx (98%) rename {configs/nginx => Frontend/Nginx}/spectralvpn_api.nginx (91%) create mode 100644 Frontend/Web/JS/control-panel.js rename Frontend/{ => Web}/JS/register.js (51%) rename Frontend/{ => Web}/Styles/control-panel.css (100%) rename Frontend/{ => Web}/Styles/index.css (100%) rename Frontend/{ => Web}/Styles/offer.css (100%) rename Frontend/{ => Web}/Styles/register.css (100%) rename Frontend/{ => Web}/control-panel.html (100%) rename Frontend/{ => Web}/index.html (100%) rename Frontend/{ => Web}/offer.html (100%) rename Frontend/{ => Web}/privacy.html (100%) rename Frontend/{ => Web}/register.html (90%) diff --git a/API/main.py b/API/main.py index fc09e84..8d013fd 100644 --- a/API/main.py +++ b/API/main.py @@ -1,6 +1,7 @@ from datetime import datetime from contextlib import asynccontextmanager from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware from utils.database import engine import models from routers import server, user, config diff --git a/API/utils/xui.py b/API/utils/xui.py index 4817b99..77628ec 100644 --- a/API/utils/xui.py +++ b/API/utils/xui.py @@ -96,8 +96,7 @@ class XUIClient: @classmethod async def from_server(cls, server): - #TODO заменить на https - base_url = f"http://{server.host}:{server.port}" + base_url = f"https://{server.host}:{server.port}" return cls(server.host, base_url, server.user, server.password, server.inbound_id, server.version) async def _login(self): diff --git a/Frontend/Dockerfile b/Frontend/Dockerfile new file mode 100644 index 0000000..12f1639 --- /dev/null +++ b/Frontend/Dockerfile @@ -0,0 +1,7 @@ +FROM nginx:stable-alpine +COPY Web/ /usr/share/nginx/html/ +COPY Nginx/spectralvpn.ru.nginx /etc/nginx/conf.d/spectralvpn.ru.conf +COPY Nginx/spectralvpn.ru_http.nginx /etc/nginx/conf.d/spectralvpn.ru_http.conf +COPY Nginx/spectralvpn_api.nginx /etc/nginx/conf.d/spectralvpn_api.conf +EXPOSE 80 443 +CMD ["nginx", "-g", "daemon off;"] diff --git a/Frontend/JS/control-panel.js b/Frontend/JS/control-panel.js deleted file mode 100644 index 92f382d..0000000 --- a/Frontend/JS/control-panel.js +++ /dev/null @@ -1,225 +0,0 @@ -const API_BASE = "https://spectralvpn.ru:8500"; - -const getCookie = (name) => { - const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); - return match ? decodeURIComponent(match[2]) : null; -}; - -const deleteCookie = (name) => { - document.cookie = `${name}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; SameSite=Strict`; -}; - -const sha256 = async (str) => { - const buf = new TextEncoder().encode(str); - const hash = await crypto.subtle.digest("SHA-256", buf); - return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, "0")).join(""); -}; - -const showNotification = (text, color = "cyan") => { - const notification = document.createElement("div"); - notification.textContent = text; - notification.style.cssText = ` - position: fixed; top: 20px; right: 20px; padding: 15px 25px; border-radius: 12px; - z-index: 9999; font-weight: 600; background: ${color === "cyan" ? "#00ffff22" : "#ff444422"}; - color: ${color}; border: 1px solid ${color === "cyan" ? "cyan" : "#f66"}; - `; - document.body.appendChild(notification); - setTimeout(() => notification.remove(), 3000); -}; - -let currentEmail = null; -let currentHash = null; - -const modal = document.getElementById("loginModal"); -const showModal = () => modal.classList.add("active"); -const hideModal = () => modal.classList.remove("active"); - -const showLoginError = (msg) => { - document.getElementById("loginError").textContent = msg; -}; - -const renderUrls = (urls) => { - const container = document.getElementById("urlsList"); - container.innerHTML = ""; - - if (urls.length === 0) { - container.innerHTML = "

У вас пока нет конфигураций

"; - return; - } - - urls.forEach(name => { - const card = document.createElement("div"); - card.className = "url-card"; - - card.innerHTML = ` -
-
${name}
-
-
- - -
- `; - - card.querySelector(".btn-copy").onclick = async () => { - try { - const url = `${API_BASE}/get_url?email=${encodeURIComponent(currentEmail)}&password=${encodeURIComponent(currentHash)}&urls_name=${encodeURIComponent(name)}`; - const res = await fetch(url); - const data = await res.json(); - - if (res.ok && data.url) { - await navigator.clipboard.writeText(data.url); - showNotification(`Конфиг "${name}" скопирован!`); - } else { - showNotification("Ошибка получения конфига", "red"); - } - } catch (e) { - showNotification("Нет связи с сервером", "red"); - } - }; - - card.querySelector(".btn-delete").onclick = async () => { - if (!confirm(`Удалить конфиг "${name}"?`)) return; - - try { - const url = `${API_BASE}/del_url?email=${encodeURIComponent(currentEmail)}&password=${encodeURIComponent(currentHash)}&urls_name=${encodeURIComponent(name)}`; - const res = await fetch(url, { method: "DELETE" }); - - if (res.ok) { - card.remove(); - showNotification(`Конфиг "${name}" удалён`, "red"); - } else { - showNotification("Не удалось удалить", "red"); - } - } catch (e) { - showNotification("Ошибка сети", "red"); - } - }; - - container.appendChild(card); - }); -}; - -const tryAutoLogin = async () => { - currentEmail = getCookie("user_email"); - currentHash = getCookie("user_hash"); - - if (!currentEmail || !currentHash) { - showModal(); - return; - } - - try { - const res = await fetch(`${API_BASE}/login`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email: currentEmail, password: currentHash }) - }); - - if (res.ok) { - const data = await res.json(); - document.getElementById("userEmail").textContent = currentEmail; - document.getElementById("mainContent").classList.remove("hidden"); - renderUrls(data.urls || []); - } else { - deleteCookie("user_email"); - deleteCookie("user_hash"); - showModal(); - } - } catch (e) { - showModal(); - } -}; - -document.getElementById("loginSubmit").onclick = async () => { - const email = document.getElementById("loginEmail").value.trim(); - const pass = document.getElementById("loginPassword").value; - - if (!email || !pass) { - showLoginError("Заполните все поля"); - return; - } - - const hash = await sha256(pass); - - try { - const res = await fetch(`${API_BASE}/login`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email, password: hash }) - }); - - const data = await res.json(); - - if (res.ok) { - currentEmail = email; - currentHash = hash; - document.cookie = `user_email=${encodeURIComponent(email)}; path=/; max-age=2592000; Secure; SameSite=Strict`; - document.cookie = `user_hash=${hash}; path=/; max-age=2592000; Secure; SameSite=Strict`; - - document.getElementById("userEmail").textContent = email; - hideModal(); - document.getElementById("mainContent").classList.remove("hidden"); - renderUrls(data.urls || []); - } else { - showLoginError("Неверный email или пароль"); - } - } catch (e) { - showLoginError("Ошибка подключения"); - } -}; - -document.getElementById("addUrlBtn").onclick = async () => { - const name = prompt("Введите название конфигурации:"); - if (!name?.trim()) return; - - try { - const res = await fetch(`${API_BASE}/add_url`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - email: currentEmail, - password: currentHash, - urls_name: name.trim() - }) - }); - - const data = await res.json(); - - if (res.ok) { - renderUrls(data.urls); - showNotification(`Конфиг "${name}" создан! Скопируйте через кнопку.`); - } else { - showNotification("Ошибка: " + (data.detail || "неизвестно"), "red"); - } - } catch (e) { - showNotification("Нет связи с сервером", "red"); - } -}; - -document.getElementById("logoutBtn").onclick = () => { - deleteCookie("user_email"); - deleteCookie("user_hash"); - location.reload(); -}; - -document.getElementById("closeModal").onclick = hideModal; - -tryAutoLogin(); - -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(el => el.classList.add('hidden')); - - const platform = btn.dataset.platform; - const target = document.getElementById(`${platform}-instructions`); - if (target) { - target.classList.remove('hidden'); - } - }); -}); - -document.addEventListener('DOMContentLoaded', () => {}); \ No newline at end of file diff --git a/configs/nginx/spectralvpn.ru.nginx b/Frontend/Nginx/spectralvpn.ru.nginx similarity index 91% rename from configs/nginx/spectralvpn.ru.nginx rename to Frontend/Nginx/spectralvpn.ru.nginx index 69a618b..75259b3 100644 --- a/configs/nginx/spectralvpn.ru.nginx +++ b/Frontend/Nginx/spectralvpn.ru.nginx @@ -10,7 +10,7 @@ server { gzip_types text/css text/javascript text/plain application/javascript application/json; gzip_min_length 1000; - root /var/www/html/spectralvpn.ru; + root /usr/share/nginx/html/; index index.html; location / { diff --git a/configs/nginx/spectralvpn.ru_http.nginx b/Frontend/Nginx/spectralvpn.ru_http.nginx similarity index 98% rename from configs/nginx/spectralvpn.ru_http.nginx rename to Frontend/Nginx/spectralvpn.ru_http.nginx index 304a955..4203451 100644 --- a/configs/nginx/spectralvpn.ru_http.nginx +++ b/Frontend/Nginx/spectralvpn.ru_http.nginx @@ -3,4 +3,4 @@ server { listen [::]:80; server_name spectralvpn.ru; return 301 https://$host$request_uri; -} \ No newline at end of file +} diff --git a/configs/nginx/spectralvpn_api.nginx b/Frontend/Nginx/spectralvpn_api.nginx similarity index 91% rename from configs/nginx/spectralvpn_api.nginx rename to Frontend/Nginx/spectralvpn_api.nginx index e2126fd..3abfedb 100644 --- a/configs/nginx/spectralvpn_api.nginx +++ b/Frontend/Nginx/spectralvpn_api.nginx @@ -1,6 +1,6 @@ server { - listen 8500 ssl; - listen [::]:8500 ssl; + listen 8000 ssl; + listen [::]:8000 ssl; server_name spectralvpn.ru; ssl_certificate /etc/letsencrypt/live/spectralvpn.ru/fullchain.pem; @@ -15,7 +15,7 @@ server { return 204; } - proxy_pass http://127.0.0.1:8000; + 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; diff --git a/Frontend/Web/JS/control-panel.js b/Frontend/Web/JS/control-panel.js new file mode 100644 index 0000000..4f072f4 --- /dev/null +++ b/Frontend/Web/JS/control-panel.js @@ -0,0 +1,174 @@ +const API_BASE = "https://spectralvpn.ru:8000"; + +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); + `; + notification.textContent = message; + document.body.appendChild(notification); + setTimeout(() => notification.remove(), 4000); +}; + +async function loadPanel() { + const token = getToken(); + if (!token) { + window.location.href = "register.html"; + return; + } + + try { + const res = await fetch(`${API_BASE}/config/get_info`, { + method: "GET", + headers: { "X-API-KEY": token } + }); + + 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); + showNotification("Ошибка загрузки данных", "error"); + } +} + +function renderConfigs(configs) { + const container = document.getElementById("urlsList"); + container.innerHTML = ""; + + if (configs.length === 0) { + container.innerHTML = `

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

`; + return; + } + + configs.forEach(cfg => { + const trafficGB = (cfg.bytes_used / (1024 ** 3)).toFixed(2); + + const card = document.createElement("div"); + card.className = "url-card"; + card.innerHTML = ` +
+
${cfg.name}
+
+ Использовано: ${trafficGB} ГБ +
+
+
+ + +
+ `; + + card.querySelector(".btn-copy").addEventListener("click", async () => { + try { + await navigator.clipboard.writeText(cfg.config); + showNotification(`Конфиг "${cfg.name}" скопирован в буфер`); + } catch (e) { + showNotification("Не удалось скопировать", "error"); + } + }); + + 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 }) + }); + + if (res.ok) { + card.remove(); + showNotification(`Конфиг "${cfg.name}" удалён`, "error"); + } else { + showNotification("Не удалось удалить конфиг", "error"); + } + } catch (e) { + showNotification("Ошибка сети", "error"); + } + }); + + container.appendChild(card); + }); +} + +document.getElementById("addConfigBtn").addEventListener("click", async () => { + const name = prompt("Введите название конфигурации (например: Телефон, Ноутбук, Рабочий):"); + if (!name || !name.trim()) return; + + const token = getToken(); + + try { + const res = await fetch(`${API_BASE}/config/add`, { + method: "POST", + headers: { + "X-API-KEY": token, + "Content-Type": "application/json" + }, + body: JSON.stringify({ name: name.trim() }) + }); + + const data = await res.json(); + + if (res.ok) { + showNotification(`Конфиг "${name}" успешно создан!`); + loadPanel(); + } else { + showNotification(data.detail || "Ошибка при создании конфига", "error"); + } + } catch (e) { + showNotification("Нет связи с сервером", "error"); + } +}); + +document.getElementById("deleteAccountBtn").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 (res.ok) { + localStorage.removeItem("access_token"); + alert("Аккаунт успешно удалён."); + window.location.href = "index.html"; + } else { + showNotification("Не удалось удалить аккаунт", "error"); + } + } catch (e) { + showNotification("Ошибка сети", "error"); + } +}); + +document.getElementById("logoutBtn").addEventListener("click", () => { + if (confirm("Выйти из аккаунта?")) { + localStorage.removeItem("access_token"); + window.location.href = "index.html"; + } +}); + +document.addEventListener("DOMContentLoaded", loadPanel); \ No newline at end of file diff --git a/Frontend/JS/register.js b/Frontend/Web/JS/register.js similarity index 51% rename from Frontend/JS/register.js rename to Frontend/Web/JS/register.js index acb49ff..017110d 100644 --- a/Frontend/JS/register.js +++ b/Frontend/Web/JS/register.js @@ -4,26 +4,20 @@ document.addEventListener("DOMContentLoaded", () => { const setError = (fieldId, message) => { const errorEl = document.getElementById(fieldId); - const inputEl = errorEl.previousElementSibling; - errorEl.textContent = message; - errorEl.classList.add("active"); - if (inputEl) inputEl.classList.add("invalid"); + if (errorEl) { + errorEl.textContent = message; + errorEl.classList.add("active"); + } + const input = document.getElementById(fieldId.replace("Error", "")); + if (input) input.classList.add("invalid"); }; const clearErrors = () => { - form.querySelectorAll(".error").forEach(el => { + document.querySelectorAll(".error").forEach(el => { el.textContent = ""; el.classList.remove("active"); }); - form.querySelectorAll(".invalid").forEach(el => el.classList.remove("invalid")); - document.querySelector(".checkbox-container")?.classList.remove("invalid"); - }; - - const sha256 = async (message) => { - const msgBuffer = new TextEncoder().encode(message); - const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - return hashArray.map(b => b.toString(16).padStart(2, "0")).join(""); + document.querySelectorAll(".input").forEach(el => el.classList.remove("invalid")); }; form.addEventListener("submit", async (e) => { @@ -33,59 +27,49 @@ document.addEventListener("DOMContentLoaded", () => { const email = document.getElementById("email").value.trim(); const password = document.getElementById("password").value; const passwordReply = document.getElementById("password_reply").value; - const terms = document.getElementById("terms").checked; - - let hasError = false; + const promo_code = document.getElementById("promo_code") ? document.getElementById("promo_code").value.trim() : ""; if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { setError("emailError", "Введите корректный email"); - hasError = true; + return; } - if (!password || password.length < 6 || !/(?=.*[A-Za-z])(?=.*\d)/.test(password)) { - setError("passwordError", "Минимум 6 символов, буквы + цифры"); - hasError = true; + if (!password || password.length < 6) { + setError("passwordError", "Пароль минимум 6 символов"); + return; } if (password !== passwordReply) { setError("passwordReplyError", "Пароли не совпадают"); - hasError = true; + return; } - if (!terms) { - setError("termsError", "Принять условия обязательно"); - document.querySelector(".checkbox-container").classList.add("invalid"); - hasError = true; - } - - if (hasError) return; submitButton.disabled = true; submitButton.textContent = "Создаём аккаунт..."; try { - const passwordHash = await sha256(password); - - const response = await fetch("https://spectralvpn.ru:8500/registration", { + const response = await fetch("https://spectralvpn.ru:8000/user/signup", { method: "POST", - headers: { - "Content-Type": "application/json", - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: email, - password: passwordHash + password: password, + promo_code: promo_code }) }); const result = await response.json(); if (response.ok) { - Cookies.set("user_email", email, { expires: 30, sameSite: "strict" }); - Cookies.set("user_hash", passwordHash, { expires: 30, sameSite: "strict" }); + // Сохраняем токен + localStorage.setItem("access_token", result.access_token); window.location.href = "control-panel.html"; } else { - if (result.detail === "Email is busy.") { - setError("emailError", "Этот email уже зарегистрирован"); + if (result.detail?.includes("Promo-code")) { + setError("promoError", "Неверный промокод"); + } else if (result.detail?.includes("Email") || result.detail?.includes("busy")) { + setError("emailError", "Этот email уже используется"); } else { - setError("emailError", result.detail || "Ошибка сервера"); + setError("emailError", result.detail || "Ошибка регистрации"); } } } catch (err) { diff --git a/Frontend/Styles/control-panel.css b/Frontend/Web/Styles/control-panel.css similarity index 100% rename from Frontend/Styles/control-panel.css rename to Frontend/Web/Styles/control-panel.css diff --git a/Frontend/Styles/index.css b/Frontend/Web/Styles/index.css similarity index 100% rename from Frontend/Styles/index.css rename to Frontend/Web/Styles/index.css diff --git a/Frontend/Styles/offer.css b/Frontend/Web/Styles/offer.css similarity index 100% rename from Frontend/Styles/offer.css rename to Frontend/Web/Styles/offer.css diff --git a/Frontend/Styles/register.css b/Frontend/Web/Styles/register.css similarity index 100% rename from Frontend/Styles/register.css rename to Frontend/Web/Styles/register.css diff --git a/Frontend/control-panel.html b/Frontend/Web/control-panel.html similarity index 100% rename from Frontend/control-panel.html rename to Frontend/Web/control-panel.html diff --git a/Frontend/index.html b/Frontend/Web/index.html similarity index 100% rename from Frontend/index.html rename to Frontend/Web/index.html diff --git a/Frontend/offer.html b/Frontend/Web/offer.html similarity index 100% rename from Frontend/offer.html rename to Frontend/Web/offer.html diff --git a/Frontend/privacy.html b/Frontend/Web/privacy.html similarity index 100% rename from Frontend/privacy.html rename to Frontend/Web/privacy.html diff --git a/Frontend/register.html b/Frontend/Web/register.html similarity index 90% rename from Frontend/register.html rename to Frontend/Web/register.html index 0a350af..7d3dc13 100644 --- a/Frontend/register.html +++ b/Frontend/Web/register.html @@ -21,6 +21,9 @@
+ + +
diff --git a/docker-compose.yml b/docker-compose.yml index 617d798..c2c5836 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,6 @@ services: api: - build: - context: ./API - dockerfile: Dockerfile + build: ./API container_name: spectralvpn-api restart: unless-stopped depends_on: @@ -16,9 +14,15 @@ services: - host.docker.internal:172.17.0.1 ports: - 8000:8000 - - #web: - # Todo + web: + build: ./Frontend + container_name: spectralvpn-web + restart: unless-stopped + depends_on: + - api + ports: + - 80:80 + - 443:443 db: image: postgres:latest container_name: spectralvpn-db