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
2 changes: 2 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ GOOGLE_CLIENT_ID=your_google_client_id
VAPID_PUBLIC_KEY=your_vapid_public_key
VAPID_PRIVATE_KEY=your_vapid_private_key
VAPID_SUBJECT=mailto:your@email.com
EMAIL=your_email@gmail.com
EMAIL_PASSWORD=your_google_email_password
10 changes: 10 additions & 0 deletions backend/package-lock.json

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

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.3",
"mongoose": "^9.6.1",
"nodemailer": "^8.0.11",
"socket.io": "^4.8.3",
"web-push": "^3.6.7",
"xss": "^1.0.15"
Expand Down
94 changes: 93 additions & 1 deletion backend/src/controllers/auth.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { OAuth2Client } from "google-auth-library";
import User from "../models/user.model.js";
import { generateTokenAndSetCookie, catchAsync } from "../lib/utils.js";
import cloudinary from "../lib/cloudinary.js";
import transporter from "../utils/nodemailer.js"

let googleClient;

Expand Down Expand Up @@ -247,4 +248,95 @@ export const subscribeToPush = catchAsync(async (req, res) => {
}).select("-password -__v");

res.status(200).json({ message: "Push subscription saved" });
});
} catch (err) {
console.error("subscribeToPush:", err.message);
res.status(500).json({ message: "Could not save push subscription" });
}
}

export const emailVerification = async (req, res) => {
try {
const user = await User.findById(req.userId);

if (!user) {
return res.status(404).json({
message: "User not found",
});
}

if (user.isVerified) {
return res.status(400).json({
message: "Email already verified",
});
}

const otp = Math.floor(
100000 + Math.random() * 900000
).toString();
Comment on lines +301 to +303

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

According to official Node.js documentation, is Math.random() cryptographically secure for OTP generation? What API is recommended instead for secure random integer generation?

💡 Result:

According to official documentation and industry standards, Math.random is not cryptographically secure and must not be used for security-sensitive operations such as OTP generation [1][2]. It is designed as a pseudo-random number generator that is not intended for cryptographic purposes [1]. For secure random integer generation in Node.js, the node:crypto module provides the recommended API, specifically crypto.randomInt(min, max) [3][4]. This function generates a cryptographically secure random integer [3][4]. Alternatively, for broader cryptographic requirements, crypto.getRandomValues() (part of the Web Crypto API) is also available and recommended for generating cryptographically strong random values [5][6][7].

Citations:


Replace Math.random() OTP generation with crypto.randomInt()
Math.random() is not cryptographically secure and must not be used for security-sensitive OTPs. Use node:crypto (e.g., crypto.randomInt(100000, 1000000)) to generate the OTP.

File: backend/src/controllers/auth.controller.js (lines 301-303)

const otp = Math.floor(
  100000 + Math.random() * 900000
).toString();
🤖 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 301 - 303, Replace
the insecure Math.random-based OTP with a cryptographically secure randomInt
call: import or require node:crypto (e.g., import { randomInt } from 'crypto' or
const { randomInt } = require('crypto')), then change the otp assignment (the
otp variable in auth.controller.js) to const otp = randomInt(100000,
1000000).toString(); so the OTP remains a 6-digit string generated securely.


user.otp = otp;
user.otpExpires = Date.now() + 5 * 60 * 1000;

await user.save();

await transporter.sendMail({
from: process.env.EMAIL,
to: user.email,
subject: "Email Verification OTP",
html: `<h2>Email Verification</h2>
<p>Your OTP is: <b>${otp}</b></p>`,
});

res.status(200).json({
message: "OTP sent successfully",
});

} catch (error) {
console.log(error);
res.status(500).json({
message: "Failed to send OTP",
});
}
};

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

const user = await User.findById(req.userId);

if (!user) {
return res.status(404).json({
message: "User not found",
});
}

if (user.otp !== otp) {
return res.status(400).json({
message: "Invalid OTP",
});
}

if (user.otpExpires < Date.now()) {
return res.status(400).json({
message: "OTP expired",
});
}
Comment on lines +330 to +352

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

verifyOtp can mark accounts verified without a valid OTP.

Current checks allow a bypass when OTP is missing/unset (e.g., omitted body OTP + unset stored OTP/expiry). Add strict validation for input format and stored OTP state before any compare.

Proposed fix
 export const verifyOtp = async (req, res) => {
   try {
-    const { otp } = req.body;
+    const otp = String(req.body?.otp ?? "").trim();
+    if (!/^\d{6}$/.test(otp)) {
+      return res.status(400).json({ message: "Please provide a valid 6-digit OTP" });
+    }

     const user = await User.findById(req.userId);

     if (!user) {
       return res.status(404).json({
         message: "User not found",
       });
     }
+
+    if (!user.otp || !user.otpExpires) {
+      return res.status(400).json({ message: "No active OTP. Please request a new OTP." });
+    }
+
+    if (user.otpExpires < Date.now()) {
+      return res.status(400).json({ message: "OTP expired" });
+    }

     if (user.otp !== otp) {
       return res.status(400).json({
         message: "Invalid OTP",
       });
     }
-
-    if (user.otpExpires < Date.now()) {
-      return res.status(400).json({
-        message: "OTP expired",
-      });
-    }
🤖 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 330 - 352, In
verifyOtp, add strict validation to prevent marking accounts verified when OTP
is missing: first validate req.body.otp exists and is the expected type/format
(e.g., non-empty string or numeric string) and return 400 if not; then ensure
the stored fields user.otp and user.otpExpires are present and that
user.otpExpires is a future timestamp before doing any comparison; if stored OTP
or expiry is missing or expired return 400; finally perform a strict comparison
between the sanitized input OTP and user.otp and only proceed to mark verified
when both checks pass. Target the verifyOtp function and the user.otp /
user.otpExpires checks for these changes.


user.isVerified = true;
user.otp = null;
user.otpExpires = null;

await user.save();

res.status(200).json({
message: "Email verified successfully",
});

} catch (error) {
res.status(500).json({
message: "Verification failed",
});
}
};
});
21 changes: 6 additions & 15 deletions backend/src/models/user.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,16 @@ const userSchema = new mongoose.Schema({
type: mongoose.Schema.Types.Mixed,
default: null,
},
isVerified: {
type: Boolean,
default: false
},
lastSeen: {
type: Date,
default: Date.now,
},
statusMood: {
type: String,
enum: [
"coding",
"coffee_break",
"studying",
"gaming",
"working",
"sleeping",
"music",
"away",
],
default: null,
trim: true,
},
otp: String,
otpExpires: Date
Comment on lines +44 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

OTP fields are modeled as normal readable fields, which leaks verification secrets.

Because these fields are selectable by default, responses like checkAuth can expose the OTP/expiry to the client, bypassing the email-verification trust model. Mark OTP fields as non-selectable and store only a derived value (hash), not raw OTP text.

Suggested direction
-    otp: String,
-    otpExpires: Date
+    otpHash: {
+        type: String,
+        default: null,
+        select: false
+    },
+    otpExpires: {
+        type: Date,
+        default: null,
+        select: false
+    }
🤖 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/models/user.model.js` around lines 44 - 45, The OTP fields otp
and otpExpires in the user schema are currently selectable and store raw OTPs;
change the schema to remove raw otp and instead store a non-selectable derived
value (e.g., otpHash: String with select: false) and keep otpExpires as select:
false (or rename to otpExpires and set select: false) so these values are not
returned by default; update any code that sets/checks OTPs (e.g., functions that
create OTPs and verify them) to hash the generated OTP into otpHash when saving
and to compare hashes when verifying, rather than storing or comparing the raw
otp field.

}, { timestamps: true });

userSchema.index({ name: "text" });
Expand Down
4 changes: 3 additions & 1 deletion backend/src/routes/auth.route.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import express from "express";
import protectRoute from "../middleware/auth.middleware.js";
import { validateSignup, validateLogin } from "../middleware/validate.js";
import { signup, login, logout, googleAuth, updateProfile, updateProfilePicture, checkAuth, subscribeToPush } from "../controllers/auth.controller.js";
import { signup, login, logout, googleAuth, updateProfile, updateProfilePicture, checkAuth, subscribeToPush, emailVerification, verifyOtp } from "../controllers/auth.controller.js";

const router = express.Router();

Expand All @@ -13,5 +13,7 @@ router.put("/update-profile", protectRoute, updateProfile);
router.put("/update-profile-picture", protectRoute, updateProfilePicture);
router.get("/check", protectRoute, checkAuth);
router.post("/push-subscribe", protectRoute, subscribeToPush);
router.post("/send-verification-email-otp",protectRoute, emailVerification);
router.post("/Verified-account",protectRoute, verifyOtp);

export default router;
11 changes: 11 additions & 0 deletions backend/src/utils/nodemailer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import nodemailer from 'nodemailer'

const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL,
pass: process.env.EMAIL_PASSWORD
}
});

export default transporter
27 changes: 11 additions & 16 deletions frontend/pages/ProfilePage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"
import { Camera, Pencil } from "lucide-react"
import toast from "react-hot-toast"
import useAuthStore from "../src/store/useAuthStore"
import StatusMoodSelector from "../components/StatusMoodSelector"
import { useNavigate } from "react-router-dom";

export default function ProfilePage() {
const { authUser: user, updateProfile, updateProfilePicture, isLoading } = useAuthStore()
Expand All @@ -12,21 +12,8 @@ export default function ProfilePage() {
const [previewImage, setPreviewImage] = useState(user?.profilePicture || null)
const [selectedFile, setSelectedFile] = useState(null)
const [isEditing, setIsEditing] = useState(false)
const [selectedMood, setSelectedMood] = useState(user?.statusMood || null)

useEffect(() => {
setSelectedMood(user?.statusMood || null)
}, [user?.statusMood])

const handleMoodChange = async (mood) => {
const previousMood = selectedMood
setSelectedMood(mood)
try {
await updateStatusMood(mood)
} catch {
setSelectedMood(previousMood)
}
}
const [isEmailOtp, SetIsEmailOtp] = useState(false);
const navigate = useNavigate();

const handleFileChange = (e) => {
const file = e.target.files[0]
Expand Down Expand Up @@ -121,6 +108,14 @@ export default function ProfilePage() {
Edit
</button>
) : null}
{!user?.isVerified && (
<button
onClick={() => navigate("/send-verify-email-otp")}
className="btn btn-primary"
>
Verify Email
</button>
)}
</div>
</div>

Expand Down
47 changes: 47 additions & 0 deletions frontend/pages/SendVerifyEmailOtp.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useState } from "react";
import axios from "axios";
import toast from "react-hot-toast";
import { useNavigate } from "react-router-dom";

function SendVerifyEmailOtp() {
const [loading, setLoading] = useState(false);
const navigate = useNavigate();

const handleSendOtp = async () => {
try {
setLoading(true);
const res = await axios.post(
"http://localhost:5001/api/auth/send-verification-email-otp",
{},
{ withCredentials: true }
);
Comment on lines +13 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Hardcoded localhost API origin will break non-local deployments.

Line 14 pins requests to http://localhost:5001, which fails outside local dev and bypasses deploy-time API configuration.

🔧 Suggested fix
+const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "";
+
 const res = await axios.post(
-  "http://localhost:5001/api/auth/send-verification-email-otp",
+  `${API_BASE}/api/auth/send-verification-email-otp`,
   {},
   { withCredentials: true }
 );

Apply the same pattern to frontend/pages/VerifyEmail.jsx endpoints.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const res = await axios.post(
"http://localhost:5001/api/auth/send-verification-email-otp",
{},
{ withCredentials: true }
);
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "";
const res = await axios.post(
`${API_BASE}/api/auth/send-verification-email-otp`,
{},
{ withCredentials: true }
);
🤖 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 `@frontend/pages/SendVerifyEmailOtp.jsx` around lines 13 - 17, The axios.post
in SendVerifyEmailOtp.jsx currently hardcodes "http://localhost:5001" which will
break non-local deployments; update the request to use a deployable API base
(e.g. a NEXT_PUBLIC_API_BASE or runtime-config/env var or a relative path)
instead of the literal origin, keep the same path
"/api/auth/send-verification-email-otp" and existing options ({ withCredentials:
true }), and mirror the same pattern used in VerifyEmail.jsx so both pages use
the shared API base configuration rather than localhost.


toast.success(res.data.message);

navigate("/verify-email");
} catch (error) {
toast.error(
error.response?.data?.message ||
"Failed to send OTP"
);
} finally {
setLoading(false);
}

};

return ( <div className="min-h-screen flex items-center justify-center"> <div className="bg-base-100 shadow-xl p-8 rounded-xl w-full max-w-md"> <h1 className="text-3xl font-bold text-center mb-6">
Verify Your Email </h1>
<button
onClick={handleSendOtp}
disabled={loading}
className="btn btn-primary w-full"
>
{loading ? "Sending..." : "Send OTP"}
</button>
</div>
</div>
);
}

export default SendVerifyEmailOtp;
Loading
Loading