Release 1/2
This commit is contained in:
parent
4c4d325245
commit
bc0838213e
5 changed files with 407 additions and 299 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = `<p style="text-align:center; color:#666; padding:40px 20px;">
|
||||
container.innerHTML = `<p style="text-align:center; color:#666; padding:50px 20px; font-size:15px;">
|
||||
У вас пока нет конфигураций.<br>Нажмите кнопку ниже, чтобы создать первую.
|
||||
</p>`;
|
||||
return;
|
||||
|
|
@ -63,22 +94,20 @@ function renderConfigs(configs) {
|
|||
const card = document.createElement("div");
|
||||
card.className = "url-card";
|
||||
card.innerHTML = `
|
||||
<div>
|
||||
<div class="url-info">
|
||||
<div class="url-name">${cfg.name}</div>
|
||||
<div style="font-size:13px; color:#888; margin-top:6px;">
|
||||
Использовано: ${trafficGB} ГБ
|
||||
</div>
|
||||
<div class="traffic">Использовано: ${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>
|
||||
<button class="btn btn-copy" data-config="${cfg.config}">Скопировать</button>
|
||||
<button class="btn 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}" скопирован в буфер`);
|
||||
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");
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
.hidden { display: none !important; }
|
||||
|
|
@ -11,73 +11,87 @@
|
|||
<div class="container">
|
||||
<header>
|
||||
<h2><b>Spectral</b>VPN</h2>
|
||||
<div class="user-info" id="userInfo">
|
||||
<div class="user-info" id="userInfo" style="display: none;">
|
||||
<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="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>
|
||||
<li>Создайте конфигурацию ниже и скопируйте ссылку.</li>
|
||||
<li>Установите <a href="https://github.com/2dust/v2rayNG/releases" target="_blank">v2rayNG</a>.</li>
|
||||
<li>Нажмите + → Импорт из буфера обмена.</li>
|
||||
<li>Включайте соединение большой кнопкой.</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>
|
||||
<li>Создайте конфигурацию и скопируйте ссылку.</li>
|
||||
<li>Установите <a href="https://github.com/qr243vbi/nekobox/releases" target="_blank">NekoBox</a>.</li>
|
||||
<li>Ctrl + V — конфиг добавится автоматически.</li>
|
||||
<li>Включите Системный прокси и/или TUN.</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>
|
||||
<li>Создайте конфигурацию.</li>
|
||||
<li>Установите <a href="https://apps.apple.com/ru/app/v2raytun/id6476628951" target="_blank">V2RayTun</a>.</li>
|
||||
<li>+ → Импорт из буфера обмена.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1>Ваши конфигурации</h1>
|
||||
<div class="urls-list" id="urlsList"></div>
|
||||
<button class="add-btn" id="addUrlBtn">+ Добавить конфиг</button>
|
||||
<button class="add-btn" id="addConfigBtn">+ Добавить конфигурацию</button>
|
||||
|
||||
<div class="danger-zone">
|
||||
<button id="deleteAccountBtn" class="delete-account-btn">Удалить аккаунт навсегда</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="modal" id="loginModal">
|
||||
<!-- Модальное окно ЛОГИНА (показывается при отсутствии токена) -->
|
||||
<div class="modal active" id="loginModal">
|
||||
<div class="modal-content">
|
||||
<h2>Вход в аккаунт</h2>
|
||||
<input type="email" id="loginEmail" placeholder="Email" required>
|
||||
<input type="password" id="loginPassword" placeholder="Пароль" required>
|
||||
<input type="email" id="loginEmail" placeholder="Email" autocomplete="email">
|
||||
<input type="password" id="loginPassword" placeholder="Пароль" autocomplete="current-password">
|
||||
<div class="error" id="loginError"></div>
|
||||
|
||||
<div class="buttons">
|
||||
<button id="loginSubmit">Войти</button>
|
||||
<button id="closeModal">Отмена</button>
|
||||
</div>
|
||||
|
||||
<p style="text-align: center; margin-top: 20px; color: #888; font-size: 14px;">
|
||||
Нет аккаунта?
|
||||
<a href="register.html" style="color: #00ffff; text-decoration: none;">Зарегистрироваться</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно создания конфига -->
|
||||
<div class="modal" id="addModal">
|
||||
<div class="modal-content">
|
||||
<h2>Новая конфигурация</h2>
|
||||
<input type="text" id="configName" placeholder="Название (Телефон, Ноутбук...)" maxlength="64">
|
||||
<div class="error" id="addError"></div>
|
||||
<div class="buttons">
|
||||
<button id="addSubmit">Создать</button>
|
||||
<button id="addCancel">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue