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
234 changes: 210 additions & 24 deletions backend/src/controllers/auth.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import User from "../models/user.model.js";
import bcrypt from "bcryptjs";
import cloudinary from "../lib/cloudinary.js";
import { sendWelcomeEmail, sendOtpEmail } from "../lib/sendEmail.js";
import { sendWelcomeEmail, sendOtpEmail, sendVerificationOtpEmail } from "../lib/sendEmail.js";
import crypto from "crypto";

//signup
Expand All @@ -13,8 +13,10 @@
// Check if user already exists in DB
// Hash password using bcrypt
// Create new user in database
// Generate JWT token and set cookie
// Send user data (without password) in response
// Generate email verification OTP
// Create unverified user
// Send verification OTP email
// Return verification required response
export const signup = async (req, res) => {
try {
const { fullName, email, password, securityQuestions } = req.body;
Expand Down Expand Up @@ -81,35 +83,44 @@
),
}))
);
const verificationOtp = crypto.randomInt(100000, 1000000).toString();

const hashedVerificationOtp = await bcrypt.hash(
verificationOtp,
10
);

// Create user (NO verification fields)
const verificationOtpExpiry = new Date(
Date.now() + 5 * 60 * 1000
);
// Create unverified user with email verification OTP
const newUser = await User.create({
fullName,
email: normalizedEmail,
password: hashedPassword,
securityQuestions: hashedQuestions,
role: "user",
isVerified: false,
emailVerificationOtp: hashedVerificationOtp,
emailVerificationOtpExpiry: verificationOtpExpiry,
});

// Generate JWT immediately
const token = generateToken(newUser._id);

// Send Welcome Email (non-blocking)
setImmediate(() => {
sendWelcomeEmail(
newUser.email,
newUser.fullName
);
// Asynchronously send the verification email to avoid blocking the HTTP response.
setImmediate(async () => {
try {
await sendVerificationOtpEmail(
newUser.email,
verificationOtp
);
} catch (error) {
console.error("Verification email failed:", error);
}
});

// Send user response
res.status(201).json({
_id: newUser._id,
fullName: newUser.fullName,
message:
"Account created successfully. Please verify your email using the OTP sent to your email address.",
Comment on lines +96 to +122

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Don’t report signup success before OTP delivery is attempted.

This creates the unverified user and returns 201 before the verification email is even attempted. Since sendVerificationOtpEmail currently returns early for missing placeholder config and only logs Brevo failures, the user can be left unable to verify or log in while the API still says signup succeeded. Treat OTP delivery as part of the signup/resend transaction, or roll back/surface a failure when delivery cannot start.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/controllers/auth.controller.js` around lines 96 - 117, The signup
flow in auth.controller.js reports success before OTP delivery is even
attempted, so move the verification email step into the main create path around
User.create and sendVerificationOtpEmail instead of setImmediate. In the signup
handler, await or otherwise handle sendVerificationOtpEmail for the
newUser.email before returning the 201 response, and surface a failure or roll
back the created user when OTP delivery cannot be started. Keep the response in
the same signup/resend transaction so the success message only returns when
email delivery has at least been attempted.

email: newUser.email,
profilePic: newUser.profilePic,
role: newUser.role,
token,
});
} catch (error) {
console.error("Signup error:", error);
Expand All @@ -120,6 +131,110 @@
}
};

export const verifyEmail = async (req, res) => {
try {
const { email, otp } = req.body;

// Validate input types
if (
typeof email !== "string" ||
typeof otp !== "string" ||
!email.trim() ||
!otp.trim()
) {
return res.status(400).json({
message: "Email and OTP are required",
});
}

const normalizedEmail = email.toLowerCase().trim();

const user = await User.findOne({
email: normalizedEmail,
}).select(
"+emailVerificationOtp +emailVerificationOtpExpiry"
);

const genericErrorMessage =
"Invalid or expired verification request";

// Prevent user enumeration
if (!user) {
return res.status(400).json({
message: genericErrorMessage,
});
}

if (user.isVerified) {
return res.status(400).json({
message: genericErrorMessage,
});
}

if (
!user.emailVerificationOtp ||
!user.emailVerificationOtpExpiry ||
user.emailVerificationOtpExpiry < new Date()
) {
return res.status(400).json({
message: genericErrorMessage,
});
}

const isOtpValid = await bcrypt.compare(
otp,
user.emailVerificationOtp
);

if (!isOtpValid) {
return res.status(400).json({
message: genericErrorMessage,
});
}

user.isVerified = true;
user.emailVerificationOtp = null;
user.emailVerificationOtpExpiry = null;

await user.save();

// Send welcome email asynchronously
setImmediate(async () => {
try {
await sendWelcomeEmail(
user.email,
user.fullName
);
} catch (error) {
console.error(
"Welcome email failed:",
error
);
}
});

const token = generateToken(user._id);
return res.status(200).json({
_id: user._id,
fullName: user.fullName,
email: user.email,
profilePic: user.profilePic,
role: user.role,
token,
message: "Email verified successfully",
});
} catch (error) {
console.error(
"Verify email error:",
error
);

return res.status(500).json({
message: "Internal Server Error",
});
}
};

//login
// Get email, password from req.body
// Convert email to lowercase
Expand Down Expand Up @@ -161,11 +276,11 @@
});
}

// if (!user.isVerified) {
// return res.status(401).json({
// message: "Please verify your email first",
// });
// }
if (user.isVerified === false && user.isInit("isVerified")) {
return res.status(403).json({
message: "Please verify your email before logging in",
});
}

// checking user is baned from admin side or not
if (user.isBanned) {
Expand Down Expand Up @@ -387,7 +502,7 @@
message:
"Security questions saved",
});
} catch (error) {

Check warning on line 505 in backend/src/controllers/auth.controller.js

View workflow job for this annotation

GitHub Actions / backend-build

'error' is defined but never used
res.status(500).json({
message:
"Internal Server Error",
Expand Down Expand Up @@ -462,7 +577,7 @@
res.status(200).json({
resetToken,
});
} catch (error) {

Check warning on line 580 in backend/src/controllers/auth.controller.js

View workflow job for this annotation

GitHub Actions / backend-build

'error' is defined but never used
res.status(500).json({
message:
"Internal Server Error",
Expand Down Expand Up @@ -520,7 +635,7 @@
message:
"Password reset successful",
});
} catch (error) {

Check warning on line 638 in backend/src/controllers/auth.controller.js

View workflow job for this annotation

GitHub Actions / backend-build

'error' is defined but never used
res.status(500).json({
message:
"Internal Server Error",
Expand Down Expand Up @@ -788,4 +903,75 @@
message: "Internal Server Error",
});
}
};

export const resendVerificationOtp = async (
req,
res
) => {
try {
const { email } = req.body;

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

const normalizedEmail =
email.toLowerCase().trim();

const user = await User.findOne({
email: normalizedEmail,
}).select(
"+emailVerificationOtp +emailVerificationOtpExpiry"
);

if (user && user.isVerified === false) {
const verificationOtp = crypto
.randomInt(100000, 1000000)
.toString();

const hashedVerificationOtp = await bcrypt.hash(
verificationOtp,
10
);

user.emailVerificationOtp =
hashedVerificationOtp;

user.emailVerificationOtpExpiry =
new Date(
Date.now() + 5 * 60 * 1000
);

await user.save();

// Asynchronously send the verification email to avoid blocking the HTTP response.
setImmediate(async () => {
try {
await sendVerificationOtpEmail(
user.email,
verificationOtp
);
} catch (error) {
console.error("Verification email failed:", error);
}
});
}

return res.status(200).json({
message:
"If an account exists and requires verification, a verification OTP has been sent.",
});
} catch (error) {
console.error(
"Resend verification OTP error:",
error
);

return res.status(500).json({
message: "Internal Server Error",
});
}
};
Loading
Loading