diff --git a/apps/client/pages/api/auth/reset-password.ts b/apps/client/pages/api/auth/reset-password.ts index 8b1ac9bb..44255910 100644 --- a/apps/client/pages/api/auth/reset-password.ts +++ b/apps/client/pages/api/auth/reset-password.ts @@ -1,19 +1,49 @@ +import { PrismaClient } from '@prisma/client' +import crypto from 'crypto' import type { NextApiRequest, NextApiResponse } from 'next' +let prismaInstance: PrismaClient | null = null + +function getPrismaInstance() { + if (!prismaInstance) { + prismaInstance = new PrismaClient() + } + return prismaInstance +} + +const prisma = getPrismaInstance() + type ResponseData = { message: string } -export default function handler(req: NextApiRequest, res: NextApiResponse) { - // TODO: implement password reset functionality +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!req.body.email) { + res.status(400).json({ message: 'No email provided.' }) + return + } + + const user = await prisma.authUser.findUnique({ + where: { + email: req.body.email, + }, + }) + + if (!user) { + // No user found, we don't want to expose this information + return res.status(200).json({ message: 'OK' }) + } + + const token = crypto.randomBytes(32).toString('hex') + await prisma.authPasswordResets.create({ + data: { + token, + email: req.body.email, + expires: new Date(Date.now() + 1000 * 60 * 10), // 10 minutes + }, + }) - // 1. Generate a password reset token // 2. Send a password reset email - // 3. Redirect to a password reset page - // 4. Verify the password reset token - // 5. Reset the password - // 6. Redirect to the login page - // 7. Login with the new password res.status(200).json({ message: 'Hello from Next.js!' }) } diff --git a/libs/server/features/src/auth-password-reset/auth-password-reset.service.ts b/libs/server/features/src/auth-password-reset/auth-password-reset.service.ts new file mode 100644 index 00000000..daa04d04 --- /dev/null +++ b/libs/server/features/src/auth-password-reset/auth-password-reset.service.ts @@ -0,0 +1,101 @@ +import type { PrismaClient } from '@prisma/client' +import type { Logger } from 'winston' +import type { EmailService } from '../email' +import bcrypt from 'bcrypt' + +type ResetPasswordData = { + newPassword: string + token: string +} + +export interface IAuthPasswordResetService { + create(email: string): Promise + resetPassword(data: ResetPasswordData): Promise +} + +export class AuthPasswordResetsService implements IAuthPasswordResetService { + constructor( + private readonly logger: Logger, + private readonly emailService: EmailService, + private readonly prisma: PrismaClient + ) {} + + async create(email: string): Promise { + const user = await this.prisma.authUser.findUnique({ + where: { + email, + }, + }) + + if (!user) { + this.logger.log({ + level: 'info', + message: `No user found with email ${email}`, + }) + + return null + } + + const token = crypto.randomUUID() + + await this.prisma.authPasswordResets.create({ + data: { + token, + email, + expires: new Date(Date.now() + 1000 * 60 * 10), // 10 minutes + }, + }) + + await this.emailService.send({ + subject: 'Reset your password', + to: email, + // TODO: Use a template + textBody: `Click here to reset your password: ${process.env.NEXTAUTH_URL}/auth/reset-password?token=${token}&email=${email}`, + }) + + return null + } + + async resetPassword(data: { + newPassword: string + email: string + token: string + }): Promise { + const findResult = await this.prisma.authPasswordResets.findUnique({ + where: { + token: data.token, + }, + }) + + if (!findResult) { + throw new Error('Invalid token') + } + + if (findResult.expires < new Date()) { + throw new Error('Token expired') + } + + const user = await this.prisma.authUser.findUnique({ + where: { + email: data.email, + }, + }) + + if (!user) { + throw new Error('No user found') + } + + const hashedPassword = await bcrypt.hash(data.newPassword, 10) + + await this.prisma.authUser.update({ + where: { + email: data.email, + }, + data: { + password: hashedPassword, + }, + }) + + return null + } +} diff --git a/libs/server/features/src/auth-password-reset/index.ts b/libs/server/features/src/auth-password-reset/index.ts new file mode 100644 index 00000000..48f44ff8 --- /dev/null +++ b/libs/server/features/src/auth-password-reset/index.ts @@ -0,0 +1 @@ +export * from './auth-password-reset.service' diff --git a/libs/server/features/src/index.ts b/libs/server/features/src/index.ts index 4fa66d75..e9f6800b 100644 --- a/libs/server/features/src/index.ts +++ b/libs/server/features/src/index.ts @@ -4,6 +4,7 @@ export * from './account-balance' export * from './email' export * from './institution' export * from './security-pricing' +export * from './auth-password-reset' export * from './auth-user' export * from './user' export * from './valuation'