mirror of
https://github.com/BradNut/AdelieStack
synced 2025-09-08 17:40:20 +00:00
Completed password reset flow.
This commit is contained in:
parent
bfe7b8a28a
commit
f4aa035779
12 changed files with 357 additions and 61 deletions
|
|
@ -3,3 +3,5 @@ import { z } from 'zod';
|
|||
export const resetPasswordEmailDto = z.object({
|
||||
email: z.string().trim().email().max(64, { message: 'Email must be less than 64 characters' }),
|
||||
});
|
||||
|
||||
export type ResetPasswordRequestDto = z.infer<typeof resetPasswordEmailDto>;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
import { refinePasswords } from '$lib/validations/account';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const resetPasswordNewPasswordDto = z.object({
|
||||
email: z.string().trim().email(),
|
||||
password: z.string({ required_error: 'Password is required' }).trim(),
|
||||
confirm_password: z.string({ required_error: 'Confirm Password is required' }).trim(),
|
||||
})
|
||||
.superRefine(({ confirm_password, password }, ctx) => {
|
||||
return refinePasswords(confirm_password, password, ctx);
|
||||
});
|
||||
|
||||
export type ResetPasswordNewPasswordDto = z.infer<typeof resetPasswordNewPasswordDto>;
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const resetPasswordTokenDto = z.object({
|
||||
token: z.string().trim().min(6).max(6),
|
||||
export const resetPasswordCodeDto = z.object({
|
||||
email: z.string().trim().email(),
|
||||
code: z.string().trim().min(6).max(6),
|
||||
});
|
||||
|
||||
export type ResetPasswordCodeDto = z.infer<typeof resetPasswordCodeDto>;
|
||||
|
|
|
|||
|
|
@ -1,23 +1,27 @@
|
|||
import { inject, injectable } from '@needle-di/core';
|
||||
import { resetPasswordCodeDto, resetPasswordEmailDto } from '$lib/dtos/reset-password';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { inject, injectable } from '@needle-di/core';
|
||||
import { openApi } from 'hono-zod-openapi';
|
||||
import { createLoginRequestDto } from '../../../dtos/login/create-login-request.dto';
|
||||
import { LoginRequestsService } from '../iam/login-requests/login-requests.service';
|
||||
import { verifyLoginRequestDto } from '../../../dtos/login/verify-login-request.dto';
|
||||
import { SessionsService } from '../iam/sessions/sessions.service';
|
||||
import { authState } from '../common/middleware/auth.middleware';
|
||||
import { Controller } from '../common/factories/controllers.factory';
|
||||
import { loginRequestDto } from '../../../dtos/login/login-request.dto';
|
||||
import { signInEmail } from './login-requests/routes/login.routes';
|
||||
import { signinDto } from '../../../dtos/login/signin.dto';
|
||||
import { verifyLoginRequestDto } from '../../../dtos/login/verify-login-request.dto';
|
||||
import { Controller } from '../common/factories/controllers.factory';
|
||||
import { authState } from '../common/middleware/auth.middleware';
|
||||
import { rateLimit } from '../common/middleware/rate-limit.middleware';
|
||||
import { LoggerService } from '../common/services/logger.service';
|
||||
import { signinDto } from '../../../dtos/login/signin.dto';
|
||||
import { LoginRequestsService } from '../iam/login-requests/login-requests.service';
|
||||
import { SessionsService } from '../iam/sessions/sessions.service';
|
||||
import { signInEmail } from './login-requests/routes/login.routes';
|
||||
import { ResetPasswordRequestsService } from './reset-password-requests/reset-password-requests.service';
|
||||
import { resetPasswordNewPasswordDto } from '$lib/dtos/reset-password/reset-password-new-password.dto';
|
||||
|
||||
@injectable()
|
||||
export class IamController extends Controller {
|
||||
constructor(
|
||||
private loggerService = inject(LoggerService),
|
||||
private loginRequestsService = inject(LoginRequestsService),
|
||||
private resetPasswordRequestsService = inject(ResetPasswordRequestsService),
|
||||
private sessionsService = inject(SessionsService),
|
||||
) {
|
||||
super();
|
||||
|
|
@ -43,6 +47,18 @@ export class IamController extends Controller {
|
|||
await this.sessionsService.invalidateSession('');
|
||||
this.sessionsService.deleteSessionCookie();
|
||||
return c.json({ message: 'logout' });
|
||||
})
|
||||
.post("/password/reset", authState('none'), zValidator('json', resetPasswordNewPasswordDto), async (c) => {
|
||||
await this.resetPasswordRequestsService.resetPassword(c.req.valid('json'));
|
||||
return c.json({ message: 'welcome' });
|
||||
})
|
||||
.post('/password/reset/request', authState('none'), zValidator('json', resetPasswordEmailDto), async (c) => {
|
||||
await this.resetPasswordRequestsService.sendResetPasswordCode(c.req.valid('json'));
|
||||
return c.json({ message: 'success' });
|
||||
})
|
||||
.post('/password/reset/verify', authState('none'), zValidator('json', resetPasswordCodeDto), async (c) => {
|
||||
await this.resetPasswordRequestsService.verify(c.req.valid('json'));
|
||||
return c.json({ message: 'success' });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
import { injectable } from '@needle-di/core';
|
||||
import { RedisRepository } from '../../common/factories/redis-repository.factory';
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Model */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
type ResetPasswordRequest = { email: string; hashedCode: string };
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Repository */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
@injectable()
|
||||
export class ResetPasswordRequestsRepository extends RedisRepository<'password-reset-request'> {
|
||||
async set(args: ResetPasswordRequest) {
|
||||
return this.redis.setWithExpiry({
|
||||
prefix: this.prefix,
|
||||
key: args.email.toLowerCase(),
|
||||
value: args.hashedCode,
|
||||
expiry: 60 * 15
|
||||
});
|
||||
}
|
||||
|
||||
delete(email: string) {
|
||||
return this.redis.delete({ prefix: this.prefix, key: email.toLowerCase() });
|
||||
}
|
||||
|
||||
async get(email: string): Promise<ResetPasswordRequest | null> {
|
||||
const hashedCode = await this.redis.get({
|
||||
prefix: this.prefix,
|
||||
key: email.toLowerCase()
|
||||
});
|
||||
if (!hashedCode) return null;
|
||||
return { email, hashedCode: hashedCode };
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
import type { ResetPasswordCodeDto } from '$lib/dtos/reset-password';
|
||||
import type { ResetPasswordNewPasswordDto } from '$lib/dtos/reset-password/reset-password-new-password.dto';
|
||||
import { inject, injectable } from '@needle-di/core';
|
||||
import type { CreateLoginRequestDto } from '../../../../dtos/login/create-login-request.dto';
|
||||
import type { } from '../../../../dtos/login/verify-login-request.dto';
|
||||
import { LoggerService } from '../../common/services/logger.service';
|
||||
import { VerificationCodesService } from '../../common/services/verification-codes.service';
|
||||
import { BadRequest } from '../../common/utils/exceptions';
|
||||
import { MailerService } from '../../mail/mailer.service';
|
||||
import { ResetPasswordEmail } from '../../mail/templates/reset-password.template';
|
||||
import { UsersRepository } from '../../users/users.repository';
|
||||
import { UsersService } from '../../users/users.service';
|
||||
import { SessionsService } from '../sessions/sessions.service';
|
||||
import { ResetPasswordRequestsRepository } from './reset-password-requests.repository';
|
||||
|
||||
@injectable()
|
||||
export class ResetPasswordRequestsService {
|
||||
constructor(
|
||||
private loggerService = inject(LoggerService),
|
||||
private resetPasswordRequestsRepository = inject(ResetPasswordRequestsRepository),
|
||||
private usersRepository = inject(UsersRepository),
|
||||
private verificationCodesService = inject(VerificationCodesService),
|
||||
private usersService = inject(UsersService),
|
||||
private sessionsService = inject(SessionsService),
|
||||
private mailer = inject(MailerService),
|
||||
) {}
|
||||
|
||||
async verify({ email, code }: ResetPasswordCodeDto) {
|
||||
// find the hashed verification code for the email
|
||||
const resetPasswordRequest = await this.resetPasswordRequestsRepository.get(email);
|
||||
|
||||
// if no hashed code is found, the request is invalid
|
||||
if (!resetPasswordRequest) throw BadRequest('Invalid code');
|
||||
|
||||
// verify the code
|
||||
const isValid = await this.verificationCodesService.verify({
|
||||
verificationCode: code,
|
||||
hashedVerificationCode: resetPasswordRequest.hashedCode,
|
||||
});
|
||||
|
||||
// if the code is invalid, throw an error
|
||||
if (!isValid) {
|
||||
throw BadRequest('Invalid code');
|
||||
}
|
||||
|
||||
// burn the login request so it can't be used again
|
||||
await this.resetPasswordRequestsRepository.delete(email);
|
||||
|
||||
// check if the user already exists
|
||||
const existingUser = await this.usersRepository.findOneByEmail(email);
|
||||
|
||||
if (!existingUser) {
|
||||
this.loggerService.log.debug('User not found for email', email);
|
||||
throw BadRequest('Unable to reset password');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
async sendResetPasswordCode({ email }: CreateLoginRequestDto) {
|
||||
// check if the user already exists
|
||||
const existingUser = await this.usersRepository.findOneByEmail(email);
|
||||
|
||||
if (!existingUser) {
|
||||
this.loggerService.log.debug(`User not found for email: ${email}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// remove any existing login requests
|
||||
await this.resetPasswordRequestsRepository.delete(email);
|
||||
|
||||
// generate a new verification code and hash
|
||||
const { verificationCode, hashedVerificationCode } = await this.verificationCodesService.generateCodeWithHash();
|
||||
|
||||
// create a new login request
|
||||
await this.resetPasswordRequestsRepository.set({
|
||||
email,
|
||||
hashedCode: hashedVerificationCode,
|
||||
});
|
||||
|
||||
// send the verification email
|
||||
await this.mailer.send({
|
||||
to: email,
|
||||
template: new ResetPasswordEmail(verificationCode),
|
||||
});
|
||||
}
|
||||
async resetPassword({ email, password, confirm_password }: ResetPasswordNewPasswordDto) {
|
||||
if (password !== confirm_password) {
|
||||
throw BadRequest('Passwords do not match');
|
||||
}
|
||||
|
||||
// check if the user already exists
|
||||
const existingUser = await this.usersRepository.findOneByEmail(email);
|
||||
|
||||
if (!existingUser) {
|
||||
throw BadRequest('Unable to reset password');
|
||||
}
|
||||
|
||||
await this.usersService.updatePassword(existingUser.id, password);
|
||||
return true;
|
||||
}
|
||||
private async authExistingUser({ userId }: { userId: string }) {
|
||||
return this.sessionsService.createSession(userId);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { type EmailTemplate } from "../interfaces/email-template.interface"
|
||||
import type { EmailTemplate } from "../interfaces/email-template.interface"
|
||||
|
||||
export class LoginVerificationEmail implements EmailTemplate {
|
||||
constructor(private readonly token: string) { }
|
||||
|
|
|
|||
39
src/lib/server/api/mail/templates/reset-password.template.ts
Normal file
39
src/lib/server/api/mail/templates/reset-password.template.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import type { EmailTemplate } from "../interfaces/email-template.interface"
|
||||
|
||||
export class ResetPasswordEmail implements EmailTemplate {
|
||||
constructor(private readonly code: string) { }
|
||||
|
||||
subject(): string {
|
||||
return 'Reset Password'
|
||||
}
|
||||
|
||||
html() {
|
||||
return /*html*/ `
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<meta http-equiv='X-UA-Compatible' content='IE=edge' />
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
||||
<title>Message</title>
|
||||
</head>
|
||||
<body>
|
||||
<p class='title'>Reset your password</p>
|
||||
<p>
|
||||
Thanks for using example.com. You requested a password reset. Please enter the following
|
||||
verification code when prompted. If you don't have an example.com an account, you can ignore
|
||||
this message.</p>
|
||||
<div class='center'>
|
||||
<p class='token-title'>Verification Code</p>
|
||||
<p class='token-text'>${this.code}</p>
|
||||
<p class='token-subtext'>(This code is valid for 15 minutes)</p>
|
||||
</div>
|
||||
</body>
|
||||
<style>
|
||||
.title { font-size: 24px; font-weight: 700; } .token-text { font-size: 24px; font-weight: 700;
|
||||
margin-top: 8px; } .token-title { font-size: 18px; font-weight: 700; margin-bottom: 0px; }
|
||||
.center { display: flex; justify-content: center; align-items: center; flex-direction: column;}
|
||||
.token-subtext { font-size: 12px; margin-top: 0px; }
|
||||
</style>
|
||||
</html>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
export const alreadySignedInMessage = {
|
||||
type: 'success',
|
||||
message: 'You are already signed in',
|
||||
} as const;
|
||||
export const notSignedInMessage = {
|
||||
type: 'error',
|
||||
message: 'You are not signed in',
|
||||
|
|
|
|||
|
|
@ -38,6 +38,13 @@ export const actions: Actions = {
|
|||
|
||||
const loginForm = await superValidate(event, zod(signinDto));
|
||||
|
||||
if (!loginForm.valid) {
|
||||
loginForm.data.password = '';
|
||||
return fail(StatusCodes.BAD_REQUEST, {
|
||||
loginForm,
|
||||
});
|
||||
}
|
||||
|
||||
const { error } = await locals.api.iam.login.$post({ json: loginForm.data }).then(locals.parseApiResponse);
|
||||
console.log('Login error', error);
|
||||
if (error) {
|
||||
|
|
@ -46,13 +53,6 @@ export const actions: Actions = {
|
|||
return setError(loginForm, 'identifier', 'An error occurred while logging in.');
|
||||
}
|
||||
|
||||
if (!loginForm.valid) {
|
||||
loginForm.data.password = '';
|
||||
return fail(400, {
|
||||
loginForm,
|
||||
});
|
||||
}
|
||||
|
||||
loginForm.data.identifier = '';
|
||||
loginForm.data.password = '';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,56 +1,85 @@
|
|||
import { notSignedInMessage } from '$lib/utils/flashMessages';
|
||||
import { resetPasswordCodeDto, resetPasswordEmailDto } from '$lib/dtos/reset-password';
|
||||
import { alreadySignedInMessage, notSignedInMessage } from '$lib/utils/flashMessages';
|
||||
import { StatusCodes } from '$lib/utils/status-codes';
|
||||
import { resetPasswordEmailDto, resetPasswordTokenDto } from '$lib/dtos/reset-password';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import { redirect } from 'sveltekit-flash-message/server';
|
||||
import { zod } from 'sveltekit-superforms/adapters';
|
||||
import { setError, superValidate } from 'sveltekit-superforms/server';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { resetPasswordNewPasswordDto } from '$lib/dtos/reset-password/reset-password-new-password.dto';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const { locals } = event;
|
||||
const authedUser = await locals.getAuthedUser();
|
||||
if (authedUser) {
|
||||
throw redirect(302, '/', alreadySignedInMessage, event);
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
return {
|
||||
emailForm: await superValidate(zod(resetPasswordEmailDto)),
|
||||
tokenForm: await superValidate(zod(resetPasswordTokenDto)),
|
||||
tokenForm: await superValidate(zod(resetPasswordCodeDto)),
|
||||
newPasswordForm: await superValidate(zod(resetPasswordNewPasswordDto)),
|
||||
};
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
passwordReset: async (event) => {
|
||||
passwordResetRequest: async (event) => {
|
||||
const { request, locals } = event;
|
||||
|
||||
const authedUser = await locals.getAuthedUser();
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event);
|
||||
if (authedUser) {
|
||||
throw redirect(302, '/', alreadySignedInMessage, event);
|
||||
}
|
||||
|
||||
const emailForm = await superValidate(request, zod(resetPasswordEmailDto));
|
||||
if (!emailForm.valid) {
|
||||
return fail(StatusCodes.BAD_REQUEST, { emailForm });
|
||||
}
|
||||
// const error = {};
|
||||
// // const { error } = await locals.api.iam.login.request.$post({ json: emailRegisterForm.data }).then(locals.parseApiResponse);
|
||||
// if (error) {
|
||||
// return setError(emailForm, 'email', error);
|
||||
// }
|
||||
|
||||
const { error } = await locals.api.iam.password.reset.request.$post({ json: emailForm.data }).then(locals.parseApiResponse);
|
||||
if (error) {
|
||||
return setError(emailForm, 'email', error);
|
||||
}
|
||||
return { emailForm };
|
||||
},
|
||||
verifyToken: async (event) => {
|
||||
verifyCode: async (event) => {
|
||||
const { request, locals } = event;
|
||||
|
||||
const authedUser = await locals.getAuthedUser();
|
||||
if (!authedUser) {
|
||||
throw redirect(302, '/login', notSignedInMessage, event);
|
||||
if (authedUser) {
|
||||
throw redirect(302, '/', alreadySignedInMessage, event);
|
||||
}
|
||||
|
||||
const tokenForm = await superValidate(request, zod(resetPasswordTokenDto));
|
||||
const tokenForm = await superValidate(request, zod(resetPasswordCodeDto));
|
||||
console.log('tokenForm', tokenForm);
|
||||
if (!tokenForm.valid) {
|
||||
return fail(StatusCodes.BAD_REQUEST, { tokenForm });
|
||||
}
|
||||
const error = {};
|
||||
// const { error } = await locals.api.iam.login.verify.$post({ json: emailSignInForm.data }).then(locals.parseApiResponse)
|
||||
|
||||
const { error } = await locals.api.iam.password.reset.verify.$post({ json: tokenForm.data }).then(locals.parseApiResponse)
|
||||
console.log('error', error);
|
||||
if (error) {
|
||||
return setError(tokenForm, 'token', error);
|
||||
}
|
||||
redirect(301, '/');
|
||||
return { tokenForm };
|
||||
},
|
||||
resetPassword: async (event) => {
|
||||
const { request, locals } = event;
|
||||
|
||||
const authedUser = await locals.getAuthedUser();
|
||||
if (authedUser) {
|
||||
throw redirect(302, '/', alreadySignedInMessage, event);
|
||||
}
|
||||
|
||||
const newPasswordForm = await superValidate(request, zod(resetPasswordNewPasswordDto));
|
||||
if (!newPasswordForm.valid) {
|
||||
return fail(StatusCodes.BAD_REQUEST, { newPasswordForm });
|
||||
}
|
||||
const { error } = await locals.api.iam.password.reset.$post({ json: newPasswordForm.data }).then(locals.parseApiResponse);
|
||||
if (error) {
|
||||
return setError(newPasswordForm, 'password', error);
|
||||
}
|
||||
const message = { type: 'success', message: 'Successfully reset password!' } as const;
|
||||
redirect(302, '/login', message, event);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,36 +1,48 @@
|
|||
<script lang="ts">
|
||||
import * as Card from "$lib/components/ui/card/index.js";
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
import * as Form from '$lib/components/ui/form/index.js';
|
||||
import * as InputOTP from '$lib/components/ui/input-otp/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import { receive, send } from '$lib/utils/pageCrossfade';
|
||||
import { resetPasswordEmailDto, resetPasswordTokenDto } from '$lib/dtos/reset-password';
|
||||
import { resetPasswordCodeDto, resetPasswordEmailDto } from '$lib/dtos/reset-password';
|
||||
import { resetPasswordNewPasswordDto } from '$lib/dtos/reset-password/reset-password-new-password.dto';
|
||||
import { superForm } from 'sveltekit-superforms';
|
||||
import { zodClient } from 'sveltekit-superforms/adapters';
|
||||
|
||||
const { data } = $props();
|
||||
|
||||
let showTokenVerification = $state(false);
|
||||
let resetEmailStep = $state('email-reset');
|
||||
|
||||
const emailResetForm = superForm(data.emailForm, {
|
||||
const sf_email_reset = superForm(data.emailForm, {
|
||||
validators: zodClient(resetPasswordEmailDto),
|
||||
resetForm: false,
|
||||
onUpdated: ({ form }) => {
|
||||
if (form.valid) {
|
||||
showTokenVerification = true;
|
||||
$emailFormData.email = form.data.email;
|
||||
resetEmailStep = 'token-verification';
|
||||
$tokenFormData.email = form.data.email;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const tokenVerificationForm = superForm(data.tokenForm, {
|
||||
validators: zodClient(resetPasswordTokenDto),
|
||||
const sf_token_verification = superForm(data.tokenForm, {
|
||||
validators: zodClient(resetPasswordCodeDto),
|
||||
resetForm: false,
|
||||
onUpdated: ({ form }) => {
|
||||
if (form.valid) {
|
||||
resetEmailStep = 'new-password';
|
||||
$newPasswordFormData.email = form.data.email;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { form: emailFormData, enhance: emailResetEnhance } = emailResetForm;
|
||||
const { form: tokenFormData, enhance: tokenEnhance } = tokenVerificationForm;
|
||||
const sf_new_password = superForm(data.newPasswordForm, {
|
||||
validators: zodClient(resetPasswordNewPasswordDto),
|
||||
resetForm: false,
|
||||
});
|
||||
|
||||
const { form: emailFormData, enhance: emailResetEnhance } = sf_email_reset;
|
||||
const { form: tokenFormData, enhance: tokenEnhance } = sf_token_verification;
|
||||
const { form: newPasswordFormData, enhance: newPasswordEnhance } = sf_new_password;
|
||||
</script>
|
||||
|
||||
<Card.Root class="mx-auto max-w-sm">
|
||||
|
|
@ -40,18 +52,20 @@
|
|||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div class="grid gap-4">
|
||||
{#if showTokenVerification}
|
||||
{@render tokenForm()}
|
||||
{:else}
|
||||
{#if resetEmailStep === 'email-reset'}
|
||||
{@render emailForm()}
|
||||
{:else if resetEmailStep === 'token-verification'}
|
||||
{@render codeForm()}
|
||||
{:else}
|
||||
{@render resetPasswordForm()}
|
||||
{/if}
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
{#snippet emailForm()}
|
||||
<form method="POST" action="?/passwordReset" use:emailResetEnhance class="grid gap-4">
|
||||
<Form.Field form={emailResetForm} name="email">
|
||||
<form method="POST" action="?/passwordResetRequest" use:emailResetEnhance class="grid gap-4">
|
||||
<Form.Field form={sf_email_reset} name="email">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label for="email">Email</Form.Label>
|
||||
|
|
@ -70,14 +84,14 @@
|
|||
</form>
|
||||
{/snippet}
|
||||
|
||||
{#snippet tokenForm()}
|
||||
<form method="POST" action="?/verifyToken" use:tokenEnhance class="space-y-4">
|
||||
<input hidden value={$tokenFormData.token} name="email" />
|
||||
<Form.Field form={tokenVerificationForm} name="token">
|
||||
{#snippet codeForm()}
|
||||
<form method="POST" action="?/verifyCode" use:tokenEnhance class="space-y-4">
|
||||
<input hidden value={$tokenFormData.email} name="email" />
|
||||
<Form.Field form={sf_token_verification} name="code">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label for="token">Enter the token that was sent to your email</Form.Label>
|
||||
<InputOTP.Root maxlength={6} {...props} bind:value={$tokenFormData.token}>
|
||||
<Form.Label for="code">Enter the token that was sent to your email</Form.Label>
|
||||
<InputOTP.Root maxlength={6} {...props} bind:value={$tokenFormData.code}>
|
||||
{#snippet children({ cells })}
|
||||
<InputOTP.Group>
|
||||
{#each cells as cell}
|
||||
|
|
@ -93,4 +107,41 @@
|
|||
</Form.Field>
|
||||
<Button class="w-full" type="submit">Submit</Button>
|
||||
</form>
|
||||
{/snippet}
|
||||
{/snippet}
|
||||
|
||||
{#snippet resetPasswordForm()}
|
||||
<form method="POST" action="?/resetPassword" use:newPasswordEnhance class="space-y-4">
|
||||
<input hidden value={$newPasswordFormData.email} name="email" />
|
||||
<Form.Field form={sf_new_password} name="password">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label for="password">Password</Form.Label>
|
||||
<Input
|
||||
{...props}
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
bind:value={$newPasswordFormData.password}
|
||||
/>
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field form={sf_new_password} name="confirm_password">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label for="confirm_password">Confirm Password</Form.Label>
|
||||
<Input
|
||||
{...props}
|
||||
type="password"
|
||||
id="confirm_password"
|
||||
name="confirm_password"
|
||||
bind:value={$newPasswordFormData.confirm_password}
|
||||
/>
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Button class="w-full" type="submit">Submit</Button>
|
||||
</form>
|
||||
{/snippet}
|
||||
|
|
|
|||
Loading…
Reference in a new issue