Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions backend/controllers/authController.js
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down Expand Up @@ -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
};
11 changes: 10 additions & 1 deletion backend/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});

Expand Down
18 changes: 18 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
},
Expand Down
4 changes: 3 additions & 1 deletion backend/routes/authRoutes.js
Original file line number Diff line number Diff line change
@@ -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);

Expand Down
25 changes: 24 additions & 1 deletion backend/utils.js
Original file line number Diff line number Diff line change
@@ -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())) {
Expand Down Expand Up @@ -27,4 +38,16 @@ const calculateNextDueDate = (startDate, frequency) => {
return nextDueDate;
};

module.exports = { calculateNextDueDate };
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 };
4 changes: 4 additions & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -25,6 +27,8 @@ function App() {
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/contact" element={<ContactUs />} />
<Route path='/forgot-password' element={<ForgotPasswordPage />}></Route>
<Route path='/reset-password' element={<ResetPasswordPage />}></Route>
{/* Protected Routes */}
<Route
path="/setup"
Expand Down
41 changes: 41 additions & 0 deletions frontend/src/pages/ForgotPasswordPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import axios from "axios";
import { useState } from "react";

export default function ForgotPasswordPage() {
const [email, setEmail] = useState('');
const [serverError, setServerError] = useState('');
const [successMessage, setSuccessMessage] = useState('');

const handleSubmit = async (e) => {
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 (
<div className="flex flex-col justify-center items-center px-4 min-h-screen bg-gray-100 dark:bg-gray-900">
<div className="w-full max-w-md p-8 rounded-md bg-white dark:bg-gray-800 shadow-lg">
<h2 className="flex justify-center text-2xl text-white">Forgot Password</h2>
{serverError && <p className="text-center text-red-500 bg-red-100 dark:bg-red-900 p-2 rounded-md my-4">{serverError}</p>}
{successMessage && <p className="text-center text-green-500 bg-green-500 dark:bg-green-900 p-2 rounded-md my-4">{successMessage}</p>}
<form onSubmit={handleSubmit} className="mt-4">
<div className="mb-4">
<label className="block text-gray-500 dark:text-gray-300 mb-2" htmlFor="email" >Email address</label>
<input type="email" id="email" className="w-full px-4 py-2 border rounded-md focus:outline-none focus:ring-1 focus:ring-blue-600 dark:border-gray-600" required onChange={(e) => {
setEmail(e.target.value);
}} />
</div>
<div className="flex justify-center">
<button type="submit" className="px-6 py-2 text-white bg-blue-500 rounded-md hover:bg-blue-600">Send Reset Link</button>
</div>
</form>
</div>
</div>
)
}
41 changes: 24 additions & 17 deletions frontend/src/pages/LoginPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ export default function LoginPage() {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 px-4 py-8">
{/* Logo/Brand */}
<Link
to="/"
className="text-5xl font-bold bg-gradient-to-r from-blue-600 to-blue-500 dark:from-blue-400 dark:to-blue-300 bg-clip-text text-transparent mb-12 transition-all duration-500 hover:scale-105 hover:drop-shadow-2xl cursor-pointer animate-fade-in"
<Link
to="/"
className="text-5xl font-bold bg-gradient-to-r from-blue-600 to-blue-500 dark:from-blue-400 dark:to-blue-300 bg-clip-text text-transparent mb-12 transition-all duration-500 hover:scale-105 hover:drop-shadow-2xl cursor-pointer animate-fade-in"
title="Go to home"
>
Paisable
Expand Down Expand Up @@ -72,14 +72,14 @@ export default function LoginPage() {
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<HiMail aria-hidden="true" className="h-5 w-5 text-gray-400 dark:text-gray-500" />
</div>
<input
type="email"
placeholder="you@example.com"
className="w-full pl-10 pr-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
<input
type="email"
placeholder="you@example.com"
className="w-full pl-10 pr-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
</div>
Expand All @@ -89,16 +89,23 @@ export default function LoginPage() {
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2" htmlFor="password">
Password
</label>
<PasswordInput
value={password}
<PasswordInput
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
/>
</div>

{/* Forgot Password Link */}
<div className="flex justify-end">
<Link to='/forgot-password' className='text-blue-600 dark:text-blue-400 hover:underline text-sm'>
Forgot Password?
</Link>
</div>

{/* Submit Button */}
<button
type="submit"
<button
type="submit"
disabled={isLoading}
className="w-full px-6 py-3 text-white font-semibold bg-gradient-to-r from-blue-600 to-blue-500 rounded-lg hover:from-blue-700 hover:to-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transform transition-all duration-200 hover:scale-[1.02] active:scale-[0.98] shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none flex items-center justify-center gap-2"
>
Expand Down Expand Up @@ -132,8 +139,8 @@ export default function LoginPage() {

{/* Register Link */}
<div className="text-center">
<Link
to="/register"
<Link
to="/register"
className="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-semibold transition-colors duration-200"
>
Create an account
Expand Down
41 changes: 41 additions & 0 deletions frontend/src/pages/ResetPasswordPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import axios from "axios";
import { useState } from "react";
import { useSearchParams } from "react-router-dom";

export default function ResetPasswordPage() {
//get the token
const [searchParams] = useSearchParams();
const token = searchParams.get('token');

const [newPassword,setNewPassword]=useState('');

const handleSubmit=async(e)=>{
e.preventDefault();
try{
await axios.post('/api/auth/reset-password',{token,newPassword});
alert('Password reset successful. You can now log in with your new password.');
}catch(error){
alert(error.response?.data?.message || 'Something went wrong. Please try again.');
}
}

return (
<div className="flex flex-col justify-center items-center px-4 min-h-screen bg-gray-100 dark:bg-gray-900">
<div className="w-full max-w-md p-8 rounded-md bg-white dark:bg-gray-800 shadow-lg">
<h2 className="flex justify-center text-2xl text-white">Reset Password</h2>
<form className="mt-4" onSubmit={handleSubmit}>
<div className="mb-4">
<label className="block text-gray-500 dark:text-gray-300 mb-2" htmlFor="newPassword">New Password</label>
<input type="password" id="newPassword" className="w-full px-4 py-2 border rounded-md focus:outline-none focus:ring-1 focus:ring-blue-600 dark:border-gray-600" required onChange={(e)=>{
setNewPassword(e.target.value);
}}/>
</div>
<div className="flex justify-center">
<button type="submit" className="px-6 py-2 text-white bg-blue-500 rounded-md hover:bg-blue-600">Reset Password</button>
</div>
</form>
</div>
</div>
)
}

6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.