Release 1/2
This commit is contained in:
parent
4c4d325245
commit
bc0838213e
5 changed files with 407 additions and 299 deletions
|
|
@ -12,7 +12,7 @@ class Configs:
|
||||||
self.client_email = client_email
|
self.client_email = client_email
|
||||||
self.display_name = display_name
|
self.display_name = display_name
|
||||||
|
|
||||||
async def legacy_payload(self, data: dict,) -> dict:
|
def legacy_payload(self, data: dict,) -> dict:
|
||||||
inbounds = data.get("obj")
|
inbounds = data.get("obj")
|
||||||
inbound = next((i for i in inbounds if i.get("id") == self.inbound_id), None)
|
inbound = next((i for i in inbounds if i.get("id") == self.inbound_id), None)
|
||||||
if not inbound:
|
if not inbound:
|
||||||
|
|
@ -44,7 +44,7 @@ class Configs:
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async def legacy_config(self, data: dict) -> str:
|
def legacy_config(self, data: dict) -> str:
|
||||||
inbounds = data.get("obj")
|
inbounds = data.get("obj")
|
||||||
inbound = next((i for i in inbounds if i.get("id") == self.inbound_id), None)
|
inbound = next((i for i in inbounds if i.get("id") == self.inbound_id), None)
|
||||||
if not inbound:
|
if not inbound:
|
||||||
|
|
|
||||||
|
|
@ -5,24 +5,30 @@ server {
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/spectralvpn.ru/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/spectralvpn.ru/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/spectralvpn.ru/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/spectralvpn.ru/privkey.pem;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
if ($request_method = 'OPTIONS') {
|
if ($request_method = 'OPTIONS') {
|
||||||
add_header 'Access-Control-Allow-Origin' 'https://spectralvpn.ru';
|
add_header 'Access-Control-Allow-Origin' 'https://spectralvpn.ru' always;
|
||||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, OPTIONS';
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, OPTIONS' always;
|
||||||
add_header 'Access-Control-Allow-Headers' 'Content-Type';
|
add_header 'Access-Control-Allow-Headers' 'X-API-KEY, Content-Type, Authorization' always;
|
||||||
add_header 'Access-Control-Max-Age' 86400;
|
add_header 'Access-Control-Max-Age' 86400 always;
|
||||||
|
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||||
return 204;
|
return 204;
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy_pass http://api:8000;
|
add_header 'Access-Control-Allow-Origin' 'https://spectralvpn.ru' always;
|
||||||
proxy_set_header Host $host;
|
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
add_header 'Access-Control-Expose-Headers' '*' always; # если нужно читать заголовки ответа
|
||||||
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;
|
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 showNotification = (message, type = "success") => {
|
||||||
const color = type === "success" ? "#00ffff" : "#ff5555";
|
const color = type === "success" ? "#00ffff" : "#ff5555";
|
||||||
const notification = document.createElement("div");
|
const notif = document.createElement("div");
|
||||||
notification.style.cssText = `
|
notif.style.cssText = `
|
||||||
position: fixed; top: 20px; right: 20px; padding: 15px 25px; border-radius: 12px;
|
position: fixed; top: 20px; right: 20px; padding: 14px 24px; border-radius: 12px;
|
||||||
background: rgba(0,0,0,0.95); border: 1px solid ${color}; color: ${color};
|
background: rgba(0,0,0,0.96); border: 1px solid ${color}; color: ${color};
|
||||||
z-index: 10000; font-weight: 500; box-shadow: 0 4px 15px rgba(0,0,0,0.5);
|
z-index: 10000; font-weight: 500; box-shadow: 0 4px 20px rgba(0,0,0,0.6);
|
||||||
`;
|
`;
|
||||||
notification.textContent = message;
|
notif.textContent = message;
|
||||||
document.body.appendChild(notification);
|
document.body.appendChild(notif);
|
||||||
setTimeout(() => notification.remove(), 4000);
|
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() {
|
async function loadPanel() {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
window.location.href = "register.html";
|
showLoginModal();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const res = await apiRequest("/config/get_info", { method: "GET" });
|
||||||
const res = await fetch(`${API_BASE}/config/get_info`, {
|
if (!res) return;
|
||||||
method: "GET",
|
|
||||||
headers: { "X-API-KEY": token }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status === 401) {
|
if (!res.ok) {
|
||||||
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");
|
showNotification("Ошибка загрузки данных", "error");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
document.getElementById("userEmail").textContent = data.email || "Аккаунт активен";
|
||||||
|
hideLoginModal(); // ← Важно!
|
||||||
|
renderConfigs(data.configs || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderConfigs(configs) {
|
function renderConfigs(configs) {
|
||||||
|
|
@ -51,7 +82,7 @@ function renderConfigs(configs) {
|
||||||
container.innerHTML = "";
|
container.innerHTML = "";
|
||||||
|
|
||||||
if (configs.length === 0) {
|
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>Нажмите кнопку ниже, чтобы создать первую.
|
У вас пока нет конфигураций.<br>Нажмите кнопку ниже, чтобы создать первую.
|
||||||
</p>`;
|
</p>`;
|
||||||
return;
|
return;
|
||||||
|
|
@ -63,22 +94,20 @@ function renderConfigs(configs) {
|
||||||
const card = document.createElement("div");
|
const card = document.createElement("div");
|
||||||
card.className = "url-card";
|
card.className = "url-card";
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div>
|
<div class="url-info">
|
||||||
<div class="url-name">${cfg.name}</div>
|
<div class="url-name">${cfg.name}</div>
|
||||||
<div style="font-size:13px; color:#888; margin-top:6px;">
|
<div class="traffic">Использовано: ${trafficGB} ГБ</div>
|
||||||
Использовано: ${trafficGB} ГБ
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="url-actions">
|
<div class="url-actions">
|
||||||
<button class="btn-copy" data-config="${cfg.config}">Скопировать ссылку</button>
|
<button class="btn btn-copy" data-config="${cfg.config}">Скопировать</button>
|
||||||
<button class="btn-delete" data-name="${cfg.name}">Удалить</button>
|
<button class="btn btn-delete" data-name="${cfg.name}">Удалить</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
card.querySelector(".btn-copy").addEventListener("click", async () => {
|
card.querySelector(".btn-copy").addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(cfg.config);
|
await navigator.clipboard.writeText(cfg.config);
|
||||||
showNotification(`Конфиг "${cfg.name}" скопирован в буфер`);
|
showNotification(`Конфиг "${cfg.name}" скопирован`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showNotification("Не удалось скопировать", "error");
|
showNotification("Не удалось скопировать", "error");
|
||||||
}
|
}
|
||||||
|
|
@ -87,25 +116,16 @@ function renderConfigs(configs) {
|
||||||
card.querySelector(".btn-delete").addEventListener("click", async () => {
|
card.querySelector(".btn-delete").addEventListener("click", async () => {
|
||||||
if (!confirm(`Удалить конфигурацию "${cfg.name}"?`)) return;
|
if (!confirm(`Удалить конфигурацию "${cfg.name}"?`)) return;
|
||||||
|
|
||||||
const token = getToken();
|
const res = await apiRequest("/config/delete", {
|
||||||
try {
|
method: "DELETE",
|
||||||
const res = await fetch(`${API_BASE}/config/delete`, {
|
body: JSON.stringify({ name: cfg.name })
|
||||||
method: "DELETE",
|
});
|
||||||
headers: {
|
|
||||||
"X-API-KEY": token,
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ name: cfg.name })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
if (res && res.ok) {
|
||||||
card.remove();
|
showNotification(`Конфиг "${cfg.name}" удалён`);
|
||||||
showNotification(`Конфиг "${cfg.name}" удалён`, "error");
|
loadPanel();
|
||||||
} else {
|
} else {
|
||||||
showNotification("Не удалось удалить конфиг", "error");
|
showNotification("Не удалось удалить конфиг", "error");
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
showNotification("Ошибка сети", "error");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -113,61 +133,126 @@ function renderConfigs(configs) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("addConfigBtn").addEventListener("click", async () => {
|
// ==================== ЛОГИН ====================
|
||||||
const name = prompt("Введите название конфигурации (например: Телефон, Ноутбук, Рабочий):");
|
document.getElementById("loginSubmit").addEventListener("click", async () => {
|
||||||
if (!name || !name.trim()) return;
|
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 {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/config/add`, {
|
const res = await fetch(`${API_BASE}/user/login`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: { "Content-Type": "application/json" },
|
||||||
"X-API-KEY": token,
|
body: JSON.stringify({ email, password })
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ name: name.trim() })
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok && data.access_token) {
|
||||||
showNotification(`Конфиг "${name}" успешно создан!`);
|
localStorage.setItem("access_token", data.access_token);
|
||||||
loadPanel();
|
showNotification("Вход выполнен успешно!");
|
||||||
|
document.getElementById("loginEmail").value = "";
|
||||||
|
document.getElementById("loginPassword").value = "";
|
||||||
|
loadPanel(); // ← Главное исправление
|
||||||
} else {
|
} else {
|
||||||
showNotification(data.detail || "Ошибка при создании конфига", "error");
|
errorEl.textContent = data.detail || "Неверный email или пароль";
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (err) {
|
||||||
showNotification("Нет связи с сервером", "error");
|
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();
|
const token = getToken();
|
||||||
try {
|
if (token) {
|
||||||
const res = await fetch(`${API_BASE}/user/delete`, {
|
await fetch(`${API_BASE}/user/logout`, {
|
||||||
method: "DELETE",
|
method: "POST",
|
||||||
headers: { "X-API-KEY": token }
|
headers: { "X-API-KEY": token, "Content-Type": "application/json" },
|
||||||
});
|
body: JSON.stringify({ token_to_revoke: token })
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
if (res.ok) {
|
localStorage.removeItem("access_token");
|
||||||
localStorage.removeItem("access_token");
|
showLoginModal();
|
||||||
alert("Аккаунт успешно удалён.");
|
});
|
||||||
window.location.href = "index.html";
|
|
||||||
} else {
|
// ==================== Удаление аккаунта ====================
|
||||||
showNotification("Не удалось удалить аккаунт", "error");
|
document.getElementById("deleteAccountBtn").addEventListener("click", async () => {
|
||||||
}
|
if (!confirm("Вы уверены? Это действие необратимо!")) return;
|
||||||
} catch (e) {
|
|
||||||
showNotification("Ошибка сети", "error");
|
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("Выйти из аккаунта?")) {
|
document.querySelectorAll(".tab-btn").forEach(btn => {
|
||||||
localStorage.removeItem("access_token");
|
btn.addEventListener("click", () => {
|
||||||
window.location.href = "index.html";
|
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;
|
--bg: #000;
|
||||||
--card: #111;
|
--card: #111;
|
||||||
--text: #e0e0e0;
|
--text: #e0e0e0;
|
||||||
--accent: cyan;
|
--accent: #00ffff;
|
||||||
--border: #333;
|
--border: #333;
|
||||||
|
--danger: #ff4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
*{margin: 0; padding: 0; box-sizing: border-box;}
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
body{background: var(--bg); color: var(--text); font-family: 'Inter', sans-serif; min-height: 100vh;}
|
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;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
border-bottom: 1px solid #333;
|
border-bottom: 1px solid var(--border);
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
header h2 {font-size: 24pt;}
|
header h2 { font-size: 28px; }
|
||||||
header h2 b {color: var(--accent);}
|
header h2 b { color: var(--accent); }
|
||||||
|
|
||||||
.user-info{
|
.user-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
font-size: 14pt;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#logoutBtn{
|
#logoutBtn {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid var(--accent);
|
border: 1px solid var(--accent);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
padding: 8px 16px;
|
padding: 8px 18px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: 0.3s;
|
transition: 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
#logoutBtn:hover{
|
#logoutBtn:hover {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1{
|
h1 {
|
||||||
font-size: 28pt;
|
font-size: 26px;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 25px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.urls-list{
|
.urls-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 15px;
|
gap: 16px;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.url-card{
|
.url-card {
|
||||||
background: var(--card);
|
background: var(--card);
|
||||||
border: 1px solid #333;
|
border: 1px solid var(--border);
|
||||||
border-radius: 12px;
|
border-radius: 14px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
transition: 0.3s;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.url-card:hover{
|
.url-card:hover {
|
||||||
border-color: var(--accent);
|
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;
|
font-weight: 600;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
font-size: 17px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.url-actions{
|
.traffic {
|
||||||
|
font-size: 13.5px;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-copy, .btn-delete{
|
.btn {
|
||||||
padding: 8px 16px;
|
padding: 9px 18px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
transition: 0.3s;
|
transition: 0.3s;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-copy{
|
.btn-copy {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid var(--accent);
|
border: 1px solid var(--accent);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-copy:hover{
|
.btn-copy:hover {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-delete{
|
.btn-delete {
|
||||||
background: #330000;
|
background: #2a0a0a;
|
||||||
border: 1px solid #800;
|
border: 1px solid #800;
|
||||||
color: #f88;
|
color: #ff8888;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-delete:hover{
|
.btn-delete:hover {
|
||||||
background: #800;
|
background: #800;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-btn{
|
.add-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 16px;
|
padding: 18px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 2px dashed var(--accent);
|
border: 2px dashed var(--accent);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-size: 16pt;
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:hover{
|
transition: 0.3s;
|
||||||
background: rgba(0, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal{
|
.add-btn:hover {
|
||||||
display: none;
|
background: rgba(0, 255, 255, 0.08);
|
||||||
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;}
|
.danger-zone {
|
||||||
|
margin-top: 60px;
|
||||||
.modal-content{
|
|
||||||
background: #111;
|
|
||||||
padding: 30px;
|
|
||||||
border-radius: 16px;
|
|
||||||
width: 90%;
|
|
||||||
max-width: 400px;
|
|
||||||
border: 1px solid var(--accent);
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content input{
|
.delete-account-btn {
|
||||||
width: 100%;
|
background: #3a0a0a;
|
||||||
padding: 14px;
|
color: var(--danger);
|
||||||
margin: 10px 0;
|
border: 1px solid #a00;
|
||||||
background: #222;
|
padding: 12px 28px;
|
||||||
border: 1px solid #444;
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-account-btn:hover {
|
||||||
|
background: #600;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content .error{
|
/* Модальное окно */
|
||||||
color: #ff6b6b;
|
.modal {
|
||||||
margin: 10px 0;
|
display: none;
|
||||||
min-height: 20px;
|
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;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons button{
|
.buttons button {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 12px;
|
padding: 14px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
cursor: pointer;
|
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;
|
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);
|
background: var(--card);
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 28px 32px;
|
padding: 26px 30px;
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.instructions ol{
|
.instructions ol {
|
||||||
counter-reset: step;
|
counter-reset: step;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
font-size: 15.5px;
|
|
||||||
line-height: 1.65;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.instructions li{
|
.instructions li {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: 38px;
|
padding-left: 42px;
|
||||||
margin-bottom: 18px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.instructions li::before{
|
.instructions li::before {
|
||||||
content: counter(step);
|
content: counter(step);
|
||||||
counter-increment: step;
|
counter-increment: step;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 2px;
|
top: 0;
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
background: #222;
|
background: #222;
|
||||||
|
|
@ -265,37 +298,7 @@ h1{
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 14px;
|
font-weight: 700;
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.instructions a{
|
.hidden { display: none !important; }
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
@ -11,73 +11,87 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header>
|
<header>
|
||||||
<h2><b>Spectral</b>VPN</h2>
|
<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>
|
<span id="userEmail"></span>
|
||||||
<button id="logoutBtn">Выйти</button>
|
<button id="logoutBtn">Выйти</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- Главный контент (скрыт, пока не авторизован) -->
|
||||||
<main id="mainContent" class="hidden">
|
<main id="mainContent" class="hidden">
|
||||||
|
<!-- Как подключиться -->
|
||||||
<div class="how-to-connect">
|
<div class="how-to-connect">
|
||||||
<h1>Как подключиться</h1>
|
<h1>Как подключиться</h1>
|
||||||
|
|
||||||
<div class="platform-tabs">
|
<div class="platform-tabs">
|
||||||
<button class="tab-btn active" data-platform="android">Android</button>
|
<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>
|
<button class="tab-btn" data-platform="ios">iOS</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="instructions" id="android-instructions">
|
<div class="instructions" id="android-instructions">
|
||||||
<ol>
|
<ol>
|
||||||
<li>Создайте и скопируйте свою конфигурацию в личном кабинете.</li>
|
<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>Установите <a href="https://github.com/2dust/v2rayNG/releases" target="_blank">v2rayNG</a>.</li>
|
||||||
<li>Откройте приложение.</li>
|
<li>Нажмите + → Импорт из буфера обмена.</li>
|
||||||
<li>Нажмите <b>+</b> в правом верхнем углу → <b>Импорт из буфера обмена</b>.</li>
|
<li>Включайте соединение большой кнопкой.</li>
|
||||||
<li><b>Готово!</b> Включайте/выключайте соединение большой кнопкой внизу справа.</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="instructions hidden" id="windows-instructions">
|
<div class="instructions hidden" id="windows-instructions">
|
||||||
<ol>
|
<ol>
|
||||||
<li>Создайте и скопируйте свою конфигурацию в личном кабинете.</li>
|
<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>Установите <a href="https://github.com/qr243vbi/nekobox/releases" target="_blank">NekoBox</a>.</li>
|
||||||
<li>Откройте программу.</li>
|
<li>Ctrl + V — конфиг добавится автоматически.</li>
|
||||||
<li>Нажмите <kbd>Ctrl</kbd> + <kbd>V</kbd> — конфигурация добавится автоматически.</li>
|
<li>Включите Системный прокси и/или TUN.</li>
|
||||||
<li>Выделите добавленную конфигурацию и нажмите <kbd>Enter</kbd>.</li>
|
|
||||||
<li>Вверху включите:
|
|
||||||
<ul>
|
|
||||||
<li><b>Системный прокси</b> — для работы в браузерах</li>
|
|
||||||
<li><b>Режим TUN</b> — для всей системы</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li><b>Готово!</b> Вы подключены.</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="instructions hidden" id="ios-instructions">
|
<div class="instructions hidden" id="ios-instructions">
|
||||||
<ol>
|
<ol>
|
||||||
<li>Создайте и скопируйте свою конфигурацию в личном кабинете.</li>
|
<li>Создайте конфигурацию.</li>
|
||||||
<li>Скачайте приложение <a href="https://apps.apple.com/ru/app/v2raytun/id6476628951" target="_blank" rel="noopener">V2RayTun</a> из App Store.</li>
|
<li>Установите <a href="https://apps.apple.com/ru/app/v2raytun/id6476628951" target="_blank">V2RayTun</a>.</li>
|
||||||
<li>Откройте приложение.</li>
|
<li>+ → Импорт из буфера обмена.</li>
|
||||||
<li>Нажмите <b>+</b> в правом верхнем углу → <b>Импорт из буфера обмена</b>.</li>
|
|
||||||
<li><b>Готово!</b> Включайте/выключайте соединение центральной кнопкой.</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1>Ваши конфигурации</h1>
|
<h1>Ваши конфигурации</h1>
|
||||||
<div class="urls-list" id="urlsList"></div>
|
<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>
|
</main>
|
||||||
|
|
||||||
<div class="modal" id="loginModal">
|
<!-- Модальное окно ЛОГИНА (показывается при отсутствии токена) -->
|
||||||
|
<div class="modal active" id="loginModal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<h2>Вход в аккаунт</h2>
|
<h2>Вход в аккаунт</h2>
|
||||||
<input type="email" id="loginEmail" placeholder="Email" required>
|
<input type="email" id="loginEmail" placeholder="Email" autocomplete="email">
|
||||||
<input type="password" id="loginPassword" placeholder="Пароль" required>
|
<input type="password" id="loginPassword" placeholder="Пароль" autocomplete="current-password">
|
||||||
<div class="error" id="loginError"></div>
|
<div class="error" id="loginError"></div>
|
||||||
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button id="loginSubmit">Войти</button>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue