Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
64 changes: 64 additions & 0 deletions backend/src/controllers/userController.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const User = require("../models/User");
const { sendEmail } = require("../services/emailService");
require('dotenv').config();


Expand Down Expand Up @@ -54,3 +55,66 @@ exports.login = async (req, res) => {
res.status(400).json({ success: false, error: err.message });
}
};
exports.sendOtp = async (req,res) => {
try {
const {email} = req.body;
const user = await User.findOne({email});
if (!user) return res.status(400).json({message:"User does not exist."});

const otp = Math.floor(1000 + Math.random() * 9000).toString(); // 4-digit OTP
user.resetOtp = otp;
user.otpExpires = Date.now() + 5*60*1000; // 5 minutes
user.isOtpVerified = false;
await user.save();

await sendEmail




({ to: email, otp }); // ✅ Correct usage
return res.status(200).json({message:"OTP sent successfully"});
} catch (error) {
return res.status(500).json({message: `send otp error ${error}`});
}
};


exports.verifyOtp = async (req,res) => {
try {
const {email, otp} = req.body;
const user = await User.findOne({email});
if (!user || user.resetOtp !== otp || user.otpExpires < Date.now()) {
return res.status(400).json({message:"Invalid/expired OTP"});
}

user.isOtpVerified = true;
user.resetOtp = undefined;
user.otpExpires = undefined;
await user.save();

return res.status(200).json({message:"OTP verified successfully"});
} catch (error) {
return res.status(500).json({message: `verify otp error ${error}`});
}
};

exports.resetPassword = async (req,res) => {
try {
const {email, newPassword} = req.body;
const user = await User.findOne({email});
if (!user || !user.isOtpVerified) {
return res.status(400).json({message:"OTP verification required"});
}

const hashedPassword = await bcrypt.hash(newPassword, 10);
user.password = hashedPassword;
user.isOtpVerified = false; // reset verification
await user.save();

return res.status(200).json({message:"Password reset successfully"});
} catch (error) {
return res.status(500).json({message: `reset password error ${error}`});
}
};

21 changes: 17 additions & 4 deletions backend/src/middlewares/validationMiddleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,9 @@ const chatbotSendMessageSchema = z.object({

const validateRequest = (schema) => {
return (req, res, next) => {
try {

try {

const validatedData = schema.parse(req.body);

const validatedData = schema.parse(req.body);


req.body = validatedData;
Expand Down Expand Up @@ -101,14 +99,29 @@ const validateRequest = (schema) => {
}
};
};
const loginValidationSchema = z.object({
email: z.string()
.email('Invalid email format')
.min(1, 'Email is required')
.max(100, 'Email must be less than 100 characters')
.trim()
.toLowerCase(),

password: z.string()
.min(6, 'Password must be at least 6 characters')
.max(100, 'Password must be less than 100 characters')
});



const validateUser = validateRequest(userValidationSchema);
const validateMessage = validateRequest(messageValidationSchema);
const validateChatbotMessage = validateRequest(chatbotMessageSchema);
const validateChatbotSendMessage = validateRequest(chatbotSendMessageSchema);
const validateLogin = validateRequest(loginValidationSchema);

module.exports = {
validateLogin,
validateUser,
validateMessage,
validateChatbotMessage,
Expand Down
11 changes: 11 additions & 0 deletions backend/src/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ const UserSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
resetOtp:{
type:String
},
isOtpVerified:{
type:Boolean,
default:false
},
otpExpires:{
type:Date
},

}, { timestamps: true });

module.exports = mongoose.model('User', UserSchema);
10 changes: 7 additions & 3 deletions backend/src/routes/userRoutes.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
const express = require('express');
const router = express.Router();
const { register, login } = require('../controllers/userController');
const { validateUser } = require('../middlewares/validationMiddleware');
const { register, login, sendOtp, verifyOtp, resetPassword } = require('../controllers/userController');
const { validateUser, validateLogin } = require('../middlewares/validationMiddleware');

router.post('/register', validateUser, register);
router.post('/login', validateUser, login);
router.post('/login', validateLogin, login);
router.post("/send-otp",sendOtp)
router.post("/verify-otp",verifyOtp)
router.post("/reset-password",resetPassword)


module.exports = router;
8 changes: 7 additions & 1 deletion backend/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ const { errorMiddleware } = require('./middlewares/errorMiddleware');
const chatbotRoutes = require('./routes/chatbotRoutes');

const app = express();
app.use(cors());
app.use(
cors({
origin: "http://localhost:5173",
credentials: true,
})
);

app.use(express.json());

app.use('/api/users', userRoutes);
Expand Down
26 changes: 12 additions & 14 deletions backend/src/services/emailService.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
const nodemailer = require('nodemailer');
const nodemailer = require("nodemailer");

exports.sendEmail = async ({ to, otp }) => {
if (!to) throw new Error("Recipient email is required");

const sendEmail = async ({ to, subject, text, html }) => {
// This uses ethereal by default for local testing
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST || 'smtp.ethereal.email',
port: process.env.SMTP_PORT || 587,
secure: false,
host: process.env.SMTP_HOST || "smtp.gmail.com",
port: process.env.SMTP_PORT || 465,
secure: true, // must be true for port 465
auth: {
user: process.env.SMTP_USER || '',
pass: process.env.SMTP_PASS || '',
user: process.env.EMAIL,
pass: process.env.PASS,
},
});

const info = await transporter.sendMail({
from: process.env.EMAIL_FROM || 'no-reply@mailmern.local',
from: process.env.EMAIL,
to,
subject,
text,
html,
subject: "Reset Your Password",
html: `<p>Your OTP is <b>${otp}</b>. It expires in 5 minutes.</p>`,
});

return info;
};

module.exports = { sendEmail };
2 changes: 1 addition & 1 deletion frontend/src/pages/Register.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ const Register = () => {
<button
type="button"
onClick={() => setShowPassword(prev => !prev)}
className="absolute right-3 top-3 text-gray-400 hover:text-cyan-400 transition"
className="absolute right-3 top-4 text-gray-400 hover:text-cyan-400 transition"
>
{showPassword ? <FaRegEyeSlash /> : <FaRegEye />}
</button>
Expand Down
Loading