From 558278b7296b6e97e52a39a16134011bc0bd8c52 Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 25 Jan 2026 10:43:36 +0300 Subject: [PATCH] Add files via upload --- src/pages/Login.tsx | 129 +++++++++++++++++++++++++- src/pages/ResetPassword.tsx | 175 ++++++++++++++++++++++++++++++++++++ 2 files changed, 301 insertions(+), 3 deletions(-) create mode 100644 src/pages/ResetPassword.tsx diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 45f5182..194f88c 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -3,6 +3,7 @@ import { useNavigate, useLocation, useSearchParams } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useQuery } from '@tanstack/react-query' import { useAuthStore } from '../store/auth' +import { authApi } from '../api/auth' import { brandingApi, getCachedBranding, setCachedBranding, preloadLogo, isLogoPreloaded, type BrandingInfo, type EmailAuthEnabled } from '../api/branding' import { getAndClearReturnUrl } from '../utils/token' import LanguageSwitcher from '../components/LanguageSwitcher' @@ -33,6 +34,11 @@ export default function Login() { const [isTelegramWebApp, setIsTelegramWebApp] = useState(false) const [logoLoaded, setLogoLoaded] = useState(() => isLogoPreloaded()) const [registeredEmail, setRegisteredEmail] = useState(null) + const [showForgotPassword, setShowForgotPassword] = useState(false) + const [forgotPasswordEmail, setForgotPasswordEmail] = useState('') + const [forgotPasswordSent, setForgotPasswordSent] = useState(false) + const [forgotPasswordLoading, setForgotPasswordLoading] = useState(false) + const [forgotPasswordError, setForgotPasswordError] = useState('') // Получаем URL для возврата после авторизации const getReturnUrl = useCallback(() => { @@ -186,6 +192,36 @@ export default function Login() { } } + const handleForgotPassword = async (e: React.FormEvent) => { + e.preventDefault() + setForgotPasswordError('') + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!forgotPasswordEmail.trim() || !emailRegex.test(forgotPasswordEmail.trim())) { + setForgotPasswordError(t('auth.invalidEmail', 'Please enter a valid email address')) + return + } + + setForgotPasswordLoading(true) + try { + await authApi.forgotPassword(forgotPasswordEmail.trim()) + setForgotPasswordSent(true) + } catch (err: unknown) { + const error = err as { response?: { status?: number; data?: { detail?: string } } } + const detail = error.response?.data?.detail + setForgotPasswordError(detail || t('common.error')) + } finally { + setForgotPasswordLoading(false) + } + } + + const closeForgotPasswordModal = () => { + setShowForgotPassword(false) + setForgotPasswordEmail('') + setForgotPasswordSent(false) + setForgotPasswordError('') + } + return (
{/* Background gradient */} @@ -446,15 +482,102 @@ export default function Login() { {/* Forgot password link - only for login */} {authMode === 'login' && ( -

- {t('auth.registerHint')} -

+
+ +
)}
)} )} + + {/* Forgot Password Modal */} + {showForgotPassword && ( +
+
+
+ + + {forgotPasswordSent ? ( +
+
+ + + +
+

+ {t('auth.checkEmail', 'Check your email')} +

+

+ {t('auth.passwordResetSent', 'If an account exists with this email, we sent password reset instructions.')} +

+ +
+ ) : ( + <> +

+ {t('auth.forgotPassword', 'Forgot password?')} +

+

+ {t('auth.forgotPasswordHint', 'Enter your email and we will send you instructions to reset your password.')} +

+ +
+
+ + setForgotPasswordEmail(e.target.value)} + placeholder="you@example.com" + className="input" + autoFocus + /> +
+ + {forgotPasswordError && ( +
+ {forgotPasswordError} +
+ )} + + +
+ + )} +
+
+ )}
) } diff --git a/src/pages/ResetPassword.tsx b/src/pages/ResetPassword.tsx new file mode 100644 index 0000000..debd7f2 --- /dev/null +++ b/src/pages/ResetPassword.tsx @@ -0,0 +1,175 @@ +import { useState } from 'react' +import { useSearchParams, Link, useNavigate } from 'react-router-dom' +import { useTranslation } from 'react-i18next' +import { authApi } from '../api/auth' +import LanguageSwitcher from '../components/LanguageSwitcher' + +export default function ResetPassword() { + const { t } = useTranslation() + const navigate = useNavigate() + const [searchParams] = useSearchParams() + const token = searchParams.get('token') + + const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [status, setStatus] = useState<'form' | 'loading' | 'success' | 'error'>('form') + const [error, setError] = useState('') + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError('') + + if (!token) { + setError(t('resetPassword.invalidToken', 'Invalid or missing reset token')) + return + } + + if (password.length < 8) { + setError(t('auth.passwordTooShort', 'Password must be at least 8 characters')) + return + } + + if (password !== confirmPassword) { + setError(t('auth.passwordMismatch', 'Passwords do not match')) + return + } + + setStatus('loading') + + try { + await authApi.resetPassword(token, password) + setStatus('success') + setTimeout(() => navigate('/login', { replace: true }), 2000) + } catch (err: unknown) { + setStatus('error') + const error = err as { response?: { data?: { detail?: string } } } + setError(error.response?.data?.detail || t('common.error')) + } + } + + if (!token) { + return ( +
+
+
+ +
+
+
+
!
+

+ {t('resetPassword.invalidToken', 'Invalid reset link')} +

+

+ {t('resetPassword.tokenExpiredOrInvalid', 'This password reset link is invalid or has expired.')} +

+ + {t('auth.backToLogin', 'Back to login')} + +
+
+
+ ) + } + + return ( +
+
+
+
+ +
+ +
+
+ {status === 'success' ? ( +
+
+ + + +
+

+ {t('resetPassword.success', 'Password changed!')} +

+

+ {t('resetPassword.redirectingToLogin', 'Redirecting to login...')} +

+
+
+ ) : ( + <> +

+ {t('resetPassword.title', 'Set new password')} +

+

+ {t('resetPassword.enterNewPassword', 'Enter your new password below.')} +

+ +
+
+ + setPassword(e.target.value)} + placeholder="••••••••" + className="input" + autoComplete="new-password" + disabled={status === 'loading'} + /> +
+ +
+ + setConfirmPassword(e.target.value)} + placeholder="••••••••" + className="input" + autoComplete="new-password" + disabled={status === 'loading'} + /> +
+ + {error && ( +
+ {error} +
+ )} + + +
+ +
+ + {t('auth.backToLogin', 'Back to login')} + +
+ + )} +
+
+
+ ) +}