This commit is contained in:
Lev 2026-04-19 17:46:56 +03:00
parent 11a84f4926
commit 4c4d325245
19 changed files with 226 additions and 279 deletions

View file

@ -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 = `<p style="text-align:center; color:#666; padding:40px 20px;">
У вас пока нет конфигураций.<br>Нажмите кнопку ниже, чтобы создать первую.
</p>`;
return;
}
configs.forEach(cfg => {
const trafficGB = (cfg.bytes_used / (1024 ** 3)).toFixed(2);
const card = document.createElement("div");
card.className = "url-card";
card.innerHTML = `
<div>
<div class="url-name">${cfg.name}</div>
<div style="font-size:13px; color:#888; margin-top:6px;">
Использовано: ${trafficGB} ГБ
</div>
</div>
<div class="url-actions">
<button class="btn-copy" data-config="${cfg.config}">Скопировать ссылку</button>
<button class="btn-delete" data-name="${cfg.name}">Удалить</button>
</div>
`;
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);

View file

@ -0,0 +1,83 @@
document.addEventListener("DOMContentLoaded", () => {
const form = document.getElementById("registerForm");
const submitButton = form.querySelector("button[type='submit']");
const setError = (fieldId, message) => {
const errorEl = document.getElementById(fieldId);
if (errorEl) {
errorEl.textContent = message;
errorEl.classList.add("active");
}
const input = document.getElementById(fieldId.replace("Error", ""));
if (input) input.classList.add("invalid");
};
const clearErrors = () => {
document.querySelectorAll(".error").forEach(el => {
el.textContent = "";
el.classList.remove("active");
});
document.querySelectorAll(".input").forEach(el => el.classList.remove("invalid"));
};
form.addEventListener("submit", async (e) => {
e.preventDefault();
clearErrors();
const email = document.getElementById("email").value.trim();
const password = document.getElementById("password").value;
const passwordReply = document.getElementById("password_reply").value;
const promo_code = document.getElementById("promo_code") ? document.getElementById("promo_code").value.trim() : "";
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
setError("emailError", "Введите корректный email");
return;
}
if (!password || password.length < 6) {
setError("passwordError", "Пароль минимум 6 символов");
return;
}
if (password !== passwordReply) {
setError("passwordReplyError", "Пароли не совпадают");
return;
}
submitButton.disabled = true;
submitButton.textContent = "Создаём аккаунт...";
try {
const response = await fetch("https://spectralvpn.ru:8000/user/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: email,
password: password,
promo_code: promo_code
})
});
const result = await response.json();
if (response.ok) {
// Сохраняем токен
localStorage.setItem("access_token", result.access_token);
window.location.href = "control-panel.html";
} else {
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 || "Ошибка регистрации");
}
}
} catch (err) {
console.error(err);
setError("emailError", "Нет связи с сервером");
} finally {
submitButton.disabled = false;
submitButton.textContent = "Зарегистрироваться";
}
});
});

View file

@ -0,0 +1,301 @@
: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;
flex-wrap: wrap;
}
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;
}
.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{
background: var(--card);
border: 1px solid #333;
border-radius: 16px;
padding: 28px 32px;
transition: all 0.3s ease;
}
.instructions ol{
counter-reset: step;
list-style: none;
font-size: 15.5px;
line-height: 1.65;
}
.instructions li{
position: relative;
padding-left: 38px;
margin-bottom: 18px;
}
.instructions li::before{
content: counter(step);
counter-increment: step;
position: absolute;
left: 0;
top: 2px;
width: 28px;
height: 28px;
background: #222;
border: 1px solid var(--accent);
color: var(--accent);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
}
.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;
}

View file

@ -0,0 +1,402 @@
*{
margin: 0;
padding: 0;
}
body{
background-color: black;
color: white;
font-family: 'Inter';
}
header{
height: 5vh;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
border: 0;
border-bottom: 1px;
border-style: solid;
border-color: rgb(41, 41, 41);
}
header h2{
margin-left: 2vw;
}
h2 b{
color: aqua;
}
header div button{
height: 4vh;
font-size: 14pt;
border-width: 0;
font-weight: 600;
padding-left: 10px;
padding-right: 10px;
border-radius: 5px;
cursor: pointer;
}
#register{
background-color: black;
color: white;
border-color: aqua;
border-width: 1px;
margin-right: 10px;
transition: 0.1s;
}
#register:hover{
color: black;
background-color: aqua;
}
#register:active{
opacity: 0.7;
}
#login{
background-color: aqua;
margin-right: 2vw;
}
#login:hover{
background-color: black;
border-color: aqua;
border-style: solid;
border-width: 1px;
color: white;
}
#login:active{
opacity: 0.7;
}
#main{
display: flex;
flex-direction: column;
align-items: center;
min-height: 50vh;
}
#main h1{
font-size: 25pt;
text-align: center;
margin-top: 10vh;
line-height: 35pt;
}
#main h4{
margin-top: 1vh;
font-size: 13pt;
font-weight: 500;
max-width: 550px;
text-align: center;
color: lightgrey;
line-height: 20pt;
}
#main h4 b{
font-weight: 700;
}
#container{
display: flex;
margin-top: 5vh;
flex-wrap: wrap;
justify-content: center;
}
#container div{
height: 4vh;
width: 100px;
background-color: rgba(10, 117, 136, 0.575);
border-radius: 7px;
border-width: 1px;
border-color: aqua;
border-style: solid;
margin: 5px;
color: aqua;
display: flex;
justify-content: center;
align-items: center;
font-size: 13pt;
font-weight: 700;
flex-wrap: wrap;
}
#main button{
margin-top: 3vh;
height: 5vh;
padding-left: 20px;
padding-right: 20px;
background-color: aqua;
border-radius: 7px;
border-style: none;
font-size: 14pt;
font-weight: 700;
cursor: pointer;
transition: 0.1s;
margin-bottom: 5vh;
}
#main button:hover{
background-color: black;
border-color: aqua;
border-style: solid;
border-width: 1px;
color: white;
}
#main button:active{
opacity: 0.7;
}
#info{
background-color: rgb(7, 7, 7);
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
padding: 5vh;
min-height: 30vh;
align-items: center;
border: 0;
border-top: 1px;
border-bottom: 1px;
border-style: solid;
border-color: rgb(27, 27, 27);
}
#info div{
margin: 5vh;
display: flex;
flex-direction: column;
align-items: center;
}
#info div h1{
font-weight: 300;
font-size: 25pt;
color: lightgray;
}
#info div h3{
font-size: 14pt;
margin-top: 1vh;
}
#info div p{
width: 250px;
text-align: center;
color: gray;
margin-top: 1vh;
}
#final{
display: flex;
flex-direction: column;
align-items: center;
padding: 5vh;
}
#final h2{
font-size: 25pt;
}
#final h3{
font-weight: 300;
color:gray;
margin-top: 2vh;
max-width: 500px;
text-align: center;
line-height: 17pt;
}
#final button{
color: white;
background-color: black;
border-color: aqua;
border-style: solid;
border-radius: 7px;
padding-left: 20px;
padding-right: 20px;
height: 5vh;
font-size: 15pt;
font-weight: 700;
margin-top: 3vh;
cursor: pointer;
transition: 0.1s;
}
#final button:hover{
color: black;
background-color: aqua;
}
#final button:active{
opacity: 0.7;
}
footer{
height: 5vh;
width: 100%;
display: flex;
align-items: center;
justify-content: space-around;
border: 0;
border-top: 1px;
border-style: solid;
border-color: rgb(41, 41, 41);
}
footer a{
color: darkgray;
}
footer a:active{
color: rgb(109, 109, 109);
}
footer p{
color: darkgray;
}
@media (width < 600px) {
header {
height: 10vh;
padding: 0 15px;
flex-wrap: wrap;
width: 91vw;
}
header h2 {
margin-left: 0;
font-size: 18pt;
}
header div {
display: flex;
gap: 8px;
}
header div button {
font-size: 12pt;
height: 4.5vh;
padding: 0 12px;
}
#register {
margin-right: 0;
}
#login {
margin-right: 0;
}
/* Main Section */
#main h1 {
font-size: 20pt;
line-height: 28pt;
margin-top: 8vh;
padding: 0 20px;
}
#main h4 {
font-size: 12pt;
line-height: 18pt;
padding: 0 20px;
max-width: 90%;
}
#container {
flex-wrap: wrap;
justify-content: center;
gap: 10px;
margin-top: 4vh;
padding: 0 20px;
}
#container div {
width: 45%;
min-width: 90px;
height: 5.5vh;
font-size: 11pt;
margin: 0;
}
#main button {
margin-top: 4vh;
height: 6vh;
font-size: 13pt;
width: 80%;
max-width: 300px;
}
/* Info Section */
#info {
flex-direction: column;
padding: 4vh 0;
gap: 3vh;
}
#info div {
margin: 0;
width: 80%;
max-width: 300px;
}
#info div h1 {
font-size: 22pt;
}
#info div h3 {
font-size: 13pt;
}
#info div p {
width: 100%;
font-size: 12pt;
line-height: 16pt;
}
/* Final Section */
#final {
padding: 6vh 20px;
}
#final h2 {
font-size: 20pt;
text-align: center;
}
#final h3 {
width: 100%;
font-size: 12pt;
line-height: 18pt;
margin-top: 2vh;
}
#final button {
width: 80%;
max-width: 300px;
height: 6vh;
font-size: 13pt;
margin-top: 4vh;
}
/* Footer */
footer {
flex-direction: column;
height: auto;
padding: 15px 0;
text-align: center;
gap: 8px;
}
footer a, footer p {
font-size: 11pt;
}
}

View file

@ -0,0 +1,108 @@
:root{
--bg: #0f0f1a;
--text: #e0e0ff;
--accent: cyan;
--card: #1a1a2e;
--border: #333355;
}
*{
box-sizing: border-box; margin:0; padding:0;
}
body{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.7;
}
.container{
max-width: 900px;
margin: 40px auto;
padding: 30px;
background: var(--card);
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
border: 1px solid var(--border);
}
header{
text-align: center;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border);
}
h1{
font-size: 2.2rem; color: var(--accent); margin:0;
}
h2{
font-size: 1.5rem; color: var(--accent); margin-top: 2rem; border-left: 4px solid var(--accent); padding-left: 12px;
}
h3{
font-size: 1.2rem; color: #c0c0ff; margin-top: 1.5rem;
}
p, li{
font-size: 1rem; margin: 0.8rem 0;
}
ul, ol{
padding-left: 20px;
}
code{
background: #2a2a3a;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
}
.highlight{
background: rgba(110, 86, 207, 0.15);
padding: 12px;
border-radius: 8px;
border-left: 4px solid var(--accent);
margin: 1rem 0;
}
.footer{
margin-top: 50px;
text-align: center;
font-size: 0.9rem;
color: #888;
padding-top: 20px;
border-top: 1px solid var(--border);
}
.btn{
display: inline-block;
margin: 15px 10px 0 0;
padding: 10px 20px;
background: var(--accent);
color: var(--card);
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: 0.3s;
}
.btn:hover{
background: #5a45b0; transform: translateY(-2px);
}
a{
color: var(--accent); text-decoration: none;
}
a:hover{
text-decoration: underline;
}
@media (max-width: 768px) {
.container { margin: 20px; padding: 20px; }
h1 { font-size: 1.8rem; }
}

View file

@ -0,0 +1,156 @@
:root{
--accent: cyan;
--bg-dark: rgb(8, 8, 8);
--card-bg: rgb(22, 22, 22);
--text: #e0e0e0;
--border: var(--accent);
}
*{
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Inter', sans-serif;
color: var(--text);
}
body{
background-color: var(--bg-dark);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
form{
display: flex;
flex-direction: column;
background-color: var(--card-bg);
border: 1px solid var(--border);
border-radius: 15px;
padding: 30px 25px;
width: 100%;
max-width: 380px;
}
h1{
text-align: center;
margin-bottom: 20px;
color: var(--accent);
font-weight: 700;
font-size: 1.8rem;
}
label{
margin-top: 16px;
margin-bottom: 6px;
font-size: 0.95rem;
font-weight: 500;
display: block;
}
.input{
padding: 12px 14px;
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(0, 255, 255, 0.3);
border-radius: 10px;
color: white;
font-size: 1rem;
outline: none;
}
.input:focus{
border-color: var(--accent);
}
.input::placeholder{
color: rgba(255, 255, 255, 0.4);
}
.checkbox-container{
display: flex;
align-items: center;
gap: 10px;
margin-top: 16px;
font-size: 0.9rem;
line-height: 1.4;
}
.checkbox-container input[type="checkbox"]{
accent-color: var(--accent);
width: 18px;
height: 18px;
margin: 0;
flex-shrink: 0;
}
.checkbox-container a{
color: var(--accent);
text-decoration: underline;
}
button{
margin-top: 24px;
padding: 14px;
background-color: black;
color: var(--accent);
border: 1px solid var(--accent);
border-radius: 10px;
font-weight: 600;
font-size: 1.1rem;
cursor: pointer;
transition: all 0.3s ease;
}
button:hover{
background-color: var(--accent);
color: black;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 255, 255, 0.3);
}
button:active{
transform: translateY(0);
box-shadow: 0 2px 6px rgba(0, 255, 255, 0.2);
}
p{
text-align: center;
margin-top: 2vh;
}
.error{
color: #ff6b6b;
font-size: 0.85rem;
margin-top: 6px;
min-height: 20px;
opacity: 0;
transition: opacity 0.2s ease;
}
.error.active{
opacity: 1;
}
.input.invalid,
.checkbox.invalid{
border-color: #ff6b6b !important;
box-shadow: 0 0 8px rgba(255, 107, 107, 0.3);
}
button:disabled{
opacity: 0.7;
cursor: not-allowed;
transform: none !important;
}
@media (max-width: 480px){
form{
padding: 25px 20px;
}
h1{
font-size: 1.6rem;
}
}

View file

@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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">
<div class="how-to-connect">
<h1>Как подключиться</h1>
<div class="platform-tabs">
<button class="tab-btn active" data-platform="android">Android</button>
<button class="tab-btn" data-platform="windows">ПК (Windows)</button>
<button class="tab-btn" data-platform="ios">iOS</button>
</div>
<div class="instructions" id="android-instructions">
<ol>
<li>Создайте и скопируйте свою конфигурацию в личном кабинете.</li>
<li>Скачайте и установите <a href="https://github.com/2dust/v2rayNG/releases/download/2.0.13/v2rayNG_2.0.13_universal.apk" target="_blank" rel="noopener">приложение v2rayNG</a>.</li>
<li>Откройте приложение.</li>
<li>Нажмите <b>+</b> в правом верхнем углу → <b>Импорт из буфера обмена</b>.</li>
<li><b>Готово!</b> Включайте/выключайте соединение большой кнопкой внизу справа.</li>
</ol>
</div>
<div class="instructions hidden" id="windows-instructions">
<ol>
<li>Создайте и скопируйте свою конфигурацию в личном кабинете.</li>
<li>Скачайте и установите <a href="https://github.com/qr243vbi/nekobox/releases/download/5.10.25/nekobox-5.10.25-windows64-installer.exe" target="_blank" rel="noopener">NekoBox for Windows</a>.</li>
<li>Откройте программу.</li>
<li>Нажмите <kbd>Ctrl</kbd> + <kbd>V</kbd> — конфигурация добавится автоматически.</li>
<li>Выделите добавленную конфигурацию и нажмите <kbd>Enter</kbd>.</li>
<li>Вверху включите:
<ul>
<li><b>Системный прокси</b> — для работы в браузерах</li>
<li><b>Режим TUN</b> — для всей системы</li>
</ul>
</li>
<li><b>Готово!</b> Вы подключены.</li>
</ol>
</div>
<div class="instructions hidden" id="ios-instructions">
<ol>
<li>Создайте и скопируйте свою конфигурацию в личном кабинете.</li>
<li>Скачайте приложение <a href="https://apps.apple.com/ru/app/v2raytun/id6476628951" target="_blank" rel="noopener">V2RayTun</a> из App Store.</li>
<li>Откройте приложение.</li>
<li>Нажмите <b>+</b> в правом верхнем углу → <b>Импорт из буфера обмена</b>.</li>
<li><b>Готово!</b> Включайте/выключайте соединение центральной кнопкой.</li>
</ol>
</div>
</div>
<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>

57
Frontend/Web/index.html Normal file
View file

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spectralvpn</title>
<link rel="stylesheet" href="Styles/index.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<header>
<h2><b>Spectral</b>VPN</h2>
<div>
<button id="register" onclick="location.href='register.html'">Зарегистрироваться</button>
<button id="login" onclick="location.href='control-panel.html'">Войти</button>
</div>
</header>
<section id="main">
<h1>Максимальная скорость<br>и неуязвимое шифрование</h1>
<h4>Мы предлагаем современный протокол <b>VLESS + Reality</b> — шифрование трафика без потери скорости.</h4>
<div id="container">
<div>VLESS</div>
<div>Reality</div>
<div>TLS 1.3</div>
<div>1 Гбит/с</div>
</div>
<button onclick="location.href='register.html'">Попробовать бесплатно</button>
</section>
<section id="info">
<div>
<h1>Speed</h1>
<h3>Высокая скорость</h3>
<p>До 1 Гбит/с</p>
</div>
<div>
<h1>Lock</h1>
<h3>Полное шифрование</h3>
<p>VLESS + TLS 1.3 + Reality</p>
</div>
<div>
<h1>Shield</h1>
<h3>Reality-маскировка</h3>
<p>Трафик выглядит как обычное открытие Web-сайта</p>
</div>
</section>
<section id="final">
<h2>Подключитесь за 5 минут</h2>
<h3>Скачайте конфигурацию VLESS + Reality и импортируйте в любой клиент</h3>
<button onclick="location.href='register.html'">Получить бесплатно</button>
</section>
<footer>
<a href="offer.html">Пользовательское соглашение</a>
<p>2025 SpectralVPN</p>
<a href="privacy.html">Политика конфиденциальности</a>
</footer>
</body>
</html>

118
Frontend/Web/offer.html Normal file
View file

@ -0,0 +1,118 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Пользовательское соглашение (Публичная оферта) — SpectralVPN</title>
<link rel="stylesheet" href="Styles/offer.css">
</head>
<body>
<div class="container">
<header>
<h1>Пользовательское соглашение (Публичная оферта)</h1>
<p>VPN-сервис <strong>SpectralVPN</strong></p>
<p>Дата вступления в силу: <strong>15 ноября 2025 г.</strong></p>
</header>
<section>
<h2>1. Термины и определения</h2>
<p><strong>1.1.</strong> <strong>VPN-сервис SpectralVPN</strong> (далее — «Продавец») — онлайн-сервис, расположенный на доменном имени <a href="https://spectralvpn.ru">spectralvpn.ru</a>, а также его официальная система в мессенджере Telegram с учетной записью <code>@spectralvpnbot</code> (далее — «Телеграм-бот»). Продавец предоставляет платные услуги доступа к виртуальной частной сети (VPN) на условиях настоящего соглашения.</p>
<p><strong>1.2.</strong> <strong>Пользователь</strong> (далее — «Покупатель») — дееспособное физическое либо юридическое лицо, посетившее Сайт или воспользовавшееся Телеграм-ботом и намеревающееся приобрести либо использующее услуги VPN-сервиса SpectralVPN. Факт использования Сайта или Телеграм-бота, включая оформление Заказа, означает <strong>полное и безоговорочное принятие</strong> Пользователем условий настоящего соглашения.</p>
<p><strong>1.3.</strong> <strong>Услуга</strong> — услуга платного предоставления доступа к VPN-соединению, реализуемая путем выдачи Покупателю <strong>уникального URL для подключения к VPN-серверу по протоколу VLESS с использованием Reality</strong>. URL позволяет установить защищённое соединение с интернетом через серверы Продавца.</p>
<p><strong>1.4.</strong> <strong>Заказ</strong> — должным образом оформленный запрос Покупателя на приобретение Услуги через Сайт или Телеграм-бот, совмещённый с внесением оплаты согласно выбранному тарифу. Оплата Заказа Покупателем признаётся <strong>акцептом настоящей оферты</strong>, то есть полным и безусловным принятием условий соглашения.</p>
<p><strong>1.5.</strong> <strong>Стороны</strong> — совместное именование Продавца и Покупателя в рамках настоящего соглашения.</p>
</section>
<section>
<h2>2. Предмет соглашения</h2>
<p><strong>2.1.</strong> Продавец обязуется предоставить Покупателю Услугу VPN-доступа на условиях настоящего соглашения, а Покупатель обязуется оплатить предоставление данной Услуги. Услуга заключается в выдаче Покупателю <strong>уникального URL для подключения к VPN-серверу</strong> по протоколу <strong>VLESS+Reality</strong> с целью защиты интернет-соединения и данных Покупателя. Покупатель самостоятельно выбирает клиентское приложение и настраивает подключение.</p>
<p><strong>2.2.</strong> Настоящее соглашение является договором возмездного оказания услуг и публичной офертой в соответствии с п.2 ст. 437 ГК РФ. Полным и безоговорочным акцептом оферты признается совершение Покупателем оплаты Заказа. С момента акцепта договор считается заключённым.</p>
<p><strong>2.3.</strong> Сервис предоставляется «как есть» (“as is”). Продавец не гарантирует бесперебойную работу на всех устройствах. Характеристики (скорость, задержка, доступность ресурсов) зависят от внешних факторов.</p>
</section>
<section>
<h2>3. Порядок оформления заказа и предоставления Услуги</h2>
<p><strong>3.1.</strong> Оформление Заказа — через <a href="https://spectralvpn.ru">spectralvpn.ru</a> или <code>@spectralvpnbot</code>. Покупатель предоставляет достоверный email.</p>
<p><strong>3.2.</strong> Оплата — 100 % предоплата через ЮKassa. Момент зачисления — начало исполнения.</p>
<p><strong>3.3.</strong> URL выдаётся <strong>в течение 72 часов</strong> (обычно мгновенно) в личном кабинете, по email или в Telegram. Обязательства Продавца выполнены с момента передачи работоспособного URL.</p>
<p><strong>3.4.</strong> Покупатель самостоятельно настраивает клиент (Nekobox, v2rayNG и др.). Инструкции — на Сайте.</p>
<p><strong>3.5.</strong> Продавец вправе отказать в Услуге до оплаты без объяснений. При отказе — полный возврат.</p>
<p><strong>3.6.</strong> Возможны плановые работы. Перерывы не являются нарушением.</p>
</section>
<section>
<h2>4. Условия использования и ограничения</h2>
<p><strong>4.1.</strong> URL — <strong>один Покупатель</strong>, неограниченное количество устройств. Передача третьим лицам запрещена.</p>
<p><strong>4.2.</strong> Запрещено:</p>
<ul>
<li>Противоправные действия (хакерство, спам, мошенничество)</li>
<li>Распространение запрещённых материалов</li>
<li><strong>Торренты (P2P) — автоматически блокируются системой 3x-ui</strong></li>
<li>Утечка URL в открытый доступ</li>
</ul>
<p class="highlight"><strong>При нарушении — блокировка без возврата средств.</strong></p>
<p><strong>4.3.</strong> Покупатель несёт полную ответственность за свои действия в сети.</p>
<p><strong>4.4.</strong> Продавец не ограничивает доступ к ресурсам, но оставляет право блокировки при выявлении нарушений.</p>
<p><strong>4.5.</strong> URL действует до конца оплаченного периода. Продление — опционально.</p>
<p><strong>4.6.</strong> <strong>IP-адрес сервера может меняться ежемесячно.</strong> Актуальный URL — в личном кабинете.</p>
</section>
<section>
<h2>5. Стоимость и расчёты</h2>
<p><strong>5.1.</strong> Тарифы — в рублях РФ, на Сайте и в боте.</p>
<p><strong>5.2.</strong> Цены могут меняться. Изменения не касаются оплаченных Заказов.</p>
<p><strong>5.3.</strong> Оплата — 100 % предоплата через ЮKassa. Чек отправляется автоматически (ФЗ-54).</p>
<p><strong>5.4.</strong> Услуга считается оказанной с момента выдачи URL.</p>
</section>
<section>
<h2>6. Возврат средств</h2>
<p><strong>6.1.</strong> Возврат возможен <strong>в течение 7 календарных дней</strong> с момента оплаты, если <strong>подключение по URL технически невозможно</strong> по вине Продавца.</p>
<p><strong>6.2.</strong> Если URL выдан и работает — <strong>возврат не производится</strong>.</p>
<p><strong>6.3.</strong> При невыдаче URL в течение 72 часов — полный возврат в течение 10 рабочих дней.</p>
<p><strong>6.4.</strong> При блокировке за нарушение — возврат не производится.</p>
</section>
<section>
<h2>7. Конфиденциальность и защита данных</h2>
<p><strong>7.1.</strong> <strong>No-Logs политика:</strong> Продавец <strong>не ведёт журналы интернет-активности</strong> (IP, трафик, DNS, сайты). Вся передаваемая информация шифруется и не сохраняется.</p>
<p><strong>7.2.</strong> Хранятся только:</p>
<ul>
<li>Email (для входа)</li>
<li>Хэш пароля</li>
<li>Приватный ключ (UUID)</li>
<li>Дата оплаты, тариф</li>
<li>Дата принятия соглашения</li>
</ul>
<p class="highlight"><strong>Объём трафика, IP, DNS — НЕ хранятся.</strong></p>
<p><strong>7.3.</strong> Покупатель отвечает за сохранность URL и пароля.</p>
<p><strong>7.5.</strong> Обработка данных — в соответствии с <a href="/privacy.html">Политикой конфиденциальности</a>.</p>
</section>
<section>
<h2>8. Ответственность сторон</h2>
<p><strong>8.1.</strong> Ответственность — по законодательству РФ с учётом ограничений.</p>
<p><strong>8.2.</strong> Продавец отвечает только за выдачу работоспособного URL.</p>
<p><strong>8.6.</strong> Максимальная ответственность — <strong>стоимость последнего оплаченного периода</strong>.</p>
<p><strong>8.7.</strong> Форс-мажор: блокировки, сбои связи, действия властей.</p>
</section>
<section>
<h2>9. Заключительные положения</h2>
<p><strong>9.1.</strong> Оферта действует бессрочно. Изменения публикуются на Сайте. Продолжение использования = согласие.</p>
<p><strong>9.3.</strong> Недействительность одного пункта не влияет на остальные.</p>
<p><strong>9.4.</strong> Споры — в суде по месту нахождения Продавца (РФ).</p>
<p><strong>9.5.</strong> <strong>Безусловный акцепт — использование <a href="https://spectralvpn.ru">spectralvpn.ru</a> или <code>@spectralvpnbot</code>.</strong> Начало использования = согласие с офертой.</p>
</section>
<div class="footer">
<p>© 2025 SpectralVPN. Все права защищены.</p>
<p>Самозанятый | ИНН: <em>[вставьте ваш ИНН]</em></p>
<p>
<a href="index.html" class="btn">На главную</a>
<a href="privacy.html" class="btn">Политика конфиденциальности</a>
</p>
</div>
</div>
</body>
</html>

84
Frontend/Web/privacy.html Normal file
View file

@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Политика конфиденциальности — SpectralVPN</title>
<link rel="stylesheet" href="Styles/offer.css">
</head>
<body>
<div class="container">
<header>
<h1>Политика конфиденциальности</h1>
<p>SpectralVPN — <strong>анонимный VPN без логов активности</strong></p>
<p>Дата вступления в силу: 15 ноября 2025 г.</p>
</header>
<section>
<h2>1. Общие положения</h2>
<p>Настоящая Политика конфиденциальности регулирует порядок обработки персональных данных Пользователей сервиса <strong>SpectralVPN</strong>, расположенного по адресу <a href="https://spectralvpn.ru">spectralvpn.ru</a> и в Telegram-боте <code>@spectralvpnbot</code>.</p>
<p>Использование Сервиса означает безоговорочное согласие Пользователя с настоящей Политикой.</p>
</section>
<section>
<h2>2. Какие данные мы собираем</h2>
<p>Мы собираем <strong>минимальный объём данных</strong>, необходимый только для предоставления Услуги:</p>
<ul>
<li><strong>Email</strong> — для входа и получения URL</li>
<li><strong>Хэш пароля</strong> — оригинал не хранится</li>
<li><strong>Приватный ключ (UUID)</strong> — для аутентификации по VLESS+Reality</li>
<li><strong>Дата оплаты и тариф</strong></li>
<li><strong>Дата принятия соглашения</strong></li>
</ul>
<div class="highlight">
<strong>Мы НЕ собираем и НЕ храним:</strong><br>
• IP-адреса<br>• Объём трафика<br>• DNS-запросы<br>• Посещаемые сайты<br>• Содержимое передаваемых данных
</div>
</section>
<section>
<h2>3. No-Logs политика</h2>
<p>Сервис <strong>SpectralVPN построен на принципе нулевого логирования активности</strong>. Весь трафик шифруется сквозным шифрованием и <strong>не сохраняется</strong> на серверах.</p>
</section>
<section>
<h2>4. Как используются данные</h2>
<ul>
<li>Выдача и активация URL</li>
<li>Продление подписки</li>
<li>Техподдержка</li>
<li>Соблюдение ФЗ-152, ФЗ-54</li>
</ul>
</section>
<section>
<h2>5. Хранение и защита</h2>
<p>Данные хранятся на защищённых серверах в Европе (геолокация меняется ежемесячно). Применяется AES-256, ограничение доступа, резервное копирование. Данные удаляются через 30 дней после окончания подписки.</p>
</section>
<section>
<h2>6. Передача третьим лицам</h2>
<p>Передача только по судебному решению РФ или ЮKassa (сумма, email, статус).</p>
</section>
<section>
<h2>7. Права Пользователя</h2>
<p>Запрос доступа, удаления, отзыва согласия (прекратит доступ). Обращайтесь: <a href="mailto:support@spectralvpn.ru">support@spectralvpn.ru</a></p>
</section>
<section>
<h2>8. Изменения</h2>
<p>Обновления публикуются здесь. Продолжение использования = согласие.</p>
</section>
<div class="footer">
<p>© 2025 SpectralVPN. Все права защищены.</p>
<p>Самозанятый | ИНН: <em>[вставьте ваш ИНН]</em></p>
<p>
<a href="index.html" class="btn">На главную</a>
<a href="offer.html" class="btn">Пользовательское соглашение</a>
</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Регистрация - SpectralVPN</title>
<link rel="stylesheet" href="Styles/register.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="JS/register.js"></script>
<script src="https://cdn.jsdelivr.net/npm/js-cookie@3.0.5/dist/js.cookie.min.js"></script>
</head>
<body>
<form id="registerForm" novalidate>
<h1>Регистрация</h1>
<label for="email">Электронная почта</label>
<input type="email" id="email" class="input" placeholder="example@domain.com" required autocomplete="email">
<div class="error" id="emailError"></div>
<label for="password">Пароль</label>
<input type="password" id="password" class="input" placeholder="Введите пароль">
<div class="error" id="passwordError"></div>
<label for="password_reply">Повторите пароль</label>
<input type="password" id="password_reply" class="input" placeholder="Повторите пароль">
<div class="error" id="passwordReplyError"></div>
<label for="promo_code">Промокод</label>
<input type="text" id="promo_code" class="input" placeholder="Введите прокод">
<div class="error" id="promoError"></div>
<div class="checkbox-container">
<input type="checkbox" id="terms" class="checkbox" required>
<label for="terms">Я прочитал и согласен с <a href="offer.html" target="_blank">пользовательским соглашением</a> и <a href="privacy.html" target="_blank">политикой конфиденциальности</a>.</label>
</div>
<div class="error" id="termsError"></div>
<button type="submit">Зарегистрироваться</button>
<p>Уже есть аккаунт? <a href="control-panel.html">Войти</a></p>
</form>
</body>
</html>