Add files via upload

This commit is contained in:
Egor
2026-01-23 11:33:26 +03:00
committed by GitHub
parent f5bd2544c9
commit c8d94251e9
2 changed files with 168 additions and 59 deletions

View File

@@ -12,10 +12,13 @@ export default function Login() {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const { isAuthenticated, loginWithTelegram, loginWithEmail } = useAuthStore() const { isAuthenticated, loginWithTelegram, loginWithEmail, registerWithEmail } = useAuthStore()
const [activeTab, setActiveTab] = useState<'telegram' | 'email'>('telegram') const [activeTab, setActiveTab] = useState<'telegram' | 'email'>('telegram')
const [authMode, setAuthMode] = useState<'login' | 'register'>('login')
const [email, setEmail] = useState('') const [email, setEmail] = useState('')
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [firstName, setFirstName] = useState('')
const [error, setError] = useState('') const [error, setError] = useState('')
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [isTelegramWebApp, setIsTelegramWebApp] = useState(false) const [isTelegramWebApp, setIsTelegramWebApp] = useState(false)
@@ -94,22 +97,49 @@ export default function Login() {
tryTelegramAuth() tryTelegramAuth()
}, [loginWithTelegram, navigate, t, getReturnUrl]) }, [loginWithTelegram, navigate, t, getReturnUrl])
const handleEmailLogin = async (e: React.FormEvent) => { const handleEmailSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
setError('') setError('')
// Валидация email
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!email.trim() || !emailRegex.test(email.trim())) {
setError(t('auth.invalidEmail', 'Please enter a valid email address'))
return
}
if (authMode === 'register') {
// Валидация для регистрации
if (password !== confirmPassword) {
setError(t('auth.passwordMismatch', 'Passwords do not match'))
return
}
if (password.length < 8) {
setError(t('auth.passwordTooShort', 'Password must be at least 8 characters'))
return
}
}
setIsLoading(true) setIsLoading(true)
try { try {
await loginWithEmail(email, password) if (authMode === 'login') {
await loginWithEmail(email, password)
} else {
await registerWithEmail(email, password, firstName || undefined)
}
navigate(getReturnUrl(), { replace: true }) navigate(getReturnUrl(), { replace: true })
} catch (err: unknown) { } catch (err: unknown) {
const error = err as { response?: { status?: number; data?: { detail?: string } } } const error = err as { response?: { status?: number; data?: { detail?: string } } }
// Show user-friendly error messages without exposing sensitive server details
const status = error.response?.status const status = error.response?.status
if (status === 401 || status === 403) { const detail = error.response?.data?.detail
setError(t('auth.invalidCredentials', 'Неверный email или пароль'))
if (status === 400 && detail?.includes('already registered')) {
setError(t('auth.emailAlreadyRegistered', 'This email is already registered'))
} else if (status === 401 || status === 403) {
setError(t('auth.invalidCredentials', 'Invalid email or password'))
} else if (status === 429) { } else if (status === 429) {
setError(t('auth.tooManyAttempts', 'Слишком много попыток. Попробуйте позже')) setError(t('auth.tooManyAttempts', 'Too many attempts. Please try again later'))
} else { } else {
setError(t('common.error')) setError(t('common.error'))
} }
@@ -205,60 +235,137 @@ export default function Login() {
)} )}
</div> </div>
) : ( ) : (
<form className="space-y-5" onSubmit={handleEmailLogin}> <div className="space-y-5">
<div> {/* Login / Register toggle */}
<label htmlFor="email" className="label"> <div className="flex bg-dark-800 rounded-lg p-1">
{t('auth.email')} <button
</label> type="button"
<input className={`flex-1 py-2 text-sm font-medium rounded-md transition-all ${
id="email" authMode === 'login'
name="email" ? 'bg-accent-500 text-white'
type="email" : 'text-dark-400 hover:text-dark-200'
autoComplete="email" }`}
required onClick={() => setAuthMode('login')}
className="input" >
placeholder="you@example.com" {t('auth.login')}
value={email} </button>
onChange={(e) => setEmail(e.target.value)} <button
/> type="button"
className={`flex-1 py-2 text-sm font-medium rounded-md transition-all ${
authMode === 'register'
? 'bg-accent-500 text-white'
: 'text-dark-400 hover:text-dark-200'
}`}
onClick={() => setAuthMode('register')}
>
{t('auth.register', 'Register')}
</button>
</div> </div>
<div> <form className="space-y-4" onSubmit={handleEmailSubmit}>
<label htmlFor="password" className="label"> {/* First name field - only for registration */}
{t('auth.password')} {authMode === 'register' && (
</label> <div>
<input <label htmlFor="firstName" className="label">
id="password" {t('auth.firstName', 'First Name')}
name="password" </label>
type="password" <input
autoComplete="current-password" id="firstName"
required name="firstName"
className="input" type="text"
placeholder="••••••••" autoComplete="given-name"
value={password} className="input"
onChange={(e) => setPassword(e.target.value)} placeholder={t('auth.firstNamePlaceholder', 'Your name (optional)')}
/> value={firstName}
</div> onChange={(e) => setFirstName(e.target.value)}
/>
<button </div>
type="submit"
disabled={isLoading}
className="btn-primary w-full py-3"
>
{isLoading ? (
<span className="flex items-center justify-center gap-2">
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
{t('common.loading')}
</span>
) : (
t('auth.login')
)} )}
</button>
<p className="text-center text-xs text-dark-500"> <div>
{t('auth.registerHint')} <label htmlFor="email" className="label">
</p> {t('auth.email')}
</form> </label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
className="input"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<label htmlFor="password" className="label">
{t('auth.password')}
</label>
<input
id="password"
name="password"
type="password"
autoComplete={authMode === 'login' ? 'current-password' : 'new-password'}
required
className="input"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
{/* Confirm password - only for registration */}
{authMode === 'register' && (
<div>
<label htmlFor="confirmPassword" className="label">
{t('auth.confirmPassword', 'Confirm Password')}
</label>
<input
id="confirmPassword"
name="confirmPassword"
type="password"
autoComplete="new-password"
required
className="input"
placeholder="••••••••"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
</div>
)}
<button
type="submit"
disabled={isLoading}
className="btn-primary w-full py-3"
>
{isLoading ? (
<span className="flex items-center justify-center gap-2">
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
{t('common.loading')}
</span>
) : (
authMode === 'login' ? t('auth.login') : t('auth.register', 'Register')
)}
</button>
</form>
{/* Verification notice for registration */}
{authMode === 'register' && (
<p className="text-center text-xs text-dark-500">
{t('auth.verificationEmailNotice', 'After registration, a verification email will be sent to your address')}
</p>
)}
{/* Forgot password link - only for login */}
{authMode === 'login' && (
<p className="text-center text-xs text-dark-500">
{t('auth.registerHint')}
</p>
)}
</div>
)} )}
</div> </div>
</div> </div>

View File

@@ -74,8 +74,10 @@ export default function Profile() {
setError(null) setError(null)
setSuccess(null) setSuccess(null)
if (!email.trim()) { // Валидация email
setError(t('profile.emailRequired')) const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!email.trim() || !emailRegex.test(email.trim())) {
setError(t('profile.invalidEmail', 'Please enter a valid email address'))
return return
} }