Skip to content
Open
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
30 changes: 20 additions & 10 deletions package-lock.json

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"express": "^4.21.2",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.10.0",
"validator": "^13.12.0"
"nodemailer": "^7.0.5",
"validator": "^13.15.15"
},
"devDependencies": {
"mocha": "^11.1.0"
Expand Down
9 changes: 1 addition & 8 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,7 @@ app.use(cookieParser());
const dotenv = require("dotenv");
dotenv.config();
app.use(cors({
origin: [
"https://dev-tinder-frontend-main.vercel.app",
"http://localhost:5173",
"https://dev-tinder-ax9m.onrender.com",
"www.devtinder.engineer",
"https://www.devtinder.engineer",
"https://legendary-waffle-r4rpvxgr79vvcx5pw-5173.app.github.dev"
],
origin: true, // Allow all origins for development
credentials: true,
}));

Expand Down
59 changes: 59 additions & 0 deletions src/models/passwordReset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const mongoose = require('mongoose');
const { Schema } = mongoose;

const passwordResetSchema = new Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
index: true
},
token: {
type: String,
required: true,
unique: true,
index: true
},
// Store the plain token temporarily to support OTP verification redirect.
// This value is equivalent to what would be sent via email link.
// Do not index this field.
plainToken: {
type: String,
},
expiresAt: {
type: Date,
required: true,
index: true
},
used: {
type: Boolean,
default: false
},
// OTP for email verification (hashed)
otpCode: {
type: String,
},
// When the OTP expires (typically short-lived, e.g., 10 minutes)
otpExpiresAt: {
type: Date,
},
// Track incorrect attempts if needed in future
otpAttempts: {
type: Number,
default: 0,
}
}, {
timestamps: true
});

// Index for automatic cleanup of expired tokens
passwordResetSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });

// Method to check if token is valid and not expired
passwordResetSchema.methods.isValid = function() {
return !this.used && new Date() < this.expiresAt;
};

const PasswordReset = mongoose.model('PasswordReset', passwordResetSchema);

module.exports = PasswordReset;
174 changes: 172 additions & 2 deletions src/routes/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ const express = require('express');
const bcrypt = require('bcrypt');
const {User} = require('../models/user');
const {validateSignupData} = require("../utils/validation");
const PasswordReset = require('../models/passwordReset');
const { sendPasswordResetEmail, sendWelcomeEmail, sendPasswordResetOtpEmail } = require('../utils/emailService');
const crypto = require('crypto');
const validator = require('validator');

const authRouter = express.Router();

Expand All @@ -17,14 +21,21 @@ authRouter.post("/signup", async (req, res) => {
email: emailId,
password: hashedPassword
});
const savedUser = await user.save();
const savedUser = await user.save();
const token = await savedUser.getJWT();

res.cookie("token", token, {
expires: new Date(Date.now() + 8 * 3600000),
});

res.json({ message: "User Added successfully!", data: savedUser });
res.json({ message: "User Added successfully!", data: savedUser });

// Send welcome email (optional)
try {
await sendWelcomeEmail(emailId, firstName);
} catch (emailError) {
console.log('Welcome email failed to send:', emailError.message);
}
} catch (err) {
console.log(err.message);
res.status(400).send("Something went wrong: " + err.message);
Expand Down Expand Up @@ -73,4 +84,163 @@ authRouter.post("/logout", async (req,res) => {
res.send("Error: " + err.message);
}
})

// Forgot Password - Request Reset
authRouter.post("/auth/password/forgot", async (req, res) => {
try {
const { emailId } = req.body;

if (!emailId) {
return res.status(400).json({ message: "Email is required" });
}

// Find user by email
const user = await User.findOne({ email: emailId });
if (!user) {
// Don't reveal if user exists or not for security
return res.json({ message: "If an account with that email exists, a password reset link has been sent." });
}

// Generate secure token for reset URL
const resetToken = crypto.randomBytes(32).toString('hex');
const hashedToken = crypto.createHash('sha256').update(resetToken).digest('hex');

// Generate 6-digit OTP code and hash it for storage
const otpCode = (Math.floor(100000 + Math.random() * 900000)).toString();
const hashedOtp = crypto.createHash('sha256').update(otpCode).digest('hex');

// Create password reset record
const passwordReset = new PasswordReset({
userId: user._id,
token: hashedToken,
plainToken: resetToken,
expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour for final reset
otpCode: hashedOtp,
otpExpiresAt: new Date(Date.now() + 10 * 60 * 1000), // 10 minutes for OTP
otpAttempts: 0,
});

await passwordReset.save();

// Create reset URL
const resetUrl = `${process.env.FRONTEND_URL || 'http://localhost:5173'}/reset-password/${resetToken}`;

// Send OTP email instead of direct link
const emailSent = await sendPasswordResetOtpEmail(emailId, otpCode);

if (emailSent) {
res.json({ message: "If an account with that email exists, an OTP has been sent to your email." });
} else {
res.status(500).json({ message: "Failed to send OTP email. Please try again." });
}

} catch (err) {
console.error('Forgot password error:', err);
res.status(500).json({ message: "Something went wrong. Please try again." });
}
});

// Verify OTP and return reset URL token
authRouter.post("/auth/password/verify-otp", async (req, res) => {
try {
const { emailId, otp } = req.body;
if (!emailId || !otp) {
return res.status(400).json({ message: "Email and OTP are required" });
}

const user = await User.findOne({ email: emailId });
if (!user) {
// do not reveal existence
return res.status(400).json({ message: "Invalid OTP or expired" });
}

// Find the latest unused reset request with a valid OTP
const resetRecord = await PasswordReset.findOne({
userId: user._id,
used: false,
otpExpiresAt: { $gt: new Date() }
}).sort({ createdAt: -1 });

if (!resetRecord || !resetRecord.otpCode) {
return res.status(400).json({ message: "Invalid OTP or expired" });
}

// Rate limit attempts (optional)
if (resetRecord.otpAttempts >= 5) {
return res.status(429).json({ message: "Too many attempts. Request a new code." });
}

const hashedInput = crypto.createHash('sha256').update(otp.toString()).digest('hex');
if (hashedInput !== resetRecord.otpCode) {
resetRecord.otpAttempts = (resetRecord.otpAttempts || 0) + 1;
await resetRecord.save();
return res.status(400).json({ message: "Invalid OTP" });
}

// OTP is valid. Option 1: immediately mark OTP as used by clearing it.
resetRecord.otpCode = null;
resetRecord.otpExpiresAt = null;
resetRecord.otpAttempts = 0;
await resetRecord.save();

// Return the reset URL token to the client to navigate to the reset page
// Or we could also set a short-lived session/cookie. For SPA, returning token is fine.
return res.json({
message: "OTP verified",
resetToken: resetRecord.plainToken,
});
} catch (err) {
console.error('Verify OTP error:', err);
res.status(500).json({ message: "Something went wrong. Please try again." });
}
});
// Reset Password - Set New Password
authRouter.post("/auth/password/reset/:token", async (req, res) => {
try {
const { token } = req.params;
const { newPassword } = req.body;

if (!newPassword) {
return res.status(400).json({ message: "New password is required" });
}

// Hash the token to compare with stored hash
const hashedToken = crypto.createHash('sha256').update(token).digest('hex');

// Find valid password reset record
const passwordReset = await PasswordReset.findOne({
token: hashedToken,
used: false,
expiresAt: { $gt: new Date() }
});

if (!passwordReset) {
return res.status(400).json({ message: "Invalid or expired reset token" });
}

// Validate password strength
if (!validator.isStrongPassword(newPassword)) {
return res.status(400).json({ message: "Password is not strong enough" });
}

// Hash new password
const hashedPassword = await bcrypt.hash(newPassword, 10);

// Update user password
await User.findByIdAndUpdate(passwordReset.userId, {
password: hashedPassword
});

// Mark token as used
passwordReset.used = true;
await passwordReset.save();

res.json({ message: "Password has been reset successfully. You can now login with your new password." });

} catch (err) {
console.error('Reset password error:', err);
res.status(500).json({ message: "Something went wrong. Please try again." });
}
});

module.exports = authRouter;
1 change: 0 additions & 1 deletion src/routes/profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ profileRouter.patch("/profile/password/forgot",userAuth, async(req,res)=>{
throw new Error("The Password is no Strong Enough");
}
else{

const loggedInUser = req.user;
const {_id} = loggedInUser;
const result = await User.findById(_id);
Expand Down
Loading