const crypto = require('crypto'); const MailService = require('../../services/email/MailService'); const UnitOfWork = require('../../database/UnitOfWork'); const PersonalUserRepository = require('../../repositories/user/personal/PersonalUserRepository'); const CompanyUserRepository = require('../../repositories/user/company/CompanyUserRepository'); const PasswordResetRepository = require('../../repositories/passwordReset/PasswordResetRepository'); const User = require('../../models/User'); const { logger } = require('../../middleware/logger'); function generateToken() { return crypto.randomBytes(32).toString('hex'); } module.exports = { async requestPasswordReset(req, res) { // If rate limiter already sent a response, do not continue if (res.headersSent) return; const { email } = req.body; if (!email || typeof email !== 'string') { logger.warn('passwordReset:invalid_request', { email }); return res.status(400).json({ success: false, message: 'Invalid request.' }); } // Always respond generically let user = null, userType = null; const uow = new UnitOfWork(); try { await uow.start(); const personalRepo = new PersonalUserRepository(uow); const companyRepo = new CompanyUserRepository(uow); const passwordResetRepo = new PasswordResetRepository(uow); user = await personalRepo.findByEmail(email); userType = user ? 'personal' : null; if (!user) { user = await companyRepo.findByEmail(email); userType = user ? 'company' : null; } if (user) { logger.info('passwordReset:user_found', { userId: user.id, userType }); const existingToken = await passwordResetRepo.findValidTokenByUserId(user.id); if (existingToken) { await uow.rollback(); logger.info('passwordReset:existing_token', { userId: user.id }); return res.json({ success: true, message: 'A password reset link has already been sent and is still valid. Please check your email.' }); } // Generate token and expiry const token = generateToken(); const expiresAt = new Date(Date.now() + 25 * 60 * 1000); // Store in password_resets await passwordResetRepo.createToken(user.id, token, expiresAt); logger.info('passwordReset:token_created', { userId: user.id, token }); // Send email await MailService.sendPasswordResetEmail({ email, firstName: userType === 'personal' ? user.firstName : undefined, companyName: userType === 'company' ? user.companyName : undefined, token, lang: req.body.lang || 'en' }); logger.info('passwordReset:email_sent', { email }); } await uow.commit(); } catch (err) { await uow.rollback(); logger.error('passwordReset:request_error', { error: err }); } return res.json({ success: true, message: 'If an account exists for this email, a password reset link has been sent.' }); }, async verifyPasswordResetToken(req, res) { const { token } = req.query; if (!token || typeof token !== 'string') { logger.warn('passwordReset:invalid_token', { token }); return res.status(400).json({ success: false, message: 'Invalid token.' }); } const uow = new UnitOfWork(); try { await uow.start(); const passwordResetRepo = new PasswordResetRepository(uow); const row = await passwordResetRepo.findValidToken(token); if (!row) { await uow.rollback(); logger.warn('passwordReset:token_invalid_or_expired', { token }); return res.status(400).json({ success: false, message: 'Invalid or expired token.' }); } await uow.commit(); logger.info('passwordReset:token_valid', { token }); return res.json({ success: true, message: 'Token is valid.' }); } catch (err) { await uow.rollback(); logger.error('passwordReset:verify_error', { error: err }); return res.status(500).json({ success: false, message: 'Internal server error.' }); } }, async resetPassword(req, res) { const { token, newPassword } = req.body; if (!token || typeof token !== 'string' || !newPassword || typeof newPassword !== 'string' || newPassword.length < 8) { logger.warn('passwordReset:invalid_reset_request', { token }); return res.status(400).json({ success: false, message: 'Invalid request.' }); } const uow = new UnitOfWork(); try { await uow.start(); const passwordResetRepo = new PasswordResetRepository(uow); const row = await passwordResetRepo.findValidToken(token); if (!row) { await uow.rollback(); logger.warn('passwordReset:token_invalid_or_expired', { token }); return res.status(400).json({ success: false, message: 'Invalid or expired token.' }); } // Update password const hashed = await User.hashPassword(newPassword); await passwordResetRepo.updateUserPassword(row.user_id, hashed); await passwordResetRepo.markTokenUsed(row.id); await uow.commit(); logger.info('passwordReset:password_reset', { userId: row.user_id }); return res.json({ success: true, message: 'Password has been reset.' }); } catch (err) { await uow.rollback(); logger.error('passwordReset:reset_error', { error: err }); return res.status(500).json({ success: false, message: 'Internal server error.' }); } } };