From a4e96f9ede0968fea2fcd9af9b7ac450f16c3184 Mon Sep 17 00:00:00 2001 From: Colin Kallemein Date: Tue, 29 Oct 2024 22:49:21 +0100 Subject: [PATCH] (WIP) Added pw reset token row, added checks to reset function --- package-lock.json | 11 ++++++ package.json | 1 + .../migration.sql | 14 ++++++++ prisma/schema/user.prisma | 23 ++++++++---- src/services/userService.ts | 35 +++++++++++++++---- src/utilities/config.ts | 1 + src/utilities/http.ts | 4 +-- 7 files changed, 73 insertions(+), 16 deletions(-) rename prisma/migrations/{20241027162231_init => 20241028210606_init}/migration.sql (94%) diff --git a/package-lock.json b/package-lock.json index e819f41..fa89162 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@types/express": "^4.17.21", "@types/jsonwebtoken": "^9.0.6", "@types/node": "^20.14.11", + "@types/nodemailer": "^6.4.16", "nodemon": "^3.1.4", "prettier": "^3.3.3" } @@ -728,6 +729,16 @@ "undici-types": "~6.19.2" } }, + "node_modules/@types/nodemailer": { + "version": "6.4.16", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.16.tgz", + "integrity": "sha512-uz6hN6Pp0upXMcilM61CoKyjT7sskBoOWpptkjjJp8jIMlTdc3xG01U7proKkXzruMS4hS0zqtHNkNPFB20rKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.16", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", diff --git a/package.json b/package.json index d6b5203..b1ddb7a 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@types/express": "^4.17.21", "@types/jsonwebtoken": "^9.0.6", "@types/node": "^20.14.11", + "@types/nodemailer": "^6.4.16", "nodemon": "^3.1.4", "prettier": "^3.3.3" } diff --git a/prisma/migrations/20241027162231_init/migration.sql b/prisma/migrations/20241028210606_init/migration.sql similarity index 94% rename from prisma/migrations/20241027162231_init/migration.sql rename to prisma/migrations/20241028210606_init/migration.sql index 60cc439..70e6ef1 100644 --- a/prisma/migrations/20241027162231_init/migration.sql +++ b/prisma/migrations/20241028210606_init/migration.sql @@ -49,6 +49,17 @@ CREATE TABLE `User` ( PRIMARY KEY (`id`) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +-- CreateTable +CREATE TABLE `PasswordResetToken` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `userId` INTEGER NOT NULL, + `token` VARCHAR(191) NOT NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + UNIQUE INDEX `PasswordResetToken_token_key`(`token`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + -- CreateTable CREATE TABLE `CharacterType` ( `id` INTEGER NOT NULL AUTO_INCREMENT, @@ -204,6 +215,9 @@ ALTER TABLE `Chat` ADD CONSTRAINT `Chat_zoneId_fkey` FOREIGN KEY (`zoneId`) REFE -- AddForeignKey ALTER TABLE `SpriteAction` ADD CONSTRAINT `SpriteAction_spriteId_fkey` FOREIGN KEY (`spriteId`) REFERENCES `Sprite`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; +-- AddForeignKey +ALTER TABLE `PasswordResetToken` ADD CONSTRAINT `PasswordResetToken_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + -- AddForeignKey ALTER TABLE `CharacterType` ADD CONSTRAINT `CharacterType_spriteId_fkey` FOREIGN KEY (`spriteId`) REFERENCES `Sprite`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema/user.prisma b/prisma/schema/user.prisma index d67fc4b..7c1f967 100644 --- a/prisma/schema/user.prisma +++ b/prisma/schema/user.prisma @@ -1,10 +1,19 @@ model User { - id Int @id @default(autoincrement()) - username String @unique - email String @unique - password String - online Boolean @default(false) - characters Character[] + id Int @id @default(autoincrement()) + username String @unique + email String @unique + password String + online Boolean @default(false) + characters Character[] + passwordResetTokens PasswordResetToken[] +} + +model PasswordResetToken { + id Int @id @default(autoincrement()) + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + token String @unique + createdAt DateTime @default(now()) } enum CharacterGender { @@ -62,4 +71,4 @@ model CharacterItem { itemId String item Item @relation(fields: [itemId], references: [id], onDelete: Cascade) quantity Int -} +} \ No newline at end of file diff --git a/src/services/userService.ts b/src/services/userService.ts index 92e9644..f974200 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -1,8 +1,9 @@ import bcrypt from 'bcryptjs' import UserRepository from '../repositories/userRepository' import prisma from '../utilities/prisma' -import { User } from '@prisma/client' +import { User, PasswordResetToken } from '@prisma/client' import config from '../utilities/config' +import NodeMailer from 'nodemailer' /** * User service @@ -59,10 +60,27 @@ class UserService { * Reset password * @param email */ - async resetPassword(email: string): Promise { - const nodemailer = require("nodemailer"); + async resetPassword(email: string): Promise { - const transporter = nodemailer.createTransport({ + const user = await UserRepository.getByEmail(email) + if ( !user ) return + const token = await bcrypt.genSalt(10) + + //Check if password reset has been requested recently + if (await prisma.passwordResetToken.findFirst({ + where: { + userId: user.id + }, + })) return + + prisma.passwordResetToken.create({ + data: { + userId: user.id, + token: token, + } + }); + + const transporter = NodeMailer.createTransport({ host: config.SMTP_HOST, port: config.SMTP_PORT, secure: false, @@ -76,12 +94,15 @@ class UserService { from: config.SMTP_USER, to: email, subject: "Reset your password", - text: "A password reset has been requested, reset your password here: ", // Plain text body - html: "

A password reset has been requested, reset your password here:

", // 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 }); console.log("Message sent: %s", info.messageId); - return info.messageId + + if (info) { + return true + } } } diff --git a/src/utilities/config.ts b/src/utilities/config.ts index f8c7be8..4359d2d 100644 --- a/src/utilities/config.ts +++ b/src/utilities/config.ts @@ -7,6 +7,7 @@ class config { static REDIS_URL: string = process.env.REDIS_URL || 'redis://@127.0.0.1:6379/4' static HOST: string = process.env.HOST || '0.0.0.0' static PORT: number = process.env.PORT ? parseInt(process.env.PORT) : 6969 + static CLIENT_URL: string = process.env.CLIENT_URL ? process.env.CLIENT_URL : 'https://sylvan.quest/' static JWT_SECRET: string = process.env.JWT_SECRET || 'secret' static ALLOW_DIAGONAL_MOVEMENT: boolean = process.env.ALLOW_DIAGONAL_MOVEMENT === 'true' diff --git a/src/utilities/http.ts b/src/utilities/http.ts index 45b8f69..ea0d948 100644 --- a/src/utilities/http.ts +++ b/src/utilities/http.ts @@ -73,9 +73,9 @@ async function addHttpRoutes(app: Application) { } const userService = new UserService() - const user = await userService.resetPassword( email ) + const sentEmail = await userService.resetPassword( email ) - if (user) { + if (sentEmail) { return res.status(200).json({ message: 'Email has been sent' }) }