203 lines
6.8 KiB
JavaScript
203 lines
6.8 KiB
JavaScript
'use client';
|
||
|
||
import { z as zod } from 'zod';
|
||
import { useState, Suspense } from 'react';
|
||
import { useForm } from 'react-hook-form';
|
||
import { zodResolver } from '@hookform/resolvers/zod';
|
||
import Link from '@mui/material/Link';
|
||
import Alert from '@mui/material/Alert';
|
||
import Stack from '@mui/material/Stack';
|
||
import IconButton from '@mui/material/IconButton';
|
||
import Typography from '@mui/material/Typography';
|
||
import LoadingButton from '@mui/lab/LoadingButton';
|
||
import InputAdornment from '@mui/material/InputAdornment';
|
||
|
||
import { paths } from 'src/routes/paths';
|
||
import { useRouter, useSearchParams } from 'src/routes/hooks';
|
||
import { RouterLink } from 'src/routes/components';
|
||
|
||
import { useBoolean } from 'src/hooks/use-boolean';
|
||
|
||
import { Iconify } from 'src/components/iconify';
|
||
import { Form, Field } from 'src/components/hook-form';
|
||
|
||
import { confirmPasswordReset } from 'src/auth/context/jwt';
|
||
|
||
// ----------------------------------------------------------------------
|
||
|
||
const ResetPasswordSchema = zod
|
||
.object({
|
||
newPassword: zod
|
||
.string()
|
||
.min(1, { message: 'Введите пароль!' })
|
||
.min(8, { message: 'Пароль должен содержать не менее 8 символов!' }),
|
||
newPasswordConfirm: zod.string().min(1, { message: 'Подтвердите пароль!' }),
|
||
})
|
||
.refine((data) => data.newPassword === data.newPasswordConfirm, {
|
||
message: 'Пароли не совпадают!',
|
||
path: ['newPasswordConfirm'],
|
||
});
|
||
|
||
// ----------------------------------------------------------------------
|
||
|
||
function ResetPasswordContent() {
|
||
const router = useRouter();
|
||
const searchParams = useSearchParams();
|
||
const token = searchParams.get('token');
|
||
|
||
const [errorMsg, setErrorMsg] = useState('');
|
||
const [successMsg, setSuccessMsg] = useState('');
|
||
|
||
const newPassword = useBoolean();
|
||
const newPasswordConfirm = useBoolean();
|
||
|
||
const methods = useForm({
|
||
resolver: zodResolver(ResetPasswordSchema),
|
||
defaultValues: { newPassword: '', newPasswordConfirm: '' },
|
||
});
|
||
|
||
const {
|
||
handleSubmit,
|
||
formState: { isSubmitting },
|
||
} = methods;
|
||
|
||
const onSubmit = handleSubmit(async (data) => {
|
||
if (!token) {
|
||
setErrorMsg('Ссылка для сброса отсутствует. Запросите новый сброс пароля.');
|
||
return;
|
||
}
|
||
try {
|
||
await confirmPasswordReset({
|
||
token,
|
||
newPassword: data.newPassword,
|
||
newPasswordConfirm: data.newPasswordConfirm,
|
||
});
|
||
setSuccessMsg('Пароль успешно изменён. Теперь вы можете войти с новым паролем.');
|
||
setErrorMsg('');
|
||
} catch (error) {
|
||
console.error(error);
|
||
const msg = error?.response?.data?.error?.message || error?.response?.data?.message || error?.response?.data?.detail || 'Не удалось сбросить пароль. Возможно, ссылка устарела — запросите новую.';
|
||
setErrorMsg(msg);
|
||
}
|
||
});
|
||
|
||
if (!token) {
|
||
return (
|
||
<>
|
||
<Stack spacing={1.5} sx={{ mb: 5 }}>
|
||
<Typography variant="h5">Сброс пароля</Typography>
|
||
</Stack>
|
||
<Alert severity="error" sx={{ mb: 3 }}>
|
||
Ссылка для сброса отсутствует. Перейдите по ссылке из письма или запросите новую.
|
||
</Alert>
|
||
<Link component={RouterLink} href={paths.auth.jwt.forgotPassword} variant="subtitle2">
|
||
Запросить новую ссылку
|
||
</Link>
|
||
</>
|
||
);
|
||
}
|
||
|
||
if (successMsg) {
|
||
return (
|
||
<>
|
||
<Stack spacing={1.5} sx={{ mb: 5 }}>
|
||
<Typography variant="h5">Сброс пароля</Typography>
|
||
</Stack>
|
||
<Alert severity="success" sx={{ mb: 3 }}>
|
||
{successMsg}
|
||
</Alert>
|
||
<Link component={RouterLink} href={paths.auth.jwt.signIn} variant="subtitle2">
|
||
Войти
|
||
</Link>
|
||
</>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<Stack spacing={1.5} sx={{ mb: 5 }}>
|
||
<Typography variant="h5">Новый пароль</Typography>
|
||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||
Введите новый пароль ниже.
|
||
</Typography>
|
||
</Stack>
|
||
|
||
{!!errorMsg && (
|
||
<Alert severity="error" sx={{ mb: 3 }}>
|
||
{errorMsg}
|
||
</Alert>
|
||
)}
|
||
|
||
<Form methods={methods} onSubmit={onSubmit}>
|
||
<Stack spacing={3}>
|
||
<Field.Text
|
||
name="newPassword"
|
||
label="Новый пароль"
|
||
placeholder="8+ символов"
|
||
type={newPassword.value ? 'text' : 'password'}
|
||
InputLabelProps={{ shrink: true }}
|
||
InputProps={{
|
||
endAdornment: (
|
||
<InputAdornment position="end">
|
||
<IconButton onClick={newPassword.onToggle} edge="end">
|
||
<Iconify icon={newPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'} />
|
||
</IconButton>
|
||
</InputAdornment>
|
||
),
|
||
}}
|
||
/>
|
||
|
||
<Field.Text
|
||
name="newPasswordConfirm"
|
||
label="Подтвердите пароль"
|
||
placeholder="8+ символов"
|
||
type={newPasswordConfirm.value ? 'text' : 'password'}
|
||
InputLabelProps={{ shrink: true }}
|
||
InputProps={{
|
||
endAdornment: (
|
||
<InputAdornment position="end">
|
||
<IconButton onClick={newPasswordConfirm.onToggle} edge="end">
|
||
<Iconify icon={newPasswordConfirm.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'} />
|
||
</IconButton>
|
||
</InputAdornment>
|
||
),
|
||
}}
|
||
/>
|
||
|
||
<LoadingButton
|
||
fullWidth
|
||
color="inherit"
|
||
size="large"
|
||
type="submit"
|
||
variant="contained"
|
||
loading={isSubmitting}
|
||
loadingIndicator="Сохранение..."
|
||
>
|
||
Сохранить пароль
|
||
</LoadingButton>
|
||
</Stack>
|
||
</Form>
|
||
|
||
<Link
|
||
component={RouterLink}
|
||
href={paths.auth.jwt.signIn}
|
||
variant="body2"
|
||
color="inherit"
|
||
sx={{ display: 'block', mt: 3, textAlign: 'center' }}
|
||
>
|
||
Вернуться ко входу
|
||
</Link>
|
||
</>
|
||
);
|
||
}
|
||
|
||
// ----------------------------------------------------------------------
|
||
|
||
export function JwtResetPasswordView() {
|
||
return (
|
||
<Suspense fallback={<Typography variant="body2">Загрузка...</Typography>}>
|
||
<ResetPasswordContent />
|
||
</Suspense>
|
||
);
|
||
}
|