add qr code auth
This commit is contained in:
@ -511,6 +511,59 @@ export async function toggleBonusActivation(
|
|||||||
return await response.json();
|
return await response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== QR AUTH =====
|
||||||
|
|
||||||
|
export type QrInitResponse = {
|
||||||
|
token: string;
|
||||||
|
qr_url: string;
|
||||||
|
expires_at: string; // ISO
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QrStatusPending = {
|
||||||
|
status: 'pending' | 'approved' | 'expired' | 'consumed';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QrStatusOk = {
|
||||||
|
status: 'ok';
|
||||||
|
accessToken: string;
|
||||||
|
clientToken: string;
|
||||||
|
selectedProfile: { id: string; name: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QrStatusResponse = QrStatusPending | QrStatusOk;
|
||||||
|
|
||||||
|
export async function qrInit(device_id?: string): Promise<QrInitResponse> {
|
||||||
|
const url = device_id
|
||||||
|
? `${API_BASE_URL}/auth/qr/init?device_id=${encodeURIComponent(device_id)}`
|
||||||
|
: `${API_BASE_URL}/auth/qr/init`;
|
||||||
|
|
||||||
|
const response = await fetch(url, { method: 'POST' });
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Не удалось создать QR-логин');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function qrStatus(
|
||||||
|
token: string,
|
||||||
|
device_id?: string,
|
||||||
|
): Promise<QrStatusResponse> {
|
||||||
|
const qs = new URLSearchParams({ token });
|
||||||
|
if (device_id) qs.set('device_id', device_id);
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_BASE_URL}/auth/qr/status?${qs.toString()}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Не удалось проверить статус QR-логина');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
// ===== КЕЙСЫ =====
|
// ===== КЕЙСЫ =====
|
||||||
|
|
||||||
export interface CaseItemMeta {
|
export interface CaseItemMeta {
|
||||||
|
|||||||
@ -1,10 +1,17 @@
|
|||||||
import { useState } from "react";
|
import { useState } from 'react';
|
||||||
import { Box, Button, TextField, Typography, InputAdornment, IconButton } from '@mui/material';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
InputAdornment,
|
||||||
|
IconButton,
|
||||||
|
} from '@mui/material';
|
||||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||||
import GradientTextField from '../GradientTextField';
|
import GradientTextField from '../GradientTextField';
|
||||||
import GradientVisibilityToggleIcon from '../../assets/Icons/GradientVisibilityToggleIcon'
|
import GradientVisibilityToggleIcon from '../../assets/Icons/GradientVisibilityToggleIcon';
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
interface AuthFormProps {
|
interface AuthFormProps {
|
||||||
config: {
|
config: {
|
||||||
@ -35,13 +42,13 @@ const AuthForm = ({ config, handleInputChange, onLogin }: AuthFormProps) => {
|
|||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
sx={{
|
sx={{
|
||||||
mt: '2.5vw',
|
mt: '2.5vw',
|
||||||
mb: '0vw'
|
mb: '0vw',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<GradientTextField
|
<GradientTextField
|
||||||
label="Пароль"
|
label="Пароль"
|
||||||
required
|
required
|
||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? 'text' : 'password'}
|
||||||
name="password"
|
name="password"
|
||||||
value={config.password}
|
value={config.password}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
@ -62,36 +69,40 @@ const AuthForm = ({ config, handleInputChange, onLogin }: AuthFormProps) => {
|
|||||||
onClick={() => setShowPassword((prev) => !prev)}
|
onClick={() => setShowPassword((prev) => !prev)}
|
||||||
edge="end"
|
edge="end"
|
||||||
sx={{
|
sx={{
|
||||||
color: "white",
|
color: 'white',
|
||||||
margin: '0',
|
margin: '0',
|
||||||
padding: '0',
|
padding: '0',
|
||||||
'& MuiTouchRipple-root css-r3djoj-MuiTouchRipple-root': {
|
'& MuiTouchRipple-root css-r3djoj-MuiTouchRipple-root': {
|
||||||
display: 'none',
|
display: 'none',
|
||||||
},
|
},
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<GradientVisibilityToggleIcon
|
<GradientVisibilityToggleIcon
|
||||||
crossed={showPassword} // когда type="text" -> перечеркнуть
|
crossed={showPassword} // когда type="text" -> перечеркнуть
|
||||||
sx={{ fontSize: "2.5vw", mr: '0.5vw' }}
|
sx={{ fontSize: '2.5vw', mr: '0.5vw' }}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button onClick={onLogin} variant="contained"
|
<Button
|
||||||
|
onClick={onLogin}
|
||||||
|
variant="contained"
|
||||||
sx={{
|
sx={{
|
||||||
transition: 'transform 0.3s ease',
|
transition: 'transform 0.3s ease',
|
||||||
width: '60%',
|
width: '60%',
|
||||||
mt: 2,
|
mt: 2,
|
||||||
background: 'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
|
background:
|
||||||
|
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
|
||||||
fontFamily: 'Benzin-Bold',
|
fontFamily: 'Benzin-Bold',
|
||||||
borderRadius: '2.5vw',
|
borderRadius: '2.5vw',
|
||||||
fontSize: '2vw',
|
fontSize: '2vw',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
transform: 'scale(1.02)',
|
transform: 'scale(1.02)',
|
||||||
|
|
||||||
},
|
},
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
Войти
|
Войти
|
||||||
</Button>
|
</Button>
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import { Box } from '@mui/material';
|
import { Box, Button, Typography } from '@mui/material';
|
||||||
import useAuth from '../hooks/useAuth';
|
import useAuth from '../hooks/useAuth';
|
||||||
import AuthForm from '../components/Login/AuthForm';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import PopaPopa from '../components/popa-popa';
|
import PopaPopa from '../components/popa-popa';
|
||||||
import useConfig from '../hooks/useConfig';
|
import useConfig from '../hooks/useConfig';
|
||||||
import { useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { FullScreenLoader } from '../components/FullScreenLoader';
|
import { FullScreenLoader } from '../components/FullScreenLoader';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@ -15,6 +14,14 @@ import {
|
|||||||
getNotifPositionFromSettings,
|
getNotifPositionFromSettings,
|
||||||
} from '../utils/notifications';
|
} from '../utils/notifications';
|
||||||
|
|
||||||
|
// как в registration
|
||||||
|
import QRCodeStyling from 'qr-code-styling';
|
||||||
|
import popalogo from '../../../assets/icons/popa-popa.svg';
|
||||||
|
import GradientTextField from '../components/GradientTextField';
|
||||||
|
|
||||||
|
// твои API методы
|
||||||
|
import { qrInit, qrStatus } from '../api';
|
||||||
|
|
||||||
interface LoginProps {
|
interface LoginProps {
|
||||||
onLoginSuccess?: (username: string) => void;
|
onLoginSuccess?: (username: string) => void;
|
||||||
}
|
}
|
||||||
@ -25,7 +32,30 @@ const Login = ({ onLoginSuccess }: LoginProps) => {
|
|||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
// Snackbar / Notification states (как в LaunchPage)
|
// ===== UI mode: по умолчанию QR, парольная форма показывается по кнопке =====
|
||||||
|
const [showPasswordLogin, setShowPasswordLogin] = useState(false);
|
||||||
|
|
||||||
|
// ===== QR =====
|
||||||
|
const [qrLoading, setQrLoading] = useState(false);
|
||||||
|
const [qrUrl, setQrUrl] = useState<string>('');
|
||||||
|
const qrRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const pollTimerRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
// хранит один инстанс QRCodeStyling
|
||||||
|
const qrInstanceRef = useRef<QRCodeStyling | null>(null);
|
||||||
|
|
||||||
|
const deviceId = useMemo(() => {
|
||||||
|
const key = 'qr_device_id';
|
||||||
|
const existing = localStorage.getItem(key);
|
||||||
|
if (existing) return existing;
|
||||||
|
const v = (
|
||||||
|
crypto?.randomUUID?.() ?? `${Date.now()}_${Math.random()}`
|
||||||
|
).toString();
|
||||||
|
localStorage.setItem(key, v);
|
||||||
|
return v;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// ===== Notifications =====
|
||||||
const [notifOpen, setNotifOpen] = useState(false);
|
const [notifOpen, setNotifOpen] = useState(false);
|
||||||
const [notifMsg, setNotifMsg] = useState<React.ReactNode>('');
|
const [notifMsg, setNotifMsg] = useState<React.ReactNode>('');
|
||||||
const [notifSeverity, setNotifSeverity] = useState<
|
const [notifSeverity, setNotifSeverity] = useState<
|
||||||
@ -49,16 +79,140 @@ const Login = ({ onLoginSuccess }: LoginProps) => {
|
|||||||
setNotifOpen(true);
|
setNotifOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stopQrPolling = () => {
|
||||||
|
if (pollTimerRef.current) {
|
||||||
|
window.clearInterval(pollTimerRef.current);
|
||||||
|
pollTimerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => stopQrPolling();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// создаём QR инстанс с теми же настройками, что в registration
|
||||||
|
useEffect(() => {
|
||||||
|
if (!qrInstanceRef.current) {
|
||||||
|
qrInstanceRef.current = new QRCodeStyling({
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
image: popalogo,
|
||||||
|
data: 'https://t.me/popa_popa_popa_bot?start=test',
|
||||||
|
shape: 'square',
|
||||||
|
margin: 10,
|
||||||
|
dotsOptions: {
|
||||||
|
gradient: {
|
||||||
|
type: 'linear',
|
||||||
|
colorStops: [
|
||||||
|
{ offset: 0, color: 'rgb(242,113,33)' },
|
||||||
|
{ offset: 1, color: 'rgb(233,64,87)' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
type: 'extra-rounded',
|
||||||
|
},
|
||||||
|
imageOptions: {
|
||||||
|
crossOrigin: 'anonymous',
|
||||||
|
margin: 20,
|
||||||
|
imageSize: 0.5,
|
||||||
|
},
|
||||||
|
backgroundOptions: {
|
||||||
|
color: 'transparent',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// аппендим QR в контейнер, когда мы на QR-экране
|
||||||
|
useEffect(() => {
|
||||||
|
if (showPasswordLogin) return;
|
||||||
|
if (!qrRef.current) return;
|
||||||
|
if (!qrInstanceRef.current) return;
|
||||||
|
|
||||||
|
while (qrRef.current.firstChild) {
|
||||||
|
qrRef.current.removeChild(qrRef.current.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
qrInstanceRef.current.append(qrRef.current);
|
||||||
|
}, [showPasswordLogin]);
|
||||||
|
|
||||||
|
// при изменении URL обновляем data в QR
|
||||||
|
useEffect(() => {
|
||||||
|
if (!qrInstanceRef.current) return;
|
||||||
|
if (!qrUrl) return;
|
||||||
|
|
||||||
|
qrInstanceRef.current.update({ data: qrUrl });
|
||||||
|
}, [qrUrl]);
|
||||||
|
|
||||||
|
const startQrLogin = async () => {
|
||||||
|
setQrLoading(true);
|
||||||
|
setQrUrl('');
|
||||||
|
stopQrPolling();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const init = await qrInit(deviceId);
|
||||||
|
setQrUrl(init.qr_url);
|
||||||
|
|
||||||
|
pollTimerRef.current = window.setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const res = await qrStatus(init.token, deviceId);
|
||||||
|
|
||||||
|
if (res.status === 'ok') {
|
||||||
|
stopQrPolling();
|
||||||
|
|
||||||
|
saveConfig({
|
||||||
|
accessToken: res.accessToken,
|
||||||
|
clientToken: res.clientToken,
|
||||||
|
username: res.selectedProfile?.name ?? config.username,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onLoginSuccess) {
|
||||||
|
onLoginSuccess(res.selectedProfile?.name ?? config.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification('Успешный вход через QR', 'success');
|
||||||
|
navigate('/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status === 'expired') {
|
||||||
|
stopQrPolling();
|
||||||
|
showNotification('QR-код истёк. Нажми “Обновить QR”.', 'warning');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// transient ошибки игнорим, следующий тик повторит
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
} catch (e: any) {
|
||||||
|
showNotification(e?.message || 'Не удалось запустить QR-вход', 'error');
|
||||||
|
} finally {
|
||||||
|
setQrLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// автозапуск QR при открытии страницы
|
||||||
|
useEffect(() => {
|
||||||
|
startQrLogin();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const goToPasswordLogin = () => {
|
||||||
|
stopQrPolling();
|
||||||
|
setShowPasswordLogin(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const backToQr = () => {
|
||||||
|
setShowPasswordLogin(false);
|
||||||
|
startQrLogin(); // перегенерим свежий QR при возврате
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== Password login =====
|
||||||
const mapAuthErrorToMessage = (error: any): string => {
|
const mapAuthErrorToMessage = (error: any): string => {
|
||||||
const raw = error?.message ? String(error.message) : String(error);
|
const raw = error?.message ? String(error.message) : String(error);
|
||||||
|
|
||||||
// сеть
|
|
||||||
if (raw.includes('Failed to fetch') || raw.includes('NetworkError')) {
|
if (raw.includes('Failed to fetch') || raw.includes('NetworkError')) {
|
||||||
return 'Сервер недоступен';
|
return 'Сервер недоступен';
|
||||||
}
|
}
|
||||||
|
|
||||||
// вытащим JSON после статуса
|
|
||||||
// пример: "Ошибка авторизации: 401 {"detail":"Invalid credentials"}"
|
|
||||||
const jsonStart = raw.indexOf('{');
|
const jsonStart = raw.indexOf('{');
|
||||||
if (jsonStart !== -1) {
|
if (jsonStart !== -1) {
|
||||||
const jsonStr = raw.slice(jsonStart);
|
const jsonStr = raw.slice(jsonStart);
|
||||||
@ -66,16 +220,14 @@ const Login = ({ onLoginSuccess }: LoginProps) => {
|
|||||||
const data = JSON.parse(jsonStr);
|
const data = JSON.parse(jsonStr);
|
||||||
const detail = data?.detail;
|
const detail = data?.detail;
|
||||||
|
|
||||||
if (detail === 'Invalid credentials') return 'Неверный логин или пароль';
|
if (detail === 'Invalid credentials')
|
||||||
|
return 'Неверный логин или пароль';
|
||||||
if (detail === 'User not verified') return 'Аккаунт не подтверждён';
|
if (detail === 'User not verified') return 'Аккаунт не подтверждён';
|
||||||
|
|
||||||
if (typeof detail === 'string' && detail.trim()) return detail;
|
if (typeof detail === 'string' && detail.trim()) return detail;
|
||||||
} catch {
|
} catch {}
|
||||||
// если это не JSON — идём дальше
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// если бэк вернул просто строку без JSON
|
|
||||||
if (raw.includes('Invalid credentials')) return 'Неверный логин или пароль';
|
if (raw.includes('Invalid credentials')) return 'Неверный логин или пароль';
|
||||||
if (raw.includes('User not verified')) return 'Аккаунт не подтверждён';
|
if (raw.includes('User not verified')) return 'Аккаунт не подтверждён';
|
||||||
|
|
||||||
@ -95,20 +247,17 @@ const Login = ({ onLoginSuccess }: LoginProps) => {
|
|||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// Проверяем существующий токен
|
|
||||||
if (config.accessToken && config.clientToken) {
|
if (config.accessToken && config.clientToken) {
|
||||||
const isValid = await auth.validateSession(config.accessToken);
|
const isValid = await auth.validateSession(config.accessToken);
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
// Пробуем обновить
|
|
||||||
const refreshed = await auth.refreshSession(
|
const refreshed = await auth.refreshSession(
|
||||||
config.accessToken,
|
config.accessToken,
|
||||||
config.clientToken,
|
config.clientToken,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!refreshed) {
|
if (!refreshed) {
|
||||||
// Нужна новая авторизация
|
await auth.authenticateUser(
|
||||||
const session = await auth.authenticateUser(
|
|
||||||
config.username,
|
config.username,
|
||||||
config.password,
|
config.password,
|
||||||
saveConfig,
|
saveConfig,
|
||||||
@ -116,21 +265,15 @@ const Login = ({ onLoginSuccess }: LoginProps) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Новая авторизация
|
await auth.authenticateUser(
|
||||||
const session = await auth.authenticateUser(
|
|
||||||
config.username,
|
config.username,
|
||||||
config.password,
|
config.password,
|
||||||
saveConfig,
|
saveConfig,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onLoginSuccess) {
|
if (onLoginSuccess) onLoginSuccess(config.username);
|
||||||
onLoginSuccess(config.username);
|
|
||||||
}
|
|
||||||
|
|
||||||
// (опционально) можно показать успех
|
|
||||||
showNotification('Успешный вход', 'success');
|
showNotification('Успешный вход', 'success');
|
||||||
|
|
||||||
navigate('/');
|
navigate('/');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Ошибка авторизации:', error);
|
console.error('Ошибка авторизации:', error);
|
||||||
@ -147,18 +290,181 @@ const Login = ({ onLoginSuccess }: LoginProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const gradientTextSx = {
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
fontSize: '1.5vw',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.08em',
|
||||||
|
cursor: 'pointer',
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient(71deg, #F27121 0%, #E940CD 60%, #8A2387 100%)',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
WebkitTextFillColor: 'transparent',
|
||||||
|
textShadow: '0 0 15px rgba(0,0,0,0.9)',
|
||||||
|
'&:hover': { opacity: 1 },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const primaryButtonSx = {
|
||||||
|
transition: 'transform 0.3s ease',
|
||||||
|
width: '60%',
|
||||||
|
mt: 2,
|
||||||
|
background: 'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
borderRadius: '2.5vw',
|
||||||
|
fontSize: '2vw',
|
||||||
|
'&:hover': { transform: 'scale(1.1)' },
|
||||||
|
} as const;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '1vh',
|
||||||
|
width: '50vw',
|
||||||
|
mx: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<FullScreenLoader message="Входим..." />
|
<FullScreenLoader message="Входим..." />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<PopaPopa />
|
<PopaPopa />
|
||||||
<AuthForm
|
|
||||||
config={config}
|
{/* ===== QR экран по умолчанию ===== */}
|
||||||
handleInputChange={handleInputChange}
|
{!showPasswordLogin ? (
|
||||||
onLogin={handleLogin}
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
fontSize: '2vw',
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient( 136deg, rgb(242,113,33) 0%, rgb(233,64,87) 50%, rgb(138,35,135) 100%)',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
WebkitTextFillColor: 'transparent',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Вход через Telegram
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
sx={primaryButtonSx}
|
||||||
|
onClick={() => qrUrl && window.open(qrUrl, '_blank')}
|
||||||
|
disabled={!qrUrl}
|
||||||
|
>
|
||||||
|
Открыть бота
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* QR контейнер */}
|
||||||
|
<div ref={qrRef} style={{ minHeight: 300 }} />
|
||||||
|
|
||||||
|
{qrLoading ? (
|
||||||
|
<FullScreenLoader fullScreen={false} />
|
||||||
|
) : (
|
||||||
|
<Typography variant="body1">
|
||||||
|
Ждем подтверждения в боте
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '2vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography onClick={startQrLogin} sx={gradientTextSx}>
|
||||||
|
Обновить QR
|
||||||
|
</Typography>
|
||||||
|
<Typography onClick={goToPasswordLogin} sx={gradientTextSx}>
|
||||||
|
Войти по логину и паролю
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
onClick={() => navigate('/registration')}
|
||||||
|
sx={gradientTextSx}
|
||||||
|
>
|
||||||
|
Зарегистрироваться
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
/* ===== экран логин/пароль (в стиле Registration.tsx) ===== */
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
fontSize: '2vw',
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient( 136deg, rgb(242,113,33) 0%, rgb(233,64,87) 50%, rgb(138,35,135) 100%)',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
WebkitTextFillColor: 'transparent',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Вход по логину и паролю
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<GradientTextField
|
||||||
|
label="Никнейм"
|
||||||
|
required
|
||||||
|
name="username"
|
||||||
|
value={config.username}
|
||||||
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<GradientTextField
|
||||||
|
label="Пароль"
|
||||||
|
required
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
value={config.password}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
sx={primaryButtonSx}
|
||||||
|
onClick={handleLogin}
|
||||||
|
>
|
||||||
|
Войти
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '2vh',
|
||||||
|
mt: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
onClick={() => navigate('/registration')}
|
||||||
|
sx={gradientTextSx}
|
||||||
|
>
|
||||||
|
Зарегистрироваться
|
||||||
|
</Typography>
|
||||||
|
<Typography onClick={backToQr} sx={gradientTextSx}>
|
||||||
|
Назад к QR
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user