fix(auth): add forgot password flow and fix OAuth URL clearing

This commit is contained in:
Kirill
2026-05-22 14:47:06 +05:00
parent 68bbbf8895
commit b1530ef705
3 changed files with 193 additions and 3 deletions
+1
View File
@@ -0,0 +1 @@
export { AuthForgotForm } from './ui/AuthForgotForm'
@@ -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<Step>('request')
const { register, watch } = useForm<FormValues>({
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 (
<Stack spacing={2}>
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center' }}>
{step === 'request'
? 'Введите email, на который будет отправлен код для сброса пароля'
: 'Введите код и новый пароль'}
</Typography>
<TextField
label="Email"
{...register('email')}
disabled={step === 'reset'}
fullWidth
slotProps={{
input: {
startAdornment: (
<InputAdornment position="start">
<Mail size={18} />
</InputAdornment>
),
},
}}
/>
{step === 'reset' && (
<>
<TextField label="Код (6 цифр)" inputMode="numeric" {...register('code')} fullWidth />
<TextField
label="Новый пароль"
type="password"
{...register('newPassword')}
fullWidth
slotProps={{
input: {
startAdornment: (
<InputAdornment position="start">
<Lock size={18} />
</InputAdornment>
),
},
}}
/>
<TextField
label="Подтверждение пароля"
type="password"
{...register('passwordConfirm')}
fullWidth
error={Boolean(passwordError)}
helperText={passwordError}
/>
</>
)}
{step === 'request' ? (
<Button
variant="contained"
disabled={!email || forgotCodeMutation.isPending}
onClick={() => forgotCodeMutation.mutate()}
>
Отправить код
</Button>
) : (
<Button
variant="contained"
disabled={
!code ||
code.length !== 6 ||
!newPassword ||
newPassword.length < 8 ||
Boolean(passwordError) ||
resetPasswordMutation.isPending
}
onClick={() => resetPasswordMutation.mutate()}
>
Сменить пароль
</Button>
)}
<Button variant="text" size="small" onClick={onBack}>
Назад к входу
</Button>
{(forgotCodeMutation.error || resetPasswordMutation.error) && (
<TextField
error
helperText={getApiErrorMessage(forgotCodeMutation.error) || getApiErrorMessage(resetPasswordMutation.error)}
sx={{ display: 'none' }}
/>
)}
</Stack>
)
}