From b1530ef7054b7665c5a975e64e76f8e390fbbd33 Mon Sep 17 00:00:00 2001 From: Kirill Date: Fri, 22 May 2026 14:47:06 +0500 Subject: [PATCH] fix(auth): add forgot password flow and fix OAuth URL clearing --- client/src/features/auth-forgot/index.ts | 1 + .../auth-forgot/ui/AuthForgotForm.tsx | 145 ++++++++++++++++++ client/src/pages/auth/ui/AuthPage.tsx | 50 +++++- 3 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 client/src/features/auth-forgot/index.ts create mode 100644 client/src/features/auth-forgot/ui/AuthForgotForm.tsx diff --git a/client/src/features/auth-forgot/index.ts b/client/src/features/auth-forgot/index.ts new file mode 100644 index 0000000..81d088a --- /dev/null +++ b/client/src/features/auth-forgot/index.ts @@ -0,0 +1 @@ +export { AuthForgotForm } from './ui/AuthForgotForm' diff --git a/client/src/features/auth-forgot/ui/AuthForgotForm.tsx b/client/src/features/auth-forgot/ui/AuthForgotForm.tsx new file mode 100644 index 0000000..f769891 --- /dev/null +++ b/client/src/features/auth-forgot/ui/AuthForgotForm.tsx @@ -0,0 +1,145 @@ +import { useState } from 'react' +import Button from '@mui/material/Button' +import InputAdornment from '@mui/material/InputAdornment' +import Stack from '@mui/material/Stack' +import TextField from '@mui/material/TextField' +import Typography from '@mui/material/Typography' +import { useMutation } from '@tanstack/react-query' +import { Lock, Mail } from 'lucide-react' +import { useForm } from 'react-hook-form' +import { apiClient } from '@/shared/api/client' +import { getApiErrorMessage } from '@/shared/lib/get-api-error-message' + +type Step = 'request' | 'reset' + +type FormValues = { + email: string + code: string + newPassword: string + passwordConfirm: string +} + +type Props = { + onBack: () => void +} + +export function AuthForgotForm({ onBack }: Props) { + const [step, setStep] = useState('request') + + const { register, watch } = useForm({ + defaultValues: { email: '', code: '', newPassword: '', passwordConfirm: '' }, + mode: 'onChange', + }) + + const email = watch('email') + const code = watch('code') + const newPassword = watch('newPassword') + const passwordConfirm = watch('passwordConfirm') + + const forgotCodeMutation = useMutation({ + mutationFn: async () => { + await apiClient.post('auth/forgot-password', { email }) + }, + onSuccess: () => setStep('reset'), + }) + + const resetPasswordMutation = useMutation({ + mutationFn: async () => { + await apiClient.post('auth/reset-password', { email, code, newPassword }) + }, + }) + + const passwordError = newPassword && passwordConfirm && newPassword !== passwordConfirm ? 'Пароли не совпадают' : null + + return ( + + + {step === 'request' + ? 'Введите email, на который будет отправлен код для сброса пароля' + : 'Введите код и новый пароль'} + + + + + + ), + }, + }} + /> + + {step === 'reset' && ( + <> + + + + + ), + }, + }} + /> + + + )} + + {step === 'request' ? ( + + ) : ( + + )} + + + + {(forgotCodeMutation.error || resetPasswordMutation.error) && ( + + )} + + ) +} diff --git a/client/src/pages/auth/ui/AuthPage.tsx b/client/src/pages/auth/ui/AuthPage.tsx index 4d404a2..12739bd 100644 --- a/client/src/pages/auth/ui/AuthPage.tsx +++ b/client/src/pages/auth/ui/AuthPage.tsx @@ -9,6 +9,7 @@ import Typography from '@mui/material/Typography' import { useUnit } from 'effector-react' import { useNavigate, useSearchParams } from 'react-router-dom' import { AuthCodeForm } from '@/features/auth-code' +import { AuthForgotForm } from '@/features/auth-forgot' import { OAuthButtons } from '@/features/auth-oauth' import { AuthPasswordForm } from '@/features/auth-password' import { $user } from '@/shared/model/auth' @@ -19,6 +20,7 @@ export function AuthPage() { const [message, setMessage] = useState(null) const [oauthError, setOauthError] = useState(null) const [tab, setTab] = useState(0) + const [showForgot, setShowForgot] = useState(false) const [searchParams, setSearchParams] = useSearchParams() const navigate = useNavigate() const user = useUnit($user) @@ -27,10 +29,46 @@ export function AuthPage() { if (user) navigate('/', { replace: true }) }, [navigate, user]) - const oauthErrorParam = searchParams.get('oauthError') - if (oauthErrorParam) { - setOauthError(oauthErrorParam) + useEffect(() => { + const err = searchParams.get('oauthError') + if (!err) return + setOauthError(err) setSearchParams({}, { replace: true }) + }, [searchParams, setSearchParams]) + + if (showForgot) { + return ( + + + + + + + + Восстановление пароля + + + + setShowForgot(false)} /> + + + + ) } return ( @@ -104,6 +142,12 @@ export function AuthPage() { {tab === 0 && navigate('/', { replace: true })} />} {tab === 1 && navigate('/', { replace: true })} />} + + + +