control-panel

This commit is contained in:
Lev 2025-12-09 22:17:47 +03:00
parent 0e20a01a5b
commit 91443a777f
4 changed files with 435 additions and 8 deletions

View file

@ -0,0 +1,208 @@
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 = "<p style='text-align:center; color:#666;'>У вас пока нет конфигураций</p>";
return;
}
urls.forEach(name => {
const card = document.createElement("div");
card.className = "url-card";
card.innerHTML = `
<div>
<div class="url-name">${name}</div>
</div>
<div class="url-actions">
<button class="btn-copy" data-name="${name}">Скопировать</button>
<button class="btn-delete" data-name="${name}">Удалить</button>
</div>
`;
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();

View file

@ -1,4 +1,193 @@
*{
margin: 0;
padding: 0;
:root{
--bg: #000;
--card: #111;
--text: #e0e0e0;
--accent: cyan;
--border: #333;
}
*{margin: 0; padding: 0; box-sizing: border-box;}
body{background: var(--bg); color: var(--text); font-family: 'Inter', sans-serif; min-height: 100vh;}
.container{max-width: 800px; margin: 0 auto; padding: 20px;}
header{
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
border-bottom: 1px solid #333;
margin-bottom: 40px;
}
header h2 {font-size: 24pt;}
header h2 b {color: var(--accent);}
.user-info{
display: flex;
align-items: center;
gap: 15px;
font-size: 14pt;
}
#logoutBtn{
background: transparent;
border: 1px solid var(--accent);
color: var(--accent);
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
transition: 0.3s;
}
#logoutBtn:hover{
background: var(--accent);
color: #000;
}
h1{
font-size: 28pt;
margin-bottom: 30px;
text-align: center;
}
.urls-list{
display: grid;
gap: 15px;
margin-bottom: 30px;
}
.url-card{
background: var(--card);
border: 1px solid #333;
border-radius: 12px;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
transition: 0.3s;
}
.url-card:hover{
border-color: var(--accent);
box-shadow: 0 0 15px rgba(0, 255, 255, 0.2);
}
.url-name{
font-weight: 600;
color: var(--accent);
}
.url-actions{
display: flex;
gap: 10px;
}
.btn-copy, .btn-delete{
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: 0.3s;
}
.btn-copy{
background: transparent;
border: 1px solid var(--accent);
color: var(--accent);
}
.btn-copy:hover{
background: var(--accent);
color: #000;
}
.btn-delete{
background: #330000;
border: 1px solid #800;
color: #f88;
}
.btn-delete:hover{
background: #800;
color: white;
}
.add-btn{
width: 100%;
padding: 16px;
background: transparent;
border: 2px dashed var(--accent);
color: var(--accent);
font-size: 16pt;
border-radius: 12px;
cursor: pointer;
transition:hover{
background: rgba(0, 255, 255, 0.1);
}
}
.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;
}
.modal.active{display: flex;}
.modal-content{
background: #111;
padding: 30px;
border-radius: 16px;
width: 90%;
max-width: 400px;
border: 1px solid var(--accent);
text-align: center;
}
.modal-content input{
width: 100%;
padding: 14px;
margin: 10px 0;
background: #222;
border: 1px solid #444;
border-radius: 10px;
color: white;
}
.modal-content .error{
color: #ff6b6b;
margin: 10px 0;
min-height: 20px;
}
.buttons{
display: flex;
gap: 10px;
margin-top: 20px;
}
.buttons button{
flex: 1;
padding: 12px;
border-radius: 10px;
cursor: pointer;
}
#loginSubmit{
background: var(--accent);
color: black;
border: none;
}
#closeModal{
background: transparent;
border: 1px solid #666;
color: #aaa;
}
.hidden { display: none; }

View file

@ -1,12 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<title>Личный кабинет — SpectralVPN</title>
<link rel="stylesheet" href="Styles/control-panel.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<div class="container">
<header>
<h2><b>Spectral</b>VPN</h2>
<div class="user-info" id="userInfo">
<span id="userEmail"></span>
<button id="logoutBtn">Выйти</button>
</div>
</header>
<main id="mainContent" class="hidden">
<h1>Ваши конфигурации</h1>
<div class="urls-list" id="urlsList"></div>
<button class="add-btn" id="addUrlBtn">+ Добавить конфиг</button>
</main>
<div class="modal" id="loginModal">
<div class="modal-content">
<h2>Вход в аккаунт</h2>
<input type="email" id="loginEmail" placeholder="Email" required>
<input type="password" id="loginPassword" placeholder="Пароль" required>
<div class="error" id="loginError"></div>
<div class="buttons">
<button id="loginSubmit">Войти</button>
<button id="closeModal">Отмена</button>
</div>
</div>
</div>
</div>
<script src="JS/control-panel.js"></script>
</body>
</html>

View file

@ -9,7 +9,7 @@ server {
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://spectralvpn.ru';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
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;
return 204;