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({
|
export const resetPasswordEmailDto = z.object({
|
||||||
email: z.string().trim().email().max(64, { message: 'Email must be less than 64 characters' }),
|
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';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const resetPasswordTokenDto = z.object({
|
export const resetPasswordCodeDto = z.object({
|
||||||
token: z.string().trim().min(6).max(6),
|
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 { zValidator } from '@hono/zod-validator';
|
||||||
|
import { inject, injectable } from '@needle-di/core';
|
||||||
import { openApi } from 'hono-zod-openapi';
|
import { openApi } from 'hono-zod-openapi';
|
||||||
import { createLoginRequestDto } from '../../../dtos/login/create-login-request.dto';
|
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 { 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 { rateLimit } from '../common/middleware/rate-limit.middleware';
|
||||||
import { LoggerService } from '../common/services/logger.service';
|
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()
|
@injectable()
|
||||||
export class IamController extends Controller {
|
export class IamController extends Controller {
|
||||||
constructor(
|
constructor(
|
||||||
private loggerService = inject(LoggerService),
|
private loggerService = inject(LoggerService),
|
||||||
private loginRequestsService = inject(LoginRequestsService),
|
private loginRequestsService = inject(LoginRequestsService),
|
||||||
|
private resetPasswordRequestsService = inject(ResetPasswordRequestsService),
|
||||||
private sessionsService = inject(SessionsService),
|
private sessionsService = inject(SessionsService),
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -43,6 +47,18 @@ export class IamController extends Controller {
|
||||||
await this.sessionsService.invalidateSession('');
|
await this.sessionsService.invalidateSession('');
|
||||||
this.sessionsService.deleteSessionCookie();
|
this.sessionsService.deleteSessionCookie();
|
||||||
return c.json({ message: 'logout' });
|
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 {
|
export class LoginVerificationEmail implements EmailTemplate {
|
||||||
constructor(private readonly token: string) { }
|
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 = {
|
export const notSignedInMessage = {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: 'You are not signed in',
|
message: 'You are not signed in',
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,13 @@ export const actions: Actions = {
|
||||||
|
|
||||||
const loginForm = await superValidate(event, zod(signinDto));
|
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);
|
const { error } = await locals.api.iam.login.$post({ json: loginForm.data }).then(locals.parseApiResponse);
|
||||||
console.log('Login error', error);
|
console.log('Login error', error);
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
@ -46,13 +53,6 @@ export const actions: Actions = {
|
||||||
return setError(loginForm, 'identifier', 'An error occurred while logging in.');
|
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.identifier = '';
|
||||||
loginForm.data.password = '';
|
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 { StatusCodes } from '$lib/utils/status-codes';
|
||||||
import { resetPasswordEmailDto, resetPasswordTokenDto } from '$lib/dtos/reset-password';
|
|
||||||
import { fail } from '@sveltejs/kit';
|
import { fail } from '@sveltejs/kit';
|
||||||
import { redirect } from 'sveltekit-flash-message/server';
|
import { redirect } from 'sveltekit-flash-message/server';
|
||||||
import { zod } from 'sveltekit-superforms/adapters';
|
import { zod } from 'sveltekit-superforms/adapters';
|
||||||
import { setError, superValidate } from 'sveltekit-superforms/server';
|
import { setError, superValidate } from 'sveltekit-superforms/server';
|
||||||
import type { PageServerLoad } from './$types';
|
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 {
|
return {
|
||||||
emailForm: await superValidate(zod(resetPasswordEmailDto)),
|
emailForm: await superValidate(zod(resetPasswordEmailDto)),
|
||||||
tokenForm: await superValidate(zod(resetPasswordTokenDto)),
|
tokenForm: await superValidate(zod(resetPasswordCodeDto)),
|
||||||
|
newPasswordForm: await superValidate(zod(resetPasswordNewPasswordDto)),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
passwordReset: async (event) => {
|
passwordResetRequest: async (event) => {
|
||||||
const { request, locals } = event;
|
const { request, locals } = event;
|
||||||
|
|
||||||
const authedUser = await locals.getAuthedUser();
|
const authedUser = await locals.getAuthedUser();
|
||||||
if (!authedUser) {
|
if (authedUser) {
|
||||||
throw redirect(302, '/login', notSignedInMessage, event);
|
throw redirect(302, '/', alreadySignedInMessage, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
const emailForm = await superValidate(request, zod(resetPasswordEmailDto));
|
const emailForm = await superValidate(request, zod(resetPasswordEmailDto));
|
||||||
if (!emailForm.valid) {
|
if (!emailForm.valid) {
|
||||||
return fail(StatusCodes.BAD_REQUEST, { emailForm });
|
return fail(StatusCodes.BAD_REQUEST, { emailForm });
|
||||||
}
|
}
|
||||||
// const error = {};
|
|
||||||
// // const { error } = await locals.api.iam.login.request.$post({ json: emailRegisterForm.data }).then(locals.parseApiResponse);
|
const { error } = await locals.api.iam.password.reset.request.$post({ json: emailForm.data }).then(locals.parseApiResponse);
|
||||||
// if (error) {
|
if (error) {
|
||||||
// return setError(emailForm, 'email', error);
|
return setError(emailForm, 'email', error);
|
||||||
// }
|
}
|
||||||
return { emailForm };
|
return { emailForm };
|
||||||
},
|
},
|
||||||
verifyToken: async (event) => {
|
verifyCode: async (event) => {
|
||||||
const { request, locals } = event;
|
const { request, locals } = event;
|
||||||
|
|
||||||
const authedUser = await locals.getAuthedUser();
|
const authedUser = await locals.getAuthedUser();
|
||||||
if (!authedUser) {
|
if (authedUser) {
|
||||||
throw redirect(302, '/login', notSignedInMessage, event);
|
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) {
|
if (!tokenForm.valid) {
|
||||||
return fail(StatusCodes.BAD_REQUEST, { tokenForm });
|
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) {
|
if (error) {
|
||||||
return setError(tokenForm, 'token', 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">
|
<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 Form from '$lib/components/ui/form/index.js';
|
||||||
import * as InputOTP from '$lib/components/ui/input-otp/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 { Input } from '$lib/components/ui/input/index.js';
|
||||||
import { receive, send } from '$lib/utils/pageCrossfade';
|
import { resetPasswordCodeDto, resetPasswordEmailDto } from '$lib/dtos/reset-password';
|
||||||
import { resetPasswordEmailDto, resetPasswordTokenDto } from '$lib/dtos/reset-password';
|
import { resetPasswordNewPasswordDto } from '$lib/dtos/reset-password/reset-password-new-password.dto';
|
||||||
import { superForm } from 'sveltekit-superforms';
|
import { superForm } from 'sveltekit-superforms';
|
||||||
import { zodClient } from 'sveltekit-superforms/adapters';
|
import { zodClient } from 'sveltekit-superforms/adapters';
|
||||||
|
|
||||||
const { data } = $props();
|
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),
|
validators: zodClient(resetPasswordEmailDto),
|
||||||
resetForm: false,
|
resetForm: false,
|
||||||
onUpdated: ({ form }) => {
|
onUpdated: ({ form }) => {
|
||||||
if (form.valid) {
|
if (form.valid) {
|
||||||
showTokenVerification = true;
|
resetEmailStep = 'token-verification';
|
||||||
$emailFormData.email = form.data.email;
|
$tokenFormData.email = form.data.email;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const tokenVerificationForm = superForm(data.tokenForm, {
|
const sf_token_verification = superForm(data.tokenForm, {
|
||||||
validators: zodClient(resetPasswordTokenDto),
|
validators: zodClient(resetPasswordCodeDto),
|
||||||
resetForm: false,
|
resetForm: false,
|
||||||
|
onUpdated: ({ form }) => {
|
||||||
|
if (form.valid) {
|
||||||
|
resetEmailStep = 'new-password';
|
||||||
|
$newPasswordFormData.email = form.data.email;
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { form: emailFormData, enhance: emailResetEnhance } = emailResetForm;
|
const sf_new_password = superForm(data.newPasswordForm, {
|
||||||
const { form: tokenFormData, enhance: tokenEnhance } = tokenVerificationForm;
|
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>
|
</script>
|
||||||
|
|
||||||
<Card.Root class="mx-auto max-w-sm">
|
<Card.Root class="mx-auto max-w-sm">
|
||||||
|
|
@ -40,18 +52,20 @@
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<div class="grid gap-4">
|
<div class="grid gap-4">
|
||||||
{#if showTokenVerification}
|
{#if resetEmailStep === 'email-reset'}
|
||||||
{@render tokenForm()}
|
|
||||||
{:else}
|
|
||||||
{@render emailForm()}
|
{@render emailForm()}
|
||||||
|
{:else if resetEmailStep === 'token-verification'}
|
||||||
|
{@render codeForm()}
|
||||||
|
{:else}
|
||||||
|
{@render resetPasswordForm()}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|
||||||
{#snippet emailForm()}
|
{#snippet emailForm()}
|
||||||
<form method="POST" action="?/passwordReset" use:emailResetEnhance class="grid gap-4">
|
<form method="POST" action="?/passwordResetRequest" use:emailResetEnhance class="grid gap-4">
|
||||||
<Form.Field form={emailResetForm} name="email">
|
<Form.Field form={sf_email_reset} name="email">
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
{#snippet children({ props })}
|
{#snippet children({ props })}
|
||||||
<Form.Label for="email">Email</Form.Label>
|
<Form.Label for="email">Email</Form.Label>
|
||||||
|
|
@ -70,14 +84,14 @@
|
||||||
</form>
|
</form>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#snippet tokenForm()}
|
{#snippet codeForm()}
|
||||||
<form method="POST" action="?/verifyToken" use:tokenEnhance class="space-y-4">
|
<form method="POST" action="?/verifyCode" use:tokenEnhance class="space-y-4">
|
||||||
<input hidden value={$tokenFormData.token} name="email" />
|
<input hidden value={$tokenFormData.email} name="email" />
|
||||||
<Form.Field form={tokenVerificationForm} name="token">
|
<Form.Field form={sf_token_verification} name="code">
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
{#snippet children({ props })}
|
{#snippet children({ props })}
|
||||||
<Form.Label for="token">Enter the token that was sent to your email</Form.Label>
|
<Form.Label for="code">Enter the token that was sent to your email</Form.Label>
|
||||||
<InputOTP.Root maxlength={6} {...props} bind:value={$tokenFormData.token}>
|
<InputOTP.Root maxlength={6} {...props} bind:value={$tokenFormData.code}>
|
||||||
{#snippet children({ cells })}
|
{#snippet children({ cells })}
|
||||||
<InputOTP.Group>
|
<InputOTP.Group>
|
||||||
{#each cells as cell}
|
{#each cells as cell}
|
||||||
|
|
@ -93,4 +107,41 @@
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
<Button class="w-full" type="submit">Submit</Button>
|
<Button class="w-full" type="submit">Submit</Button>
|
||||||
</form>
|
</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