Skip to content

Commit

Permalink
feat: reset password flow done
Browse files Browse the repository at this point in the history
  • Loading branch information
tomaspalma committed Jan 26, 2025
1 parent 2e34389 commit 4155333
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 23 deletions.
35 changes: 27 additions & 8 deletions website/app/controllers/authentication_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
registerWithCredentialsValidator,
emailVerificationCallbackValidator,
loginWithCredentialsValidator,
passwordResetValidator,
} from '#validators/authentication'
import { UserService } from '#services/user_service'
import { inject } from '@adonisjs/core'
Expand Down Expand Up @@ -71,20 +72,38 @@ export default class AuthenticationController {

async sendForgotPassword({ request, response }: HttpContext) {
const { email } = request.only(['email'])
await this.userService.sendForgotPasswordEmail(email)

return response.redirect().toRoute('pages:auth.forgot-password')
/*
According to OWASP recommendations, the existence of the account should be transparent
to the person who issues this request, but we should not send an email that is not in
any account.
*/
if(await Account.findBy('id', `credentials:${email}`)) {
await this.userService.sendForgotPasswordEmail(email)
}

return response.redirect().toRoute('page:auth.forgot-password.sent')
}

async callbackForForgotPassword({ request, inertia }: HttpContext) {
const { email } = await request.validateUsing(emailVerificationCallbackValidator)

return inertia.render('auth/forgot-password/reset', { email })
}

async callbackForForgotPassword({ request, response, inertia }: HttpContext) {
// const { email } = await request.validateUsing(emailVerificationCallbackValidator)
async changePassword({ request, response }: HttpContext) {
const {
password,
email
} = await request.validateUsing(passwordResetValidator)

if(request.method() === 'GET') {
return inertia.render('auth/forgot-password-reset')
} else if(request.method() === 'POST') {
const account = await Account.find(`credentials:${email}`)
if (account) {
account.password = password // Auther mixin hashes it automatically on assignment
await account.save()
}

return response.redirect().back()//toRoute('actions:auth.forgot-password.success')
return response.redirect().toRoute('actions:auth.forgot-password.success')
}

// SOCIAL AUTHENTICATION
Expand Down
2 changes: 1 addition & 1 deletion website/app/listeners/send_forgot_password_email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default class SendForgotPasswordEmail {

verificationLink: buildUrl()
.qs({ email })
.makeSigned("actions:auth.forgot-password.callback", { expiresIn: "1h" }),
.makeSigned("actions:auth.forgot-password.callback", { expiresIn: "10m" }),
});

await mail.send(notification);
Expand Down
7 changes: 7 additions & 0 deletions website/app/validators/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ export const registerWithCredentialsValidator = vine.compile(
})
)

export const passwordResetValidator = vine.compile(
vine.object({
email: vine.string().email().exists({ table: User.table, column: 'email' }),
password: vine.string().minLength(8).confirmed(),
})
)

export const emailVerificationCallbackValidator = vine.compile(
vine.object({
email: vine.string().email().exists({ table: User.table, column: 'email' }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,25 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '~/com
import { Input } from '~/components/ui/input'
import { Label } from '~/components/ui/label'
import { useError } from '~/hooks/use_error'
import { useForm } from '@inertiajs/react'
import { useForm, usePage } from '@inertiajs/react'
import { cn } from '~/lib/utils'
import BaseLayout from '~/layouts/base'
import CardLayout from '~/layouts/card'

export default function ForgotPasswor() {
const page = usePage()
const oauthError = useError('oauth')

const { data, setData, errors, post } = useForm({
email: '',
const { data, setData, post } = useForm({
email: String(page.props.email),
password: '',
password_confirmation: '',
})

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()

post('/auth/password/forgot/new')
post('/auth/password/forgot/reset')
}

return (
Expand All @@ -29,27 +31,37 @@ export default function ForgotPasswor() {
<CardHeader>
<CardTitle className="text-2xl">Repôr palavra-passe</CardTitle>
<CardDescription>
Introduz o teu e-mail para recuperares a tua palavra-passe
Introduz a tua nova palavra-passe
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} method="POST" action="/auth/login">
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">E-mail</Label>
<Label htmlFor="password">Palavra-passe</Label>
<Input
id="email"
type="email"
placeholder="[email protected]"
value={data.email}
onChange={(e) => setData('email', e.target.value)}
id="password"
type="password"
placeholder="••••••••••••"
value={data.password}
onChange={(e) => setData('password', e.target.value)}
required
/>
</div>
<div className="grid gap-2">
<Label htmlFor="password">Confirmar palavra-passe</Label>
<Input
id="password_confirmation"
type="password"
placeholder="••••••••••••"
value={data.password_confirmation}
onChange={(e) => setData('password_confirmation', e.target.value)}
required
/>
{errors.email && <p className="text-sm text-red-600">{errors.email}</p>}
</div>
<div className="flex flex-col gap-4 ">
<Button type="submit" className="w-full bg-enei-blue">
Recuperar palavra-passe
Definir palavra-passe
</Button>
</div>
</div>
Expand All @@ -61,3 +73,4 @@ export default function ForgotPasswor() {
</BaseLayout>
)
}

62 changes: 62 additions & 0 deletions website/inertia/pages/auth/forgot-password/sent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useForm } from '@inertiajs/react'
import { Button } from '~/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '~/components/ui/card'
import { useCooldown } from '~/hooks/use_cooldown'
import { useToast } from '~/hooks/use_toast'
import { useTuyau } from '~/hooks/use_tuyau'
import BaseLayout from '~/layouts/base'
import CardLayout from '~/layouts/card'

export default function EmailVerification() {
const tuyau = useTuyau()

const cooldown = useCooldown({
seconds: 60,
})

const { post } = useForm()
const { toast } = useToast()
function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
post(tuyau.$url('actions:auth.forgot-password.send'), {
onSuccess: () => {
toast({
title: 'E-mail reenviado!',
description: 'Por favor, verifica a tua caixa de entrada, incluindo o spam!',
duration: 5000,
})
},
})
}

return (
<BaseLayout title="Repôr palavra-passe">
<CardLayout>
<Card>
<CardHeader>
<CardTitle className="text-2xl">E-mail de recuperação de palavra-passe enviado</CardTitle>
<CardDescription>
Um e-mail de confirmação foi enviado para o e-mail indicado.
Por favor, verifica a tua caixa de entrada
, <span className="font-bold">incluindo o spam</span>!
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={cooldown.throttle(onSubmit)} method="post">
<Button type="submit" className="w-full" disabled={cooldown.active}>
Não recebi nada...
</Button>
{cooldown.active && (
<p className="text-muted-foreground text-xs mt-2">
Por favor espera {cooldown.secondsLeft} segundos antes de tentar novamente.
</p>
)}
</form>
</CardContent>
</Card>
</CardLayout>
</BaseLayout>
)
}


25 changes: 25 additions & 0 deletions website/inertia/pages/auth/forgot-password/success.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Link } from '@tuyau/inertia/react'
import { buttonVariants } from '~/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '~/components/ui/card'
import BaseLayout from '~/layouts/base'
import CardLayout from '~/layouts/card'

export default function EmailVerification() {
return (
<BaseLayout title="E-mail confirmado">
<CardLayout>
<Card>
<CardHeader>
<CardTitle>A tua palavra-passe foi alterada com sucesso!</CardTitle>
</CardHeader>
<CardContent>
<Link route="pages:auth.login" className={buttonVariants()}>
Ir para a página de login
</Link>
</CardContent>
</Card>
</CardLayout>
</BaseLayout>
)
}

18 changes: 17 additions & 1 deletion website/start/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,16 @@ router

router
.on('/password/forgot')
.renderInertia('auth/forgot-password')
.renderInertia('auth/forgot-password/index')
.as('pages:auth.forgot-password')
.use(middleware.guest())

router
.on('/password/forgot/sent')
.renderInertia('auth/forgot-password/sent')
.as('page:auth.forgot-password.sent')
.use(middleware.guest())

router
.post('/password/forgot/new', [AuthenticationController, 'sendForgotPassword'])
.as('actions:auth.forgot-password.send')
Expand All @@ -62,6 +68,16 @@ router
.as('actions:auth.forgot-password.callback')
.middleware([middleware.verifyUrlSignature(), middleware.automaticSubmit()])

router
.post('/password/forgot/reset', [AuthenticationController, 'changePassword'])
.as('actions:auth.forgot-password.change')
.use(middleware.guest())

router
.on('/password/forgot/success')
.renderInertia('auth/forgot-password/success')
.as('actions:auth.forgot-password.success')

router
.on('/verify')
.renderInertia('auth/verify/index')
Expand Down

0 comments on commit 4155333

Please sign in to comment.