From a3385d0244bc5cd9df9a8919b01c618cc04a31c7 Mon Sep 17 00:00:00 2001 From: Adarsh-Nagar-Ops Date: Sun, 19 Oct 2025 17:31:30 +0530 Subject: [PATCH 1/3] Added Forgot Password Logic --- backend/controllers/authController.js | 51 +++++++++++++++++++++++ backend/models/User.js | 11 ++++- backend/package-lock.json | 18 ++++++++ backend/package.json | 2 + backend/routes/authRoutes.js | 4 +- backend/utils.js | 25 ++++++++++- frontend/src/App.jsx | 4 ++ frontend/src/pages/ForgotPasswordPage.jsx | 41 ++++++++++++++++++ frontend/src/pages/LoginPage.jsx | 3 +- frontend/src/pages/ResetPasswordPage.jsx | 41 ++++++++++++++++++ package-lock.json | 6 +++ 11 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 frontend/src/pages/ForgotPasswordPage.jsx create mode 100644 frontend/src/pages/ResetPasswordPage.jsx create mode 100644 package-lock.json diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js index 035a40c..835af20 100644 --- a/backend/controllers/authController.js +++ b/backend/controllers/authController.js @@ -1,6 +1,8 @@ const User = require('../models/User'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); +const crypto=require('crypto'); +const sendEmail=require('../utils/sendEmail'); // Function to generate JWT const generateToken = (id) => { @@ -113,9 +115,58 @@ const completeSetup = async (req, res) => { } }; +const forgotPassword=async (req,res)=>{ + const {email}=req.body; + try { + const user=await User.findOne({email}) + if(!user){ + return res.status(404).json({message:'User not found'}); + } + const resetToken=crypto.randomBytes(32).toString('hex'); + const hashedToken=crypto.createHash('sha256').update(resetToken).digest('hex'); + user.resetPasswordToken=hashedToken; + user.resetPasswordExpires=Date.now()+15*60*1000; + await user.save(); + const resetUrl=`${process.env.FRONTEND_URL}/reset-password?token=${resetToken}`; + await sendEmail({ + to:user.email, + subject:'Password Reset Request', + text:`You requested a password reset. Please click the link to reset your password: ${resetUrl}` + }) + res.status(200).json({message:'Password reset email sent'}); + } catch (error) { + res.status(500).json({message:'Server Error',error:error.message}); + } +} + +const resetPassword=async (req,res)=>{ + const {token,newPassword}=req.body; + const user=''; + try{ + const hashedToken=crypto.createHash('sha256').update(token).digest('hex'); + user=await User.findOne({ + resetPasswordToken:hashedToken, + resetPasswordExpires:{$gt:Date.now()} + }) + } + catch(error){ + res.status(500).json({message:'Server Error',error:error.message}); + } + if(!user){ + return res.status(400).json({message:'Invalid or expired token'}) + } + user.password=newPassword; + user.resetPasswordToken=null; + user.resetPasswordExpires=null; + await user.save(); + res.status(200).json({message:'Password reset successful'}); +} + module.exports = { signup, login, getMe, completeSetup, + forgotPassword, + resetPassword }; \ No newline at end of file diff --git a/backend/models/User.js b/backend/models/User.js index ce98e5a..5292009 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -20,7 +20,16 @@ const userSchema = new mongoose.Schema({ type: Boolean, default: false, }, -}, { + resetPasswordToken:{ + type:String, + default:null + }, + resetPasswordExpires:{ + type:Date, + default:null + } +} + , { timestamps: true, }); diff --git a/backend/package-lock.json b/backend/package-lock.json index 30137a9..1b4c084 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,6 +13,7 @@ "axios": "^1.12.2", "bcryptjs": "^3.0.2", "cors": "^2.8.5", + "crypto": "^1.0.1", "dotenv": "^17.2.2", "express": "^5.1.0", "express-validator": "^7.2.1", @@ -21,6 +22,7 @@ "mongoose": "^8.18.1", "multer": "^2.0.2", "node-cron": "^4.2.1", + "nodemailer": "^7.0.9", "papaparse": "^5.5.3", "sanitize-html": "^2.17.0" }, @@ -1946,6 +1948,13 @@ "node": ">= 8" } }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", + "license": "ISC" + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -4478,6 +4487,15 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemailer": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.9.tgz", + "integrity": "sha512-9/Qm0qXIByEP8lEV2qOqcAW7bRpL8CR9jcTwk3NBnHJNmP9fIJ86g2fgmIXqHY+nj55ZEMwWqYAT2QTDpRUYiQ==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", diff --git a/backend/package.json b/backend/package.json index 4dc34cd..637ac13 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,6 +18,7 @@ "axios": "^1.12.2", "bcryptjs": "^3.0.2", "cors": "^2.8.5", + "crypto": "^1.0.1", "dotenv": "^17.2.2", "express": "^5.1.0", "express-validator": "^7.2.1", @@ -26,6 +27,7 @@ "mongoose": "^8.18.1", "multer": "^2.0.2", "node-cron": "^4.2.1", + "nodemailer": "^7.0.9", "papaparse": "^5.5.3", "sanitize-html": "^2.17.0" }, diff --git a/backend/routes/authRoutes.js b/backend/routes/authRoutes.js index 4c4679f..5b2c94c 100644 --- a/backend/routes/authRoutes.js +++ b/backend/routes/authRoutes.js @@ -1,11 +1,13 @@ const express = require('express'); const router = express.Router(); -const { signup, login, getMe, completeSetup } = require('../controllers/authController'); +const { signup, login, getMe, completeSetup, forgotPassword, resetPassword } = require('../controllers/authController'); const { protect } = require('../middleware/authMiddleware'); const { validateRegistration } = require('../middleware/validationMiddleware'); router.post('/signup', validateRegistration, signup); router.post('/login', login); +router.post('/forgot-password',forgotPassword); +router.post('/reset-password/:token',resetPassword) router.get('/me', protect, getMe); router.put('/setup', protect, completeSetup); diff --git a/backend/utils.js b/backend/utils.js index 1dbaf2f..677cc49 100644 --- a/backend/utils.js +++ b/backend/utils.js @@ -1,3 +1,14 @@ +const nodemailer=require('nodemailer'); + +const transporter=nodemailer.createTransport({ + host:process.env.EMAIL_HOST, + port:process.env.EMAIL_PORT, + auth:{ + user:process.env.EMAIL_USER, + pass:process.env.EMAIL_PASS + } +}) + const calculateNextDueDate = (startDate, frequency) => { const start = new Date(startDate); if (isNaN(start.getTime())) { @@ -27,4 +38,16 @@ const calculateNextDueDate = (startDate, frequency) => { return nextDueDate; }; -module.exports = { calculateNextDueDate }; \ No newline at end of file +const sendEmail= async(options)=>{ + const mailOptions={ + from:`Paisable <${process.env.EMAIL_FROM}>`, + to:options.email, + subject:options.subject, + text:options.message + } + await transporter.sendMail(mailOptions); +} + + + +module.exports = { calculateNextDueDate,sendEmail }; \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 3ba7157..da2c71d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -15,6 +15,8 @@ import Layout from './components/Layout'; import ProtectedRoute from './components/ProtectedRoute'; import SetupProtectedRoute from './components/SetupProtectedRoute'; import RecurringTransactions from './pages/RecurringTransactions'; +import ForgotPasswordPage from './pages/ForgotPasswordPage'; +import ResetPasswordPage from './pages/ResetPasswordPage'; function App() { return ( @@ -25,6 +27,8 @@ function App() { } /> } /> } /> + }> + }> {/* Protected Routes */} { + e.preventDefault(); + setServerError(''); + setSuccessMessage(''); + try { + const response = await axios.post('/api/auth/forgot-password', { email }); + setSuccessMessage(response.data.message); + } catch (error) { + setServerError(error.response?.data?.message || 'Something went wrong. Please try again.'); + } + } + + return ( +
+
+

Forgot Password

+ {serverError &&

{serverError}

} + {successMessage &&

{successMessage}

} +
+
+ + { + setEmail(e.target.value); + }} /> +
+
+ +
+
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/src/pages/LoginPage.jsx b/frontend/src/pages/LoginPage.jsx index e211bee..a01da13 100644 --- a/frontend/src/pages/LoginPage.jsx +++ b/frontend/src/pages/LoginPage.jsx @@ -46,10 +46,11 @@ export default function LoginPage() { setEmail(e.target.value)} required /> -
+
setPassword(e.target.value)} />
+ Forgot Password?
{/* +
+ +
+ + ) +} + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..cd43f86 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "paisable", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} From 18ece2f8a2b42b3f7c70b11fa77a324b6e4ead16 Mon Sep 17 00:00:00 2001 From: Adarsh-Nagar-Ops Date: Thu, 23 Oct 2025 01:58:19 +0530 Subject: [PATCH 2/3] Resolved file and route issue and added link to home page on forgot-password page --- backend/controllers/authController.js | 27 +++++++++++++---------- backend/routes/authRoutes.js | 2 +- frontend/src/pages/ForgotPasswordPage.jsx | 8 +++++++ 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js index 835af20..540d710 100644 --- a/backend/controllers/authController.js +++ b/backend/controllers/authController.js @@ -2,7 +2,8 @@ const User = require('../models/User'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); const crypto=require('crypto'); -const sendEmail=require('../utils/sendEmail'); +const {sendEmail}=require('../utils'); + // Function to generate JWT const generateToken = (id) => { @@ -141,25 +142,27 @@ const forgotPassword=async (req,res)=>{ const resetPassword=async (req,res)=>{ const {token,newPassword}=req.body; - const user=''; + try{ const hashedToken=crypto.createHash('sha256').update(token).digest('hex'); - user=await User.findOne({ + const user = await User.findOne({ resetPasswordToken:hashedToken, resetPasswordExpires:{$gt:Date.now()} - }) + }); + + if(!user){ + return res.status(400).json({message:'Invalid or expired token'}); + } + + user.password=newPassword; + user.resetPasswordToken=null; + user.resetPasswordExpires=null; + await user.save(); + res.status(200).json({message:'Password reset successful'}); } catch(error){ res.status(500).json({message:'Server Error',error:error.message}); } - if(!user){ - return res.status(400).json({message:'Invalid or expired token'}) - } - user.password=newPassword; - user.resetPasswordToken=null; - user.resetPasswordExpires=null; - await user.save(); - res.status(200).json({message:'Password reset successful'}); } module.exports = { diff --git a/backend/routes/authRoutes.js b/backend/routes/authRoutes.js index 5b2c94c..a89317c 100644 --- a/backend/routes/authRoutes.js +++ b/backend/routes/authRoutes.js @@ -11,4 +11,4 @@ router.post('/reset-password/:token',resetPassword) router.get('/me', protect, getMe); router.put('/setup', protect, completeSetup); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/frontend/src/pages/ForgotPasswordPage.jsx b/frontend/src/pages/ForgotPasswordPage.jsx index b8220a7..586600d 100644 --- a/frontend/src/pages/ForgotPasswordPage.jsx +++ b/frontend/src/pages/ForgotPasswordPage.jsx @@ -1,5 +1,6 @@ import axios from "axios"; import { useState } from "react"; +import { Link } from "react-router-dom"; export default function ForgotPasswordPage() { const [email, setEmail] = useState(''); @@ -20,6 +21,13 @@ export default function ForgotPasswordPage() { return (
+ + Paisable + +

Forgot Password

{serverError &&

{serverError}

} From 94d77852255d81c67e50e22ce2da2df9d72b6e6c Mon Sep 17 00:00:00 2001 From: Adarsh-Nagar-Ops Date: Fri, 24 Oct 2025 20:04:34 +0530 Subject: [PATCH 3/3] Removed URL param from /reset-password --- backend/routes/authRoutes.js | 3 ++- backend/utils.js | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/backend/routes/authRoutes.js b/backend/routes/authRoutes.js index a89317c..4875807 100644 --- a/backend/routes/authRoutes.js +++ b/backend/routes/authRoutes.js @@ -7,7 +7,8 @@ const { validateRegistration } = require('../middleware/validationMiddleware'); router.post('/signup', validateRegistration, signup); router.post('/login', login); router.post('/forgot-password',forgotPassword); -router.post('/reset-password/:token',resetPassword) +// frontend posts the token in the request body, not the url — keep the route as POST /reset-password +router.post('/reset-password',resetPassword); router.get('/me', protect, getMe); router.put('/setup', protect, completeSetup); diff --git a/backend/utils.js b/backend/utils.js index 677cc49..20dcd7b 100644 --- a/backend/utils.js +++ b/backend/utils.js @@ -39,11 +39,17 @@ const calculateNextDueDate = (startDate, frequency) => { }; const sendEmail= async(options)=>{ + // Accept both styles for compatibility: + // controller may call with { to, subject, text } + // older code may call with { email, subject, message } + const to = options.to || options.email; + const text = options.text || options.message; + const mailOptions={ from:`Paisable <${process.env.EMAIL_FROM}>`, - to:options.email, + to, subject:options.subject, - text:options.message + text } await transporter.sendMail(mailOptions); }