From 27d8c7cff6b642208f3dd638d9afca91d540964a Mon Sep 17 00:00:00 2001 From: Colin Kallemein Date: Sun, 3 Nov 2024 21:54:54 +0100 Subject: [PATCH] Finish password reset (hopefully) --- .../passwordResetTokenRepository.ts | 13 ++++++++ src/services/userService.ts | 32 +++++++++++++++++-- src/utilities/http.ts | 26 ++++++++++++++- src/utilities/zodTypes.ts | 9 ++++++ 4 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src/repositories/passwordResetTokenRepository.ts b/src/repositories/passwordResetTokenRepository.ts index ccf2b64..cd0e77c 100644 --- a/src/repositories/passwordResetTokenRepository.ts +++ b/src/repositories/passwordResetTokenRepository.ts @@ -26,6 +26,19 @@ class PasswordResetTokenRepository { throw new Error(`Failed to get password reset token by user ID: ${error.message}`) } } + + async getByToken(token: string): Promise { + try { + return await prisma.passwordResetToken.findFirst({ + where: { + token + } + }) + } catch (error: any) { + // Handle error + throw new Error(`Failed to get password reset token by token: ${error.message}`) + } + } } export default new PasswordResetTokenRepository() diff --git a/src/services/userService.ts b/src/services/userService.ts index 074af6b..e5af8dc 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -77,9 +77,15 @@ class UserService { const isTokenExpired = latestToken.createdAt < tokenExpiryDate if (!isTokenExpired) return false + + await prisma.passwordResetToken.delete({ + where: { + id: latestToken.id + } + }) } - prisma.passwordResetToken.create({ + await prisma.passwordResetToken.create({ data: { userId: user.id, token: token, @@ -101,8 +107,8 @@ class UserService { from: config.SMTP_USER, to: email, subject: "Reset your password", - text: "A password reset has been requested, reset your password here: " + config.CLIENT_URL + "/#" + token, // Plain text body - html: "

A password reset has been requested, reset your password here: " + config.CLIENT_URL + "/#token=" + token + "

", // Html body + text: "A password reset has been requested, reset your password here: " + config.CLIENT_URL + "#" + token, // Plain text body + html: "

A password reset has been requested, reset your password here: " + config.CLIENT_URL + "#" + token + "

", // Html body }); return true @@ -110,6 +116,26 @@ class UserService { return false } } + + /** + * Set new password + * @param urlToken + * @param password + */ + async newPassword(urlToken: string, password: string): Promise { + const tokenData = await PasswordResetTokenRepository.getByToken(urlToken) + if (!tokenData) { + return false + } + + const hashedPassword = await bcrypt.hash(password, 10) + return prisma.user.update({ + where: { id: tokenData.userId }, + data: { + password: hashedPassword + } + }) + } } export default UserService diff --git a/src/utilities/http.ts b/src/utilities/http.ts index 912f652..33b0dbf 100644 --- a/src/utilities/http.ts +++ b/src/utilities/http.ts @@ -2,7 +2,7 @@ import { Application, Request, Response } from 'express' import UserService from '../services/userService' import jwt from 'jsonwebtoken' import config from './config' -import { loginAccountSchema, registerAccountSchema, resetPasswordSchema } from './zodTypes' +import { loginAccountSchema, registerAccountSchema, resetPasswordSchema, newPasswordSchema } from './zodTypes' import fs from 'fs' import { httpLogger } from './logger' import { getPublicPath } from './storage' @@ -85,6 +85,30 @@ async function addHttpRoutes(app: Application) { return res.status(400).json({ message: 'Failed to send password reset request' }) }) + /** + * New password + * @param req + * @param res + */ + app.post('/new-password', async (req: Request, res: Response) => { + const { urlToken, password } = req.body + + try { + newPasswordSchema.parse({ password }) + } catch (error: any) { + return res.status(400).json({ message: error.errors[0]?.message }) + } + + const userService = new UserService() + const resetPassword = await userService.newPassword( urlToken, password ) + + if (resetPassword) { + return res.status(200).json({ message: 'Password has been reset' }) + } + + return res.status(400).json({ message: 'Failed to set new password' }) + }) + /** * Get all tiles from a zone as an array of ids * @param req diff --git a/src/utilities/zodTypes.ts b/src/utilities/zodTypes.ts index e6831f4..7e3aa36 100644 --- a/src/utilities/zodTypes.ts +++ b/src/utilities/zodTypes.ts @@ -41,6 +41,15 @@ export const resetPasswordSchema = z.object({ .regex(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, { message: 'Email must be valid' }) }) +export const newPasswordSchema = z.object({ + password: z + .string() + .min(8, { + message: 'Password must be at least 8 characters long' + }) + .max(255) +}) + export const ZCharacterCreate = z.object({ name: z .string()