Skip to content

Commit d87d7ee

Browse files
authored
Merge pull request #40 from JamesVictor-O/feat/implement-forgot-password-endpoint
feat: implement forgot password endpoint
2 parents 5d5582a + 1122094 commit d87d7ee

5 files changed

Lines changed: 71 additions & 5 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ EMAIL_HOST="smtp.gmail.com"
77
EMAIL_PORT=587
88
EMAIL_USER="your-email@gmail.com"
99
EMAIL_PASS="your-app-password"
10+
FRONTEND_URL="http://localhost:3000"
1011
STELLAR_HORIZON_URL="https://horizon-testnet.stellar.org"
1112
STELLAR_NETWORK="testnet"
1213
NODE_ENV=development

src/controllers/auth.controller.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const jwt = require('jsonwebtoken');
44
const User = require('../models/User.model');
55
const { sendSuccess } = require('../utils/response');
66
const { sendEmail } = require('../services/email.service');
7+
const passwordResetTemplate = require('../services/templates/passwordReset.template');
78
const emailVerificationTemplate = require('../services/templates/emailVerification.template');
89

910
/**
@@ -271,10 +272,61 @@ const resetPassword = async (req, res, next) => {
271272
}
272273
};
273274

275+
/**
276+
* Request a password reset email
277+
* POST /api/auth/forgot-password
278+
*/
279+
const forgotPassword = async (req, res, next) => {
280+
try {
281+
const { email } = req.body;
282+
const genericMessage =
283+
'If an account with that email exists, a password reset link has been sent.';
284+
285+
const user = await User.findOne({ email: email.toLowerCase() });
286+
287+
if (!user) {
288+
return sendSuccess(res, {}, 200, genericMessage);
289+
}
290+
291+
const resetToken = crypto.randomBytes(32).toString('hex');
292+
const resetTokenHash = crypto.createHash('sha256').update(resetToken).digest('hex');
293+
294+
user.resetPasswordToken = resetTokenHash;
295+
user.resetPasswordExpires = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
296+
await user.save();
297+
298+
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
299+
const resetLink = `${frontendUrl}/reset-password?token=${resetToken}`;
300+
const html = passwordResetTemplate(user.fullName, resetLink, '1 hour');
301+
302+
try {
303+
await sendEmail({
304+
to: user.email,
305+
subject: 'Password Reset Request',
306+
html,
307+
});
308+
} catch {
309+
user.resetPasswordToken = null;
310+
user.resetPasswordExpires = null;
311+
await user.save();
312+
313+
const error = new Error('Failed to send password reset email. Please try again later.');
314+
error.statusCode = 500;
315+
error.isOperational = true;
316+
return next(error);
317+
}
318+
319+
return sendSuccess(res, {}, 200, genericMessage);
320+
} catch (error) {
321+
return next(error);
322+
}
323+
};
324+
274325
module.exports = {
275326
register,
276327
login,
277328
logout,
278329
resetPassword,
330+
forgotPassword,
279331
verifyEmail,
280332
};

src/routes/auth.routes.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const {
44
login,
55
logout,
66
resetPassword,
7+
forgotPassword,
78
verifyEmail,
89
} = require('../controllers/auth.controller');
910
const validate = require('../middlewares/validate');
@@ -12,6 +13,7 @@ const {
1213
registerSchema,
1314
loginSchema,
1415
resetPasswordSchema,
16+
forgotPasswordSchema,
1517
} = require('../validators/auth.validators');
1618

1719
const router = express.Router();
@@ -25,12 +27,11 @@ router.post('/login', validate(loginSchema), login);
2527
// POST /api/auth/logout - Logout user (requires authentication)
2628
router.post('/logout', authenticate, logout);
2729

30+
// POST /api/auth/forgot-password - Request a password reset email
31+
router.post('/forgot-password', validate(forgotPasswordSchema), forgotPassword);
32+
2833
// PATCH /api/auth/reset-password/:token - Reset user password with token
29-
router.patch(
30-
'/reset-password/:token',
31-
validate(resetPasswordSchema),
32-
resetPassword
33-
);
34+
router.patch('/reset-password/:token', validate(resetPasswordSchema), resetPassword);
3435

3536
// GET /api/auth/verify-email/:token - Verify email address using token from verification email
3637
router.get('/verify-email/:token', verifyEmail);

src/utils/logger.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,22 @@ const getLoggerStream = () => {
1717
};
1818
};
1919

20+
2021
/**
2122
* Logs an informational message
2223
* @param {...any} args - Message and optional metadata
2324
*/
25+
2426
const info = (...args) => {
2527
console.log(...args);
2628
};
2729

30+
2831
/**
2932
* Logs an error message
3033
* @param {...any} args - Message and optional metadata
3134
*/
35+
3236
const error = (...args) => {
3337
console.error(...args);
3438
};

src/validators/auth.validators.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,16 @@ const resetPasswordSchema = Joi.object({
3838
}),
3939
});
4040

41+
const forgotPasswordSchema = Joi.object({
42+
email: Joi.string().email().required().messages({
43+
'string.email': 'Please provide a valid email address',
44+
'any.required': 'Email is required',
45+
}),
46+
});
47+
4148
module.exports = {
4249
registerSchema,
4350
loginSchema,
4451
resetPasswordSchema,
52+
forgotPasswordSchema,
4553
};

0 commit comments

Comments
 (0)