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 9fae7c4..d972670 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -4,6 +4,7 @@ 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'); const emailVerificationTemplate = require('../services/templates/emailVerification.template'); /** @@ -271,10 +272,61 @@ 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, verifyEmail, }; diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js index ec6f37e..34cf8f8 100644 --- a/src/routes/auth.routes.js +++ b/src/routes/auth.routes.js @@ -4,6 +4,7 @@ const { login, logout, resetPassword, + forgotPassword, verifyEmail, } = require('../controllers/auth.controller'); const validate = require('../middlewares/validate'); @@ -12,6 +13,7 @@ const { registerSchema, loginSchema, resetPasswordSchema, + forgotPasswordSchema, } = require('../validators/auth.validators'); const router = express.Router(); @@ -25,12 +27,11 @@ 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); diff --git a/src/utils/logger.js b/src/utils/logger.js index b9d75c6..5f2aa57 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -17,18 +17,22 @@ const getLoggerStream = () => { }; }; + /** * Logs an informational message * @param {...any} args - Message and optional metadata */ + const info = (...args) => { console.log(...args); }; + /** * Logs an error message * @param {...any} args - Message and optional metadata */ + const error = (...args) => { console.error(...args); }; 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, };