Files
netrunner-landing/app/[lang]/auth/auth-client.tsx
T
2026-04-25 14:57:50 +07:00

275 lines
10 KiB
TypeScript

"use client";
import { useEffect, useState, useRef } from "react";
import { useRouter } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import {
Shield,
ShieldAlert,
ShieldCheck,
Loader2,
Terminal,
} from "lucide-react";
import { CyberButton } from "@/components/ui/cyber-button";
import { Dictionary } from "@/lib/IDict";
declare global {
interface Window {
onTelegramAuth: (user: any) => void;
}
}
interface AuthClientProps {
dict: Dictionary["auth"];
lang: string;
}
export function AuthClient({ dict, lang }: AuthClientProps) {
const router = useRouter();
const [status, setStatus] = useState<
"processing" | "granted" | "denied" | "awaiting"
>("processing");
const [logs, setLogs] = useState<string[]>([]);
const tgContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const authenticate = async () => {
setLogs((prev) => [...prev, `> ${dict.processing}`]);
// Эмулируем задержку для "хакерского" эффекта консоли
await new Promise((resolve) => setTimeout(resolve, 800));
const hash = window.location.hash;
let token = localStorage.getItem("nrxp_token");
// Логика получения токена из хеша (переход из Telegram бота)
if (hash.includes("token=")) {
const params = new URLSearchParams(hash.replace("#", "?"));
const urlToken = params.get("token");
if (urlToken) {
token = urlToken;
// Безопасно очищаем URL от токена
window.history.replaceState(null, "", window.location.pathname);
}
}
if (!token) {
setLogs((prev) => [
...prev,
"> [WAITING] INITIALIZE TELEGRAM SECURE LOGIN...",
]);
setStatus("awaiting");
return;
}
if (status !== "processing") setStatus("processing");
setLogs((prev) => [...prev, `> ${dict.verifying}`]);
try {
const API_BASE = process.env.NEXT_PUBLIC_API_URL || "/api";
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, `> ${dict.redirecting}`]);
setTimeout(() => router.push(`/${lang}/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();
}, [dict, lang, router]); // Этот эффект запустится 1 раз при маунте
useEffect(() => {
window.onTelegramAuth = async (user: any) => {
setStatus("processing");
setLogs((prev) => [
...prev,
"> [AUTH] RECEIVED TELEGRAM PAYLOAD",
"> VERIFYING HMAC-SHA256 SIGNATURE...",
]);
try {
const API_BASE = process.env.NEXT_PUBLIC_API_URL || "/api";
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",
`> ${dict.redirecting}`,
]);
setStatus("granted");
setTimeout(() => router.push(`/${lang}/profile`), 1500);
} else {
setLogs((prev) => [...prev, "> [FAIL] INVALID SIGNATURE OR EXPIRED"]);
setStatus("denied");
}
} catch (e) {
setLogs((prev) => [...prev, "> [FAIL] NETWORK ERROR"]);
setStatus("denied");
}
};
}, [dict, lang, router]);
useEffect(() => {
if (
status === "awaiting" &&
tgContainerRef.current &&
!document.getElementById("tg-widget")
) {
const script = document.createElement("script");
script.id = "tg-widget";
script.src = "https://telegram.org/js/telegram-widget.js?22";
script.setAttribute(
"data-telegram-login",
process.env.NEXT_PUBLIC_BOT_USERNAME || "ntrnr_vpn_bot",
);
script.setAttribute("data-size", "large");
script.setAttribute("data-onauth", "onTelegramAuth(user)");
script.setAttribute("data-request-access", "write");
tgContainerRef.current.appendChild(script);
}
}, [status]);
return (
<div className="min-h-screen pt-32 pb-24 px-4 container mx-auto max-w-2xl relative flex flex-col items-center justify-center font-mono">
{/* Мягкое фоновое свечение */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[80vw] h-[80vw] max-w-150 max-h-150 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-card/40 backdrop-blur-xl border border-border/50 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-destructive/20 blur-xl rounded-full" />
<ShieldAlert className="w-20 h-20 text-destructive 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-foreground uppercase">
{status === "processing"
? dict.title
: status === "granted"
? dict.granted
: status === "awaiting"
? dict.awaiting
: dict.denied}
</h1>
</div>
{/* Терминал с логами процесса авторизации */}
<div className="bg-background/80 border border-border/50 rounded-2xl p-6 font-mono text-sm text-muted-foreground/80 min-h-35 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-destructive 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">
{dict.noToken}
</p>
{/* При ошибке даем ссылку на телеграм бота для авторизации */}
<CyberButton
href="https://t.me/ntrnr_vpn_bot"
variant="primary"
className="w-full sm:w-auto"
>
<span className="flex items-center gap-2">
<Terminal className="w-4 h-4" />
{dict.toBot}
</span>
</CyberButton>
</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">
{dict.authPrompt}
</p>
<div
ref={tgContainerRef}
className="min-h-[40px] flex items-center justify-center"
/>
</motion.div>
)}
</motion.div>
</div>
);
}