From 282b67a5f7de71825058876dbc1fec7f0335f492 Mon Sep 17 00:00:00 2001 From: JamesVictor-O Date: Thu, 5 Mar 2026 09:17:47 +0100 Subject: [PATCH 1/3] feat: implement forgot password endpoint Add POST /api/auth/forgot-password to allow users to request a password reset via email. The endpoint finds the user by email, generates a secure SHA-256 hashed reset token with 1-hour expiry, and sends a password reset email using the existing email service and template. Always returns HTTP 200 regardless of whether the email exists to prevent user enumeration. Closes #25 Made-with: Cursor --- .env.example | 1 + src/controllers/auth.controller.js | 53 ++++++++++++++++++++++++++++++ src/routes/auth.routes.js | 7 ++-- src/validators/auth.validators.js | 8 +++++ 4 files changed, 67 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 1619e90..fb3d459 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,7 @@ EMAIL_HOST="smtp.gmail.com" EMAIL_PORT=587 EMAIL_USER="your-email@gmail.com" EMAIL_PASS="your-app-password" +FRONTEND_URL="http://localhost:3000" STELLAR_HORIZON_URL="https://horizon-testnet.stellar.org" STELLAR_NETWORK="testnet" NODE_ENV=development diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index 8352933..61ae618 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -3,6 +3,8 @@ const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const User = require('../models/User.model'); const { sendSuccess } = require('../utils/response'); +const { sendEmail } = require('../services/email.service'); +const passwordResetTemplate = require('../services/templates/passwordReset.template'); /** * Logout user by invalidating refresh token @@ -206,9 +208,60 @@ const resetPassword = async (req, res, next) => { } }; +/** + * Request a password reset email + * POST /api/auth/forgot-password + */ +const forgotPassword = async (req, res, next) => { + try { + const { email } = req.body; + const genericMessage = + 'If an account with that email exists, a password reset link has been sent.'; + + const user = await User.findOne({ email: email.toLowerCase() }); + + if (!user) { + return sendSuccess(res, {}, 200, genericMessage); + } + + const resetToken = crypto.randomBytes(32).toString('hex'); + const resetTokenHash = crypto.createHash('sha256').update(resetToken).digest('hex'); + + user.resetPasswordToken = resetTokenHash; + user.resetPasswordExpires = new Date(Date.now() + 60 * 60 * 1000); // 1 hour + await user.save(); + + const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000'; + const resetLink = `${frontendUrl}/reset-password?token=${resetToken}`; + const html = passwordResetTemplate(user.fullName, resetLink, '1 hour'); + + try { + await sendEmail({ + to: user.email, + subject: 'Password Reset Request', + html, + }); + } catch { + user.resetPasswordToken = null; + user.resetPasswordExpires = null; + await user.save(); + + const error = new Error('Failed to send password reset email. Please try again later.'); + error.statusCode = 500; + error.isOperational = true; + return next(error); + } + + return sendSuccess(res, {}, 200, genericMessage); + } catch (error) { + return next(error); + } +}; + module.exports = { register, login, logout, resetPassword, + forgotPassword, }; diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js index 2fa51f7..f7a0f3a 100644 --- a/src/routes/auth.routes.js +++ b/src/routes/auth.routes.js @@ -1,8 +1,8 @@ const express = require('express'); -const { register, login, logout, resetPassword } = require('../controllers/auth.controller'); +const { register, login, logout, resetPassword, forgotPassword } = require('../controllers/auth.controller'); const validate = require('../middlewares/validate'); const authenticate = require('../middlewares/auth'); -const { registerSchema, loginSchema, resetPasswordSchema } = require('../validators/auth.validators'); +const { registerSchema, loginSchema, resetPasswordSchema, forgotPasswordSchema } = require('../validators/auth.validators'); const router = express.Router(); @@ -14,6 +14,9 @@ router.post('/login', validate(loginSchema), login); // POST /api/auth/logout - Logout user (requires authentication) router.post('/logout', authenticate, logout); +// POST /api/auth/forgot-password - Request a password reset email +router.post('/forgot-password', validate(forgotPasswordSchema), forgotPassword); + // PATCH /api/auth/reset-password/:token - Reset user password with token router.patch('/reset-password/:token', validate(resetPasswordSchema), resetPassword); diff --git a/src/validators/auth.validators.js b/src/validators/auth.validators.js index 9f73783..8520c6c 100644 --- a/src/validators/auth.validators.js +++ b/src/validators/auth.validators.js @@ -38,8 +38,16 @@ const resetPasswordSchema = Joi.object({ }), }); +const forgotPasswordSchema = Joi.object({ + email: Joi.string().email().required().messages({ + 'string.email': 'Please provide a valid email address', + 'any.required': 'Email is required', + }), +}); + module.exports = { registerSchema, loginSchema, resetPasswordSchema, + forgotPasswordSchema, }; From 4f814bd78f92600d810a24a0369d5ee801eab8ba Mon Sep 17 00:00:00 2001 From: JamesVictor-O Date: Thu, 5 Mar 2026 09:39:32 +0100 Subject: [PATCH 2/3] fix: add logger info and error methods Made-with: Cursor --- src/utils/logger.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/utils/logger.js b/src/utils/logger.js index f50f4e4..3c3b59d 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -9,12 +9,20 @@ */ const getLoggerStream = () => { return { - write: (message) => { + write: message => { if (message && typeof message === 'string') { console.log(message.trim()); } - } + }, }; }; -module.exports = { getLoggerStream }; +const info = (...args) => { + console.log(...args); +}; + +const error = (...args) => { + console.error(...args); +}; + +module.exports = { getLoggerStream, info, error }; From 1122094c603b4169045c9a36212b717182251bfd Mon Sep 17 00:00:00 2001 From: JamesVictor-O Date: Thu, 5 Mar 2026 14:41:27 +0100 Subject: [PATCH 3/3] fix: dedupe auth routes imports and schemas Made-with: Cursor --- src/routes/auth.routes.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js index 3a5bce8..34cf8f8 100644 --- a/src/routes/auth.routes.js +++ b/src/routes/auth.routes.js @@ -1,13 +1,10 @@ const express = require('express'); -const { register, login, logout, resetPassword, forgotPassword } = require('../controllers/auth.controller'); -const validate = require('../middlewares/validate'); -const authenticate = require('../middlewares/auth'); -const { registerSchema, loginSchema, resetPasswordSchema, forgotPasswordSchema } = require('../validators/auth.validators'); const { register, login, logout, resetPassword, + forgotPassword, verifyEmail, } = require('../controllers/auth.controller'); const validate = require('../middlewares/validate'); @@ -16,6 +13,7 @@ const { registerSchema, loginSchema, resetPasswordSchema, + forgotPasswordSchema, } = require('../validators/auth.validators'); const router = express.Router(); @@ -28,16 +26,12 @@ router.post('/login', validate(loginSchema), login); // POST /api/auth/logout - Logout user (requires authentication) router.post('/logout', authenticate, logout); + // POST /api/auth/forgot-password - Request a password reset email router.post('/forgot-password', validate(forgotPasswordSchema), forgotPassword); - // PATCH /api/auth/reset-password/:token - Reset user password with token -router.patch( - '/reset-password/:token', - validate(resetPasswordSchema), - resetPassword -); +router.patch('/reset-password/:token', validate(resetPasswordSchema), resetPassword); // GET /api/auth/verify-email/:token - Verify email address using token from verification email router.get('/verify-email/:token', verifyEmail);