import { useState, useEffect, useRef } from 'react' import { useNavigate } from 'react-router-dom' import { useMutation } from '@tanstack/react-query' import { toast } from 'sonner' import { useAuthStore } from '@/stores/authStore' import { authApi } from '@/api/auth' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Label } from '@/components/ui/label' import type { CodeStatusResponse } from '@/types/models' const TELEGRAM_BOT_USERNAME = 'mnemo_cards_bot' const TELEGRAM_BOT_DEEP_LINK_BASE = `https://t.me/${TELEGRAM_BOT_USERNAME}` function telegramBotDeepLink(code: string): string { return `${TELEGRAM_BOT_DEEP_LINK_BASE}?start=${code}` } export default function LoginPage() { const navigate = useNavigate() const { login } = useAuthStore() const [code, setCode] = useState('') const [codeStatus, setCodeStatus] = useState(null) const [countdown, setCountdown] = useState(0) const [isVerifying, setIsVerifying] = useState(false) const statusPollIntervalRef = useRef(null) const countdownIntervalRef = useRef(null) // Request code mutation const requestCodeMutation = useMutation({ mutationFn: () => authApi.requestCode(), onSuccess: (data) => { if (data.success && data.code) { setCode(data.code) setCodeStatus({ success: true, code: data.code, status: data.status || 'pending', expiresAt: data.expiresAt, remainingSeconds: data.remainingSeconds || 600, isClaimed: false, isUsed: false, }) toast.success('Code generated! Send it to the Telegram bot.') // Open Telegram bot window.open(telegramBotDeepLink(data.code), '_blank') // Start polling for status startStatusPolling(data.code) startCountdown(data.remainingSeconds || 600) } else { toast.error(data.message || 'Failed to generate code') } }, onError: (error: unknown) => { const errorMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message || (error as { message?: string })?.message || 'Failed to generate code' toast.error(errorMessage) }, }) // Verify code mutation const verifyCodeMutation = useMutation({ mutationFn: (code: string) => authApi.verifyCode(code), onSuccess: (data) => { console.log('Verify code response:', data) // Check if response has success field and it's false if (data.success === false) { const errorMsg = data.message || 'Verification failed' console.error('Verification failed:', errorMsg) toast.error(errorMsg) setIsVerifying(false) // Don't restart polling if verification failed - user needs to generate new code return } // Validate response structure if (!data.token || !data.user) { console.error('Invalid response structure:', data) toast.error('Invalid response from server') setIsVerifying(false) // Don't restart polling - this is a server error return } // Stop polling stopStatusPolling() stopCountdown() setIsVerifying(false) console.log('Login successful, setting token and user') console.log('Token received:', data.token ? `Token exists, length: ${data.token.length}` : 'NO TOKEN!') console.log('User received:', data.user) // Save token first login(data.token, data.user) // Verify token was saved const savedToken = localStorage.getItem('admin_token') console.log('Token saved verification:', savedToken ? `Token exists, length: ${savedToken.length}` : 'Token NOT saved!') if (!savedToken) { console.error('CRITICAL: Token was not saved to localStorage!') toast.error('Failed to save authentication token') return } toast.success('Welcome back!') // Small delay to ensure token is available for subsequent requests setTimeout(() => { navigate('/') }, 100) }, onError: (error: unknown) => { console.error('Verify code error:', error) setIsVerifying(false) const errorResponse = error as { response?: { data?: { message?: string }; status?: number } } const errorMessage = errorResponse?.response?.data?.message || (error as { message?: string })?.message || 'Invalid code' // If 403, it means code was claimed but user is not admin if (errorResponse?.response?.status === 403) { toast.error('Access denied: You are not an admin. Code was claimed but verification failed.') stopStatusPolling() stopCountdown() } else { toast.error(errorMessage) // Restart polling in case of other errors if (code) { startStatusPolling(code) } } }, }) const startStatusPolling = (code: string) => { stopStatusPolling() // Poll immediately checkCodeStatus(code) // Then poll every 3 seconds statusPollIntervalRef.current = setInterval(() => { checkCodeStatus(code) }, 3000) } const stopStatusPolling = () => { if (statusPollIntervalRef.current) { clearInterval(statusPollIntervalRef.current) statusPollIntervalRef.current = null } } const checkCodeStatus = async (codeToCheck: string) => { try { const status = await authApi.getCodeStatus(codeToCheck) // Check if response is successful if (!status.success) { console.error('Code status check failed:', status.message || status.error) if (status.error === 'NotFound') { stopStatusPolling() stopCountdown() toast.error('Code not found. Please generate a new one.') } return } setCodeStatus(status) console.log('Code status:', status.status, 'isClaimed:', status.isClaimed, 'isUsed:', status.isUsed) if (status.status === 'claimed' && !status.isUsed && !isVerifying) { // Code is claimed, automatically verify and login console.log('Code claimed, attempting login...') setIsVerifying(true) stopStatusPolling() verifyCodeMutation.mutate(codeToCheck) } else if (status.status === 'expired' || (status.remainingSeconds !== undefined && status.remainingSeconds <= 0)) { stopStatusPolling() stopCountdown() toast.error('Code expired. Please generate a new one.') } else if (status.isUsed) { stopStatusPolling() stopCountdown() toast.info('Code already used') } else { // Update countdown if (status.remainingSeconds !== undefined && status.remainingSeconds > 0) { setCountdown(status.remainingSeconds) } } } catch (error) { console.error('Failed to check code status:', error) // Don't stop polling on network errors, just log } } const startCountdown = (initialSeconds: number) => { stopCountdown() setCountdown(initialSeconds) countdownIntervalRef.current = setInterval(() => { setCountdown((prev) => { if (prev <= 1) { stopCountdown() return 0 } return prev - 1 }) }, 1000) } const stopCountdown = () => { if (countdownIntervalRef.current) { clearInterval(countdownIntervalRef.current) countdownIntervalRef.current = null } } useEffect(() => { return () => { stopStatusPolling() stopCountdown() } }, []) const handleRequestCode = () => { requestCodeMutation.mutate() } const handleVerifyCode = (e: React.FormEvent) => { e.preventDefault() const trimmedCode = code.trim() if (!trimmedCode) { toast.error('Please enter verification code') return } // Validate code format: should be 6 digits if (!/^\d{6}$/.test(trimmedCode)) { toast.error('Code must be 6 digits') return } verifyCodeMutation.mutate(trimmedCode) } const handleBack = () => { stopStatusPolling() stopCountdown() setIsVerifying(false) setCode('') setCodeStatus(null) setCountdown(0) } const formatTime = (seconds: number): string => { const mins = Math.floor(seconds / 60) const secs = seconds % 60 return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}` } const getStatusLabel = (status: string): string => { switch (status) { case 'claimed': return 'Code confirmed. Logging in...' case 'used': return 'Code already used' case 'expired': return 'Code expired' case 'pending': default: return 'Waiting for code to be sent in bot...' } } const isCodeActive = codeStatus && codeStatus.status !== 'expired' && codeStatus.status !== 'used' && countdown > 0 return (
Mnemo Cards Admin {codeStatus ? 'Send the code to the Telegram bot to continue' : 'Click the button below to generate a verification code' } {!codeStatus ? (
) : (
{/* Code Display */}
{/* Status Card */} {codeStatus && (
Status: {isVerifying ? 'Verifying and logging in...' : getStatusLabel(codeStatus.status)}
{isCodeActive && (
Time remaining: {formatTime(countdown)}
)} {/* Debug info */} {process.env.NODE_ENV === 'development' && (
Status: {codeStatus.status}
Claimed: {codeStatus.isClaimed ? 'Yes' : 'No'}
Used: {codeStatus.isUsed ? 'Yes' : 'No'}
Verifying: {isVerifying ? 'Yes' : 'No'}
)}
)} {/* Manual Code Input (fallback) */}
) => { const value = e.target.value.replace(/\D/g, '') setCode(value) }} maxLength={6} disabled={verifyCodeMutation.isPending} autoComplete="one-time-code" />
{/* Open Bot Button */}
)}
) }