This commit is contained in:
2026-07-02 23:13:24 +07:00
parent 31f354d9ca
commit c04194bb85
36 changed files with 1005 additions and 41 deletions
+33
View File
@@ -13,6 +13,9 @@ import {
import { CyberButton } from "@/components/ui/cyber-button";
import { Dictionary } from "@/lib/IDict";
// Telegram Login Widget вызывает этот колбэк из глобального window после успешной
// авторизации в самом Telegram — вешаем на него типизацию, т.к. виджет грузится
// сторонним <script> и не завязан на React-модель.
declare global {
interface Window {
onTelegramAuth: (user: any) => void;
@@ -24,6 +27,23 @@ interface AuthClientProps {
lang: string;
}
/**
* Экран авторизации через Telegram Login Widget.
*
* Логика в двух независимых ветках:
* 1. Если в URL уже есть токен (боту редиректит юзера обратно с `?token=` или `#token=`,
* либо токен уже лежит в localStorage) — сразу проверяем его на `/api/me`.
* 2. Если токена нет — показываем статус "awaiting" и инжектим Telegram Widget script;
* после успешного логина в Telegram виджет дергает `window.onTelegramAuth`, мы шлём
* данные пользователя на `/api/auth/telegram`, получаем токен и сохраняем его.
*
* Токен читается и из query, и из hash, потому что Telegram Login Widget в разных
* сценариях редиректа (виджет на этой же странице vs. редирект из бота) кладёт токен
* то в query-параметр, то в хэш — приходится поддерживать оба варианта.
*
* Статусы: processing (идёт проверка) / granted (успех, редирект в профиль) /
* denied (сеть недоступна или подпись невалидна) / awaiting (ждём логина в Telegram).
*/
export function AuthClient({ dict, lang }: AuthClientProps) {
const router = useRouter();
const [status, setStatus] = useState<
@@ -35,6 +55,8 @@ export function AuthClient({ dict, lang }: AuthClientProps) {
const widgetInjected = useRef(false);
useEffect(() => {
// Защита от двойного запуска в React 18 Strict Mode (эффект монтируется дважды
// в dev-режиме) — без этого флага мы бы дважды дернули /api/me и задвоили логи.
if (initialized.current) return;
initialized.current = true;
@@ -45,6 +67,10 @@ export function AuthClient({ dict, lang }: AuthClientProps) {
const hash = window.location.hash;
let token = localStorage.getItem("nrxp_token");
// Токен может прийти двумя путями: как query-параметр (?token=...) или
// в hash-фрагменте (#token=...) — зависит от того, как именно бот/виджет
// сформировал редирект. Проверяем оба варианта и в любом случае вычищаем
// токен из URL через replaceState, чтобы он не остался в истории браузера.
const searchParams = new URLSearchParams(window.location.search);
const queryToken = searchParams.get("token");
@@ -105,6 +131,9 @@ export function AuthClient({ dict, lang }: AuthClientProps) {
authenticate();
}, [dict, lang, router]);
// Регистрируем глобальный колбэк для Telegram Login Widget: сам виджет
// подключается сторонним script-тегом ниже (см. следующий useEffect) и после
// логина в Telegram вызывает window.onTelegramAuth с данными пользователя.
useEffect(() => {
window.onTelegramAuth = async (user: any) => {
setStatus("processing");
@@ -138,6 +167,10 @@ export function AuthClient({ dict, lang }: AuthClientProps) {
};
}, [dict, lang, router]);
// Инжектим официальный Telegram Widget script только когда реально дошли до
// экрана "awaiting" (нет токена) — виджет сам рисует кнопку логина в контейнере
// и после успеха вызывает window.onTelegramAuth. widgetInjected защищает от
// повторной вставки script-тега при повторных ререндерах.
useEffect(() => {
if (
status === "awaiting" &&