138 lines
5.4 KiB
JavaScript
138 lines
5.4 KiB
JavaScript
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.' });
|
|
}
|
|
}
|
|
}; |