backend sliced by modules
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Shield, Server, Activity, Plus, Power, RefreshCw } from "lucide-react";
|
||||
import { TOKEN_KEY } from "./api";
|
||||
import { TOKEN_KEY, API_BASE } from "./api"; // Добавили API_BASE импорт
|
||||
|
||||
interface VpnNode {
|
||||
id: string;
|
||||
@@ -24,7 +24,8 @@ export default function AdminDashboard() {
|
||||
const fetchNodes = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
const res = await fetch("/api/nodes", {
|
||||
// Путь: /api/v1/nodes
|
||||
const res = await fetch(`${API_BASE}/nodes`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) throw new Error("API Error");
|
||||
@@ -47,7 +48,8 @@ export default function AdminDashboard() {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
const res = await fetch("/api/admin/nodes", {
|
||||
// Путь: /api/v1/admin/nodes
|
||||
const res = await fetch(`${API_BASE}/admin/nodes`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
Shield,
|
||||
ShieldAlert,
|
||||
ShieldCheck,
|
||||
Loader2,
|
||||
Terminal,
|
||||
} from "lucide-react";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
onTelegramAuth: (user: any) => void;
|
||||
}
|
||||
}
|
||||
|
||||
const DICT = {
|
||||
en: {
|
||||
processing: "INITIALIZING HANDSHAKE...",
|
||||
verifying: "VERIFYING SIGNATURE...",
|
||||
redirecting: "ACCESS GRANTED. ROUTING TO UPLINK...",
|
||||
title: "SYSTEM AUTHENTICATION",
|
||||
granted: "ACCESS GRANTED",
|
||||
awaiting: "AWAITING CREDENTIALS",
|
||||
denied: "ACCESS DENIED",
|
||||
noToken:
|
||||
"Authentication failed. Provide valid credentials or authenticate via secure Telegram widget.",
|
||||
toBot: "INITIALIZE SECURE TERMINAL (BOT)",
|
||||
authPrompt:
|
||||
"Authorize via Telegram Widget to establish a secure connection.",
|
||||
},
|
||||
ru: {
|
||||
processing: "ИНИЦИАЛИЗАЦИЯ РУКОПОЖАТИЯ...",
|
||||
verifying: "ПРОВЕРКА ЦИФРОВОЙ ПОДПИСИ...",
|
||||
redirecting: "ДОСТУП РАЗРЕШЕН. МАРШРУТИЗАЦИЯ...",
|
||||
title: "АУТЕНТИФИКАЦИЯ СИСТЕМЫ",
|
||||
granted: "ДОСТУП РАЗРЕШЕН",
|
||||
awaiting: "ОЖИДАНИЕ УЧЕТНЫХ ДАННЫХ",
|
||||
denied: "ДОСТУП ЗАПРЕЩЕН",
|
||||
noToken:
|
||||
"Сбой аутентификации. Предоставьте валидные данные или авторизуйтесь через виджет Telegram.",
|
||||
toBot: "ЗАПУСТИТЬ ТЕРМИНАЛ (BOT)",
|
||||
authPrompt:
|
||||
"Авторизуйтесь через виджет Telegram для установки защищенного соединения.",
|
||||
},
|
||||
};
|
||||
|
||||
const getLang = (): "en" | "ru" => {
|
||||
const l = localStorage.getItem("lang");
|
||||
return l === "ru" ? "ru" : "en";
|
||||
};
|
||||
|
||||
export default function AuthClient() {
|
||||
const navigate = useNavigate();
|
||||
const [lang] = useState<"en" | "ru">(getLang);
|
||||
const t = DICT[lang];
|
||||
|
||||
const [status, setStatus] = useState<
|
||||
"processing" | "granted" | "denied" | "awaiting"
|
||||
>("processing");
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
const tgContainerRef = useRef<HTMLDivElement>(null);
|
||||
const initialized = useRef(false);
|
||||
const widgetInjected = useRef(false);
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || "/api";
|
||||
const BOT_USERNAME = import.meta.env.VITE_BOT_USERNAME || "ntrnr_vpn_bot";
|
||||
|
||||
useEffect(() => {
|
||||
if (initialized.current) return;
|
||||
initialized.current = true;
|
||||
|
||||
const authenticate = async () => {
|
||||
setLogs((prev) => [...prev, `> ${t.processing}`]);
|
||||
|
||||
// Эмулируем задержку для "хакерского" эффекта консоли
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
|
||||
const hash = window.location.hash;
|
||||
let token = localStorage.getItem("nrxp_token");
|
||||
|
||||
// Логика получения токена из query parameters или хеша
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const queryToken = searchParams.get("token");
|
||||
|
||||
if (queryToken) {
|
||||
token = queryToken;
|
||||
searchParams.delete("token");
|
||||
const newSearch = searchParams.toString();
|
||||
const newUrl =
|
||||
window.location.pathname +
|
||||
(newSearch ? `?${newSearch}` : "") +
|
||||
window.location.hash;
|
||||
window.history.replaceState(null, "", newUrl);
|
||||
} else if (hash.includes("token=")) {
|
||||
const params = new URLSearchParams(hash.replace("#", "?"));
|
||||
const urlToken = params.get("token");
|
||||
if (urlToken) {
|
||||
token = urlToken;
|
||||
// Безопасно очищаем URL от токена, сохраняя search params
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
window.location.pathname + window.location.search,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
setLogs((prev) => [
|
||||
...prev,
|
||||
"> [WAITING] INITIALIZE TELEGRAM SECURE LOGIN...",
|
||||
]);
|
||||
setStatus("awaiting");
|
||||
return;
|
||||
}
|
||||
|
||||
if (status !== "processing") setStatus("processing");
|
||||
setLogs((prev) => [...prev, `> ${t.verifying}`]);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/me`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
localStorage.setItem("nrxp_token", token);
|
||||
setLogs((prev) => [...prev, "> [OK] SIGNATURE VALID"]);
|
||||
setStatus("granted");
|
||||
setLogs((prev) => [...prev, `> ${t.redirecting}`]);
|
||||
setTimeout(() => navigate("/profile"), 1500);
|
||||
} else {
|
||||
localStorage.removeItem("nrxp_token");
|
||||
setLogs((prev) => [...prev, "> [FAIL] API REJECTED TOKEN"]);
|
||||
setStatus("awaiting");
|
||||
}
|
||||
} catch (err) {
|
||||
setLogs((prev) => [...prev, "> [FAIL] CORE OFFLINE"]);
|
||||
setStatus("denied");
|
||||
}
|
||||
};
|
||||
|
||||
authenticate();
|
||||
}, [t, navigate, API_BASE]);
|
||||
|
||||
useEffect(() => {
|
||||
window.onTelegramAuth = async (user: any) => {
|
||||
setStatus("processing");
|
||||
setLogs((prev) => [
|
||||
...prev,
|
||||
"> [AUTH] RECEIVED TELEGRAM PAYLOAD",
|
||||
"> VERIFYING HMAC-SHA256 SIGNATURE...",
|
||||
]);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/auth/telegram`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(user),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
localStorage.setItem("nrxp_token", data.token);
|
||||
setLogs((prev) => [
|
||||
...prev,
|
||||
"> [OK] SIGNATURE VALID",
|
||||
`> ${t.redirecting}`,
|
||||
]);
|
||||
setStatus("granted");
|
||||
setTimeout(() => navigate("/profile"), 1500);
|
||||
} else {
|
||||
setLogs((prev) => [...prev, "> [FAIL] INVALID SIGNATURE OR EXPIRED"]);
|
||||
setStatus("denied");
|
||||
}
|
||||
} catch (e) {
|
||||
setLogs((prev) => [...prev, "> [FAIL] NETWORK ERROR"]);
|
||||
setStatus("denied");
|
||||
}
|
||||
};
|
||||
}, [t, navigate, API_BASE]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
status === "awaiting" &&
|
||||
tgContainerRef.current &&
|
||||
!widgetInjected.current
|
||||
) {
|
||||
widgetInjected.current = true;
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://telegram.org/js/telegram-widget.js?22";
|
||||
script.async = true;
|
||||
script.setAttribute("data-telegram-login", BOT_USERNAME);
|
||||
script.setAttribute("data-size", "large");
|
||||
script.setAttribute("data-onauth", "onTelegramAuth(user)");
|
||||
script.setAttribute("data-request-access", "write");
|
||||
tgContainerRef.current.appendChild(script);
|
||||
}
|
||||
}, [status, BOT_USERNAME]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pt-16 pb-24 px-4 container mx-auto max-w-2xl relative flex flex-col items-center justify-center font-mono bg-[#05050A]">
|
||||
{/* Мягкое фоновое свечение */}
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[80vw] h-[80vw] max-w-[600px] max-h-[600px] bg-primary/5 blur-[120px] rounded-full -z-10 pointer-events-none" />
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="w-full bg-white/[0.02] backdrop-blur-xl border border-white/10 rounded-2xl p-8 md:p-12 shadow-2xl relative overflow-hidden"
|
||||
>
|
||||
{/* Заголовок и анимированные иконки статуса */}
|
||||
<div className="flex flex-col items-center mb-10 text-center">
|
||||
<div className="mb-8 relative">
|
||||
{status === "processing" && (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-primary/20 blur-xl rounded-full" />
|
||||
<Shield className="w-20 h-20 text-primary animate-pulse relative z-10" />
|
||||
</div>
|
||||
)}
|
||||
{status === "granted" && (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-secondary/20 blur-xl rounded-full" />
|
||||
<ShieldCheck className="w-20 h-20 text-secondary drop-shadow-[0_0_15px_rgba(0,229,242,0.8)] relative z-10" />
|
||||
</div>
|
||||
)}
|
||||
{status === "denied" && (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-red-500/20 blur-xl rounded-full" />
|
||||
<ShieldAlert className="w-20 h-20 text-red-500 drop-shadow-[0_0_15px_rgba(255,0,0,0.8)] relative z-10" />
|
||||
</div>
|
||||
)}
|
||||
{status === "awaiting" && (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-secondary/20 blur-xl rounded-full" />
|
||||
<Terminal className="w-20 h-20 text-secondary drop-shadow-[0_0_15px_rgba(0,229,242,0.8)] relative z-10" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h1 className="text-2xl md:text-3xl font-black tracking-widest text-white/90 uppercase">
|
||||
{status === "processing"
|
||||
? t.title
|
||||
: status === "granted"
|
||||
? t.granted
|
||||
: status === "awaiting"
|
||||
? t.awaiting
|
||||
: t.denied}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Терминал с логами процесса авторизации */}
|
||||
<div className="bg-black/60 border border-white/10 rounded-2xl p-6 font-mono text-sm text-muted-foreground/80 min-h-[140px] flex flex-col justify-end space-y-3 mb-8 shadow-inner">
|
||||
<AnimatePresence>
|
||||
{logs.map((log, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className={
|
||||
log.includes("[OK]")
|
||||
? "text-secondary font-bold drop-shadow-[0_0_5px_rgba(0,229,242,0.5)]"
|
||||
: log.includes("[FAIL]")
|
||||
? "text-red-500 font-bold drop-shadow-[0_0_5px_rgba(255,0,0,0.5)]"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{log}
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Мигающий курсор загрузки */}
|
||||
{status === "processing" && (
|
||||
<div className="flex items-center gap-2 text-primary mt-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span className="animate-pulse font-bold text-lg leading-none">
|
||||
_
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Сообщение об ошибке и кнопка в бот (если доступ запрещен) */}
|
||||
{status === "denied" && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="flex flex-col items-center text-center gap-8"
|
||||
>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed max-w-md">
|
||||
{t.noToken}
|
||||
</p>
|
||||
<button
|
||||
onClick={() =>
|
||||
(window.location.href = `https://t.me/${BOT_USERNAME}`)
|
||||
}
|
||||
className="w-full relative bg-primary/10 hover:bg-primary/20 border border-primary/40 text-primary font-bold tracking-widest uppercase p-4 rounded-xl transition-all flex justify-center items-center gap-3 shadow-[0_0_15px_rgba(139,61,255,0.1)] hover:shadow-[0_0_25px_rgba(139,61,255,0.3)] group overflow-hidden"
|
||||
>
|
||||
<div className="absolute inset-0 bg-[linear-gradient(90deg,transparent_0%,rgba(139,61,255,0.1)_50%,transparent_100%)] translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000" />
|
||||
<Terminal className="w-5 h-5 group-hover:text-white transition-colors" />
|
||||
<span className="group-hover:text-white transition-colors">
|
||||
[ {t.toBot} ]
|
||||
</span>
|
||||
</button>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Блок логина виджетом Telegram (рендерится только если нет токена) */}
|
||||
{status === "awaiting" && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="flex flex-col items-center text-center gap-6 mt-4"
|
||||
>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed max-w-md">
|
||||
{t.authPrompt}
|
||||
</p>
|
||||
<div
|
||||
ref={tgContainerRef}
|
||||
className="min-h-[40px] flex items-center justify-center bg-white/5 rounded-xl p-4 w-full"
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+10
-7
@@ -1,12 +1,13 @@
|
||||
// Берем из .env, иначе используем дефолт
|
||||
export const API_BASE = import.meta.env.VITE_API_BASE || "/api";
|
||||
// Базовый путь теперь смотрит на /api/v1
|
||||
export const API_BASE = import.meta.env.VITE_API_BASE || "/api/v1";
|
||||
export const TOKEN_KEY = "nrxp_token";
|
||||
|
||||
export const fetchProfile = async () => {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
if (!token) throw new Error("No token found");
|
||||
|
||||
const res = await fetch(`${API_BASE}/me`, {
|
||||
// Путь: /api/v1/users/me
|
||||
const res = await fetch(`${API_BASE}/users/me`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
@@ -18,7 +19,8 @@ export const submitHackResult = async (score: number) => {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
if (!token) throw new Error("No token found");
|
||||
|
||||
const res = await fetch(`${API_BASE}/hack/result`, {
|
||||
// Путь: /api/v1/users/hack/result
|
||||
const res = await fetch(`${API_BASE}/users/hack/result`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -31,10 +33,10 @@ export const submitHackResult = async (score: number) => {
|
||||
return res.json();
|
||||
};
|
||||
|
||||
// 🔥 ИСПРАВЛЕНО: Правильный парсинг ошибки Триала
|
||||
export const activateTrial = async () => {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
const res = await fetch(`${API_BASE}/trial`, {
|
||||
// Путь: /api/v1/billing/trial
|
||||
const res = await fetch(`${API_BASE}/billing/trial`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
@@ -47,7 +49,8 @@ export const activateTrial = async () => {
|
||||
|
||||
export const buySubscription = async (plan: string) => {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
const res = await fetch(`${API_BASE}/subscribe`, {
|
||||
// Путь: /api/v1/billing/subscribe
|
||||
const res = await fetch(`${API_BASE}/billing/subscribe`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
Reference in New Issue
Block a user