feat: recover password flow
This commit is contained in:
@@ -12,6 +12,7 @@ import { nanoid } from '@reduxjs/toolkit'
|
|||||||
import { AppwriteException } from 'appwrite'
|
import { AppwriteException } from 'appwrite'
|
||||||
import { login } from 'entgamers-database/frontend/session'
|
import { login } from 'entgamers-database/frontend/session'
|
||||||
import { useFormik } from 'formik'
|
import { useFormik } from 'formik'
|
||||||
|
import NextLink from 'next/link'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { useEffect, type FC } from 'react'
|
import { useEffect, type FC } from 'react'
|
||||||
import { object, string } from 'yup'
|
import { object, string } from 'yup'
|
||||||
@@ -105,6 +106,9 @@ const LoginForm: FC = () => {
|
|||||||
<Typography variant="caption" color="danger">{formik.errors.password}</Typography>
|
<Typography variant="caption" color="danger">{formik.errors.password}</Typography>
|
||||||
)}
|
)}
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
<Typography variant="caption" color="muted">
|
||||||
|
Perdiste tu contraseña? <NextLink href="/recover-password">Recuperala</NextLink>
|
||||||
|
</Typography>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
'use client'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
import Typography from '@/components/ui/Typography'
|
||||||
|
import FormGroup from '@/components/ui/form/FormGroup'
|
||||||
|
import Input from '@/components/ui/form/Input'
|
||||||
|
import { useAppDispatch } from '@/hooks/useAppDispatch'
|
||||||
|
import { addAlert } from '@/state/feedbackSlice'
|
||||||
|
import { nanoid } from '@reduxjs/toolkit'
|
||||||
|
import { AppwriteException } from 'appwrite'
|
||||||
|
import { createPasswordRecovery } from 'entgamers-database/frontend/session'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import { type FC } from 'react'
|
||||||
|
import { object, string } from 'yup'
|
||||||
|
|
||||||
|
interface RecoverPasswordData {
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const recoverPasswordSchema = object({
|
||||||
|
email: string().email('El correo electrónico no es válido').required('El correo electrónico es requerido')
|
||||||
|
})
|
||||||
|
|
||||||
|
const CreateRecoverPasswordForm: FC = () => {
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
const formik = useFormik<RecoverPasswordData>({
|
||||||
|
initialValues: {
|
||||||
|
email: ''
|
||||||
|
},
|
||||||
|
onSubmit: async ({ email }) => {
|
||||||
|
try {
|
||||||
|
const callBackUrl = `${process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000'}/recover-password`
|
||||||
|
await createPasswordRecovery(email, callBackUrl)
|
||||||
|
dispatch(addAlert({
|
||||||
|
id: nanoid(),
|
||||||
|
title: 'Solicitud de recuperación de contraseña enviada',
|
||||||
|
message: 'Si el correo electrónico está registrado, se enviarán instrucciones para la recuperación de contraseña',
|
||||||
|
severity: 'success'
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AppwriteException) {
|
||||||
|
dispatch(addAlert({
|
||||||
|
id: nanoid(),
|
||||||
|
message: error.message,
|
||||||
|
title: 'Error mientras se registraba',
|
||||||
|
severity: 'error'
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
dispatch(addAlert({
|
||||||
|
id: nanoid(),
|
||||||
|
message: 'Error desconocido',
|
||||||
|
title: 'Error mientras se solicitaba la recuperación de contraseña',
|
||||||
|
severity: 'error'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
validationSchema: recoverPasswordSchema,
|
||||||
|
isInitialValid: false
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
>
|
||||||
|
<FormGroup>
|
||||||
|
<label htmlFor="email">
|
||||||
|
Correo electrónico
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
value={formik.values.email}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
status={formik.touched.email !== undefined && formik.errors.email !== undefined ? 'danger' : 'success'}
|
||||||
|
/>
|
||||||
|
{formik.touched.email !== undefined && formik.errors.email !== undefined
|
||||||
|
? (
|
||||||
|
<Typography variant="caption" color="danger">
|
||||||
|
{formik.errors.email}
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
: (<Typography variant="caption" color="info">
|
||||||
|
Por favor, introduce el correo electrónico con el que te has registrado. Te enviaremos un correo con instrucciones para la recuperación de contraseña
|
||||||
|
</Typography>)
|
||||||
|
}
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={formik.isSubmitting}
|
||||||
|
>
|
||||||
|
Enviar correo de recuperación
|
||||||
|
</Button>
|
||||||
|
</FormGroup>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateRecoverPasswordForm
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
'use client'
|
||||||
|
import CreateRecoverPasswordForm from '@/app/recover-password/CreateRecoverPasswordForm'
|
||||||
|
import { useSearchParams } from 'next/navigation'
|
||||||
|
import { useEffect, useState, type FC } from 'react'
|
||||||
|
import UpdateRecoverPasswordForm, { type UpdateRecoverPasswordFormProps } from './UpdateRecoverPasswordForm'
|
||||||
|
|
||||||
|
const ManageRecoverPassword: FC = () => {
|
||||||
|
const [recoverData, setRecoverData] = useState<UpdateRecoverPasswordFormProps | undefined>()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const userId = searchParams.get('userId')
|
||||||
|
const secret = searchParams.get('secret')
|
||||||
|
if (userId !== null && secret !== null) {
|
||||||
|
setRecoverData({ userId, secret })
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (recoverData === undefined) {
|
||||||
|
return <CreateRecoverPasswordForm />
|
||||||
|
} else {
|
||||||
|
return <UpdateRecoverPasswordForm
|
||||||
|
{...recoverData}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageRecoverPassword
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
import Typography from '@/components/ui/Typography'
|
||||||
|
import FormGroup from '@/components/ui/form/FormGroup'
|
||||||
|
import PasswordInput from '@/components/ui/form/PasswordInput'
|
||||||
|
import { useAppDispatch } from '@/hooks/useAppDispatch'
|
||||||
|
import { addAlert } from '@/state/feedbackSlice'
|
||||||
|
import { nanoid } from '@reduxjs/toolkit'
|
||||||
|
import { AppwriteException } from 'appwrite'
|
||||||
|
import { updatePasswordRecovery } from 'entgamers-database/frontend/session'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { type FC } from 'react'
|
||||||
|
import { object, ref, string } from 'yup'
|
||||||
|
|
||||||
|
export interface UpdateRecoverPasswordFormProps {
|
||||||
|
userId: string
|
||||||
|
secret: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateRecoverPasswordData extends UpdateRecoverPasswordFormProps {
|
||||||
|
password: string
|
||||||
|
confirmPassword: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateRecoverPasswordSchema = object({
|
||||||
|
password: string()
|
||||||
|
.min(6, 'La contraseña debe tener al menos 6 caracteres')
|
||||||
|
.matches(/[a-z]/, 'La contraseña debe tener al menos una letra minúscula')
|
||||||
|
.matches(/[A-Z]/, 'La contraseña debe tener al menos una letra mayúscula')
|
||||||
|
.matches(/[0-9]/, 'La contraseña debe tener al menos un número')
|
||||||
|
.required('La contraseña es requerida'),
|
||||||
|
confirmPassword: string().oneOf([ref('password')], 'Las contraseñas no coinciden').required('La confirmación de la contraseña es requerida')
|
||||||
|
})
|
||||||
|
|
||||||
|
const UpdateRecoverPasswordForm: FC<UpdateRecoverPasswordFormProps> = (props) => {
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const formik = useFormik<UpdateRecoverPasswordData>({
|
||||||
|
initialValues: {
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
userId: props.userId,
|
||||||
|
secret: props.secret
|
||||||
|
},
|
||||||
|
onSubmit: async ({ confirmPassword, password, secret, userId }) => {
|
||||||
|
try {
|
||||||
|
await updatePasswordRecovery(userId, secret, password, confirmPassword)
|
||||||
|
dispatch(addAlert({
|
||||||
|
id: nanoid(),
|
||||||
|
title: 'Contraseña actualizada',
|
||||||
|
message: 'Ahora puedes iniciar sesión',
|
||||||
|
severity: 'success'
|
||||||
|
}))
|
||||||
|
router.push('/login')
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AppwriteException) {
|
||||||
|
dispatch(addAlert({
|
||||||
|
id: nanoid(),
|
||||||
|
message: error.message,
|
||||||
|
title: 'Error mientras se registraba',
|
||||||
|
severity: 'error'
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
dispatch(addAlert({
|
||||||
|
id: nanoid(),
|
||||||
|
message: 'Error desconocido',
|
||||||
|
title: 'Error mientras se solicitaba la recuperación de contraseña',
|
||||||
|
severity: 'error'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
validationSchema: updateRecoverPasswordSchema,
|
||||||
|
isInitialValid: false
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
>
|
||||||
|
<FormGroup>
|
||||||
|
<label htmlFor="password">
|
||||||
|
Contraseña
|
||||||
|
</label>
|
||||||
|
<PasswordInput
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
value={formik.values.password}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
status={formik.touched.password !== undefined && formik.errors.password !== undefined ? 'danger' : undefined}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
{formik.touched.password !== undefined && formik.errors.password !== undefined
|
||||||
|
? (
|
||||||
|
<Typography variant="caption" color="danger">
|
||||||
|
{formik.errors.password}
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<Typography variant="caption" color="info">
|
||||||
|
Escribe tu nueva contraseña
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup>
|
||||||
|
<label htmlFor="confirmPassword">
|
||||||
|
Confirmar contraseña
|
||||||
|
</label>
|
||||||
|
<PasswordInput
|
||||||
|
id="confirmPassword"
|
||||||
|
name="confirmPassword"
|
||||||
|
value={formik.values.confirmPassword}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
status={formik.touched.confirmPassword !== undefined && formik.errors.confirmPassword !== undefined ? 'danger' : undefined}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
{formik.touched.confirmPassword !== undefined && formik.errors.confirmPassword !== undefined && (
|
||||||
|
<Typography variant="caption" color="danger">
|
||||||
|
{formik.errors.confirmPassword}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={formik.isSubmitting}
|
||||||
|
>
|
||||||
|
Actualizar contraseña
|
||||||
|
</Button>
|
||||||
|
</FormGroup>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpdateRecoverPasswordForm
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import ManageRecoverPassword from '@/app/recover-password/ManageRecoverPassword'
|
||||||
|
import Typography from '@/components/ui/Typography'
|
||||||
|
import { css, cx } from '@/styled-system/css'
|
||||||
|
import { Center } from '@/styled-system/jsx'
|
||||||
|
import { container } from '@/styled-system/patterns'
|
||||||
|
import { card } from '@/styled-system/recipes'
|
||||||
|
import { type FC } from 'react'
|
||||||
|
|
||||||
|
const page: FC = () => {
|
||||||
|
return (
|
||||||
|
<Center
|
||||||
|
className={cx(
|
||||||
|
container(),
|
||||||
|
css({
|
||||||
|
minHeight: 'calc(100vh - 60px - 72px)'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
card().body,
|
||||||
|
css({
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: { sm: 'breakpoint-sm' },
|
||||||
|
overflow: 'visible'
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
card().header
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Typography variant="h1" align="center">Recuperar contraseña</Typography>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={card().content}
|
||||||
|
>
|
||||||
|
<ManageRecoverPassword />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Center>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default page
|
||||||
Reference in New Issue
Block a user