feat: recover password flow

This commit is contained in:
2024-01-24 11:28:01 -06:00
parent 5f9b972983
commit 21bd696a30
5 changed files with 317 additions and 0 deletions
+4
View File
@@ -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
+46
View File
@@ -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