diff --git a/client/src/components/Post/CreatePost.jsx b/client/src/components/Post/CreatePost.jsx
index 13fdd9c..db62a6e 100644
--- a/client/src/components/Post/CreatePost.jsx
+++ b/client/src/components/Post/CreatePost.jsx
@@ -218,6 +218,13 @@ const CreatePost = () => {
try {
toast.loading("Creating post...");
const token = localStorage.getItem("token");
+ if (!token) {
+ toast.dismiss();
+ toast.error("You must be logged in to create a post.");
+ navigate("/login?redirect_to=/interview-experiences/create");
+ return;
+ }
+
const tagsArray = formData.tags.split(",").map((tag) => tag.trim());
// Convert compensation values to numbers, empty strings become null
@@ -293,7 +300,13 @@ const CreatePost = () => {
localStorage.removeItem("formData");
} catch (error) {
toast.dismiss();
- toast.error("Failed to create the post.");
+ if (error.response?.status === 401) {
+ toast.error("Session expired! Please log in again.");
+ localStorage.removeItem("token");
+ navigate("/login?redirect_to=/interview-experiences/create");
+ } else {
+ toast.error(error.response?.data?.error || error.response?.data?.message || "Failed to create the post.");
+ }
console.error("Error creating post:", error);
}
};
diff --git a/client/src/components/Post/EditPost.jsx b/client/src/components/Post/EditPost.jsx
index 15f6c48..5d0bd2d 100644
--- a/client/src/components/Post/EditPost.jsx
+++ b/client/src/components/Post/EditPost.jsx
@@ -291,6 +291,12 @@ const EditPost = () => {
try {
toast.loading("Updating post...");
const token = localStorage.getItem("token");
+ if (!token) {
+ toast.dismiss();
+ toast.error("You must be logged in to update a post.");
+ navigate("/login?redirect_to=/interview-experiences");
+ return;
+ }
const tagsArray = formData.tags;
@@ -334,7 +340,13 @@ const EditPost = () => {
navigate("/interview-experiences");
} catch (error) {
toast.dismiss();
- toast.error("Failed to update the post.");
+ if (error.response?.status === 401) {
+ toast.error("Session expired! Please log in again.");
+ localStorage.removeItem("token");
+ navigate("/login?redirect_to=/interview-experiences");
+ } else {
+ toast.error(error.response?.data?.error || error.response?.data?.message || "Failed to update the post.");
+ }
console.error("Error updating post:", error);
}
};
diff --git a/server/.env.example b/server/.env.example
index 60b8d02..8e30959 100644
--- a/server/.env.example
+++ b/server/.env.example
@@ -1,3 +1,21 @@
+# Server Configuration
+PORT=8081
+MONGO_URL=mongodb+srv://:@cluster.mongodb.net/nexus
+SECRET=your_jwt_secret_key_here
+CORS_ORIGIN=http://localhost:5173
+
+# Frontend URL (Used for generating reset password links, verification links, and email button links)
+# Production: https://www.nexus-svnit.in
+# Local dev: http://localhost:5173
+FRONTEND_URL=http://localhost:5173
+
+# Email Service (Gmail SMTP)
+# 1. Use a Google Account with 2-Step Verification enabled
+# 2. Generate a 16-character App Password at: https://myaccount.google.com/apppasswords
+# 3. Enter your full Gmail address and the 16-character app password (without spaces)
+EMAIL_ID=your-email@gmail.com
+EMAIL_PASSWORD=your-16-character-app-password
+
# Permanent QR redirect destination for GET /qr
# Set separately in Vercel for each backend environment:
# - Stage backend (stage branch / Preview) -> test at https://stage.nexus-svnit.in/qr
diff --git a/server/controllers/achievementController.js b/server/controllers/achievementController.js
index 1807550..f644360 100644
--- a/server/controllers/achievementController.js
+++ b/server/controllers/achievementController.js
@@ -77,17 +77,22 @@ const addAchievement = async (req, res) => {
await Achievement.create(achievement);
- const user = await userSchema.findOne({ admissionNumber: req.user.admissionNumber });
- if (!user || !user.instituteEmail) {
- return res.status(400).json({ message: "User email not found" });
+ // Send email notification (non-blocking)
+ try {
+ const user = await userSchema.findOne({ admissionNumber: req.user.admissionNumber });
+ const recipient = user?.instituteEmail || user?.personalEmail;
+ if (recipient) {
+ const emailContent = achievementSubmissionTemplate(user.fullName || 'Member');
+ await sendEmail({
+ to: recipient,
+ ...emailContent,
+ critical: false
+ });
+ }
+ } catch (emailErr) {
+ console.error('Non-blocking error sending achievement notification email:', emailErr);
}
- const emailContent = achievementSubmissionTemplate(user.fullName);
- await sendEmail({
- to: user.instituteEmail,
- ...emailContent
- });
-
return res.status(200).json({
success: true,
message: "Waiting for Review"
diff --git a/server/controllers/answerController.js b/server/controllers/answerController.js
index 2bcd390..88f5012 100644
--- a/server/controllers/answerController.js
+++ b/server/controllers/answerController.js
@@ -26,21 +26,27 @@ const createAnswer = async (req, res) => {
question.answers.push(answer);
await question.save();
- // Send email notification to the person who asked the question
- if (question.askedBy.personalEmail && question.askedBy._id.toString() !== author) {
- const answeredBy = await User.findById(author);
- const emailContent = newAnswerTemplate(
- question.askedBy,
- question.postId.title,
- question.question,
- content,
- answeredBy,
- question.postId._id
- );
- await sendEmail({
- to: question.askedBy.personalEmail,
- ...emailContent
- });
+ // Send email notification to the person who asked the question (non-blocking)
+ try {
+ const recipient = question?.askedBy?.personalEmail || question?.askedBy?.instituteEmail;
+ if (recipient && question.askedBy._id.toString() !== author) {
+ const answeredBy = await User.findById(author);
+ const emailContent = newAnswerTemplate(
+ question.askedBy,
+ question.postId.title,
+ question.question,
+ content,
+ answeredBy,
+ question.postId._id
+ );
+ await sendEmail({
+ to: recipient,
+ ...emailContent,
+ critical: false
+ });
+ }
+ } catch (emailErr) {
+ console.error("Non-blocking error sending answer notification email:", emailErr);
}
// Populate the author details of the new answer
diff --git a/server/controllers/authController.js b/server/controllers/authController.js
index 229e4fa..3b23060 100644
--- a/server/controllers/authController.js
+++ b/server/controllers/authController.js
@@ -1,17 +1,30 @@
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const user = require('../models/userModel.js');
-const bcrypt = require('bcrypt')
+const fs = require('fs');
const { sendEmail } = require('../utils/emailUtils.js');
const { validateCodingProfiles } = require('../utils/validateCodingProfiles.js');
const {
alumniEmailVerificationTemplate,
- alumniEmailVerifiedTemplate,
- signupEmailTemplate,
+ alumniEmailVerifiedTemplate,
+ signupEmailTemplate,
forgotPasswordTemplate
} = require('../utils/emailTemplates.js');
const { validateAlumni } = require('../utils/validateAlumni');
+/**
+ * Extracts frontend origin URL reliably from env or request headers
+ */
+const getFrontendBaseUrl = (req) => {
+ const raw = process.env.FRONTEND_URL || req.headers.origin || req.headers.referer || 'https://www.nexus-svnit.in';
+ try {
+ const parsed = new URL(raw);
+ return parsed.origin;
+ } catch {
+ return raw.replace(/\/+$/, '');
+ }
+};
+
const loginUser = async (req, res) => {
const { admissionNumber, password } = req.body;
@@ -46,7 +59,7 @@ const loginUser = async (req, res) => {
isAlumni: foundUser.isAlumni || false,
};
- const token = jwt.sign(payload, process.env.SECRET, {
+ const token = jwt.sign(payload, process.env.SECRET || 'fallback_secret_key', {
expiresIn: '7d'
});
@@ -63,12 +76,16 @@ const loginUser = async (req, res) => {
} catch (error) {
console.error("Login error:", error);
- res.status(500).json({ message: 'Server error', error });
+ res.status(500).json({ message: 'Server error', error: error.message });
}
};
const signupUser = async (req, res) => {
- const { fullName, admissionNumber, mobileNumber, personalEmail, instituteEmail, branch, linkedInProfile, githubProfile, leetcodeProfile, codeforcesProfile, codechefProfile, password, shareCodingProfile } = req.body;
+ const {
+ fullName, admissionNumber, mobileNumber, personalEmail, instituteEmail,
+ branch, linkedInProfile, githubProfile, leetcodeProfile, codeforcesProfile,
+ codechefProfile, password, shareCodingProfile
+ } = req.body;
try {
// Validate coding profile IDs (URL check + existence verification)
@@ -87,14 +104,14 @@ const signupUser = async (req, res) => {
return res.status(400).json({ message: 'User already exists' });
}
+ // Step 2: Validate institute email match
+ if (!instituteEmail || admissionNumber.toLowerCase() !== instituteEmail.split('@')[0].toLowerCase()) {
+ return res.status(400).json({ message: 'Admission number does not match institute email' });
+ }
+
// Step 3: Generate a verification token
const verificationToken = crypto.randomBytes(32).toString('hex');
- if (admissionNumber.toLowerCase() !== instituteEmail.split('@')[0]) {
- return res.status(400).json({ message: 'Email does not match with email' });
-
- }
-
// Step 4: Create a new user with the token and save to DB
const newUser = new user({
fullName,
@@ -108,47 +125,47 @@ const signupUser = async (req, res) => {
leetcodeProfile,
codeforcesProfile,
codechefProfile,
- shareCodingProfile, // New field for CodeChef profile
+ shareCodingProfile,
password,
verificationToken
});
await newUser.save();
- // Step 5: Send verification email...
// Step 5: Send verification email
+ const baseUrl = getFrontendBaseUrl(req);
+ const verificationUrl = `${baseUrl}/auth/verify/${verificationToken}`;
-
- const verificationUrl = `${req.headers.referer}auth/verify/${verificationToken}`;
-
- const { subject, html } = signupEmailTemplate({
+ const emailContent = signupEmailTemplate({
fullName,
verificationUrl
});
- const mailOptions = {
- from: process.env.EMAIL_ID,
- to: instituteEmail,
- subject,
- html
- };
-
- // CHANGE: await transporter.sendMail(mailOptions);
- await sendEmail(mailOptions);
+ try {
+ await sendEmail({
+ to: instituteEmail,
+ ...emailContent,
+ critical: true
+ });
+ } catch (emailError) {
+ // Roll back user creation so the user isn't stuck with "User already exists" while unverified
+ await user.findByIdAndDelete(newUser._id);
+ console.error('Signup verification email error:', emailError);
+ return res.status(500).json({
+ message: 'Failed to send verification email. Please check your email credentials or try again later.'
+ });
+ }
res.status(201).json({ message: 'User registered. Verification email sent!' });
-
} catch (error) {
- res.status(500).json({ message: 'Server error', error });
+ console.error('Signup error:', error);
+ res.status(500).json({ message: 'Server error', error: error.message });
}
};
const signUpAlumni = async (req, res) => {
- let tempFilePath = req.file?.path;
-
try {
-
const requiredFields = [
'fullName', 'personalEmail', 'admissionNumber',
'currentDesignation', 'currentCompany', 'password',
@@ -204,7 +221,7 @@ const signUpAlumni = async (req, res) => {
});
}
- // Generate email verification token
+ // Generate email verification token
const verificationToken = crypto.randomBytes(32).toString('hex');
// Create alumni user object
@@ -219,23 +236,29 @@ const signUpAlumni = async (req, res) => {
const newAlumni = await user.create(alumniData);
- // Create verification link
- const verificationUrl = `${req.headers.origin || req.headers.referer}/auth/verify/alumni/${verificationToken}`;
+ // Create verification link
+ const baseUrl = getFrontendBaseUrl(req);
+ const verificationUrl = `${baseUrl}/auth/verify/alumni/${verificationToken}`;
- //
- const { subject, html } = alumniEmailVerificationTemplate({
+ const emailContent = alumniEmailVerificationTemplate({
fullName: req.body['fullName'],
verificationUrl,
});
- const mailOptions = {
- from: process.env.EMAIL_ID,
- to: req.body['personalEmail'],
- subject,
- html
- };
-
- await sendEmail(mailOptions);
+ try {
+ await sendEmail({
+ to: req.body['personalEmail'],
+ ...emailContent,
+ critical: true
+ });
+ } catch (emailError) {
+ await user.findByIdAndDelete(newAlumni._id);
+ console.error('Alumni verification email error:', emailError);
+ return res.status(500).json({
+ success: false,
+ message: 'Failed to send verification email. Please try again later.'
+ });
+ }
return res.status(201).json({
success: true,
@@ -281,7 +304,7 @@ const verifyEmail = async (req, res) => {
res.status(200).json({ message: 'Email verified successfully' });
} catch (error) {
- res.status(500).json({ message: 'Server error', error });
+ res.status(500).json({ message: 'Server error', error: error.message });
}
};
@@ -298,23 +321,21 @@ const verifyAlumniEmail = async (req, res) => {
userData.verificationToken = undefined; // Remove the token after verification
await userData.save();
- const { html, subject } = alumniEmailVerifiedTemplate({
-
+ // Non-blocking notification email
+ const emailContent = alumniEmailVerifiedTemplate({
fullName: userData.fullName
- }
- );
- const mailOptions = {
- from: process.env.EMAIL_ID,
- to: userData.personalEmail,
- subject,
- html
- };
+ });
- await sendEmail(mailOptions);
+ await sendEmail({
+ to: userData.personalEmail,
+ ...emailContent,
+ critical: false
+ });
res.status(200).json({ message: 'Email verified successfully' });
} catch (error) {
- res.status(500).json({ message: 'Server error', error });
+ console.error('Verify alumni email error:', error);
+ res.status(500).json({ message: 'Server error', error: error.message });
}
};
@@ -337,36 +358,43 @@ const forgotPassword = async (req, res) => {
await foundUser.save();
// Step 2: Send reset email
- const resetUrl = `${req.headers.referer}auth/reset-password/${resetToken}`;
+ const baseUrl = getFrontendBaseUrl(req);
+ const resetUrl = `${baseUrl}/auth/reset-password/${resetToken}`;
- const { subject, html } = forgotPasswordTemplate({
+ const emailContent = forgotPasswordTemplate({
fullName: foundUser.fullName,
resetUrl
});
-
- const mailOptions = {
- from: process.env.EMAIL_ID,
- to: foundUser.personalEmail,
- subject,
- html
- };
- // CHANGE: await transporter.sendMail(mailOptions);
- await sendEmail(mailOptions);
+ const recipient = foundUser.personalEmail || foundUser.instituteEmail;
+ if (!recipient) {
+ return res.status(400).json({ message: 'No registered email found for this user.' });
+ }
+
+ await sendEmail({
+ to: recipient,
+ ...emailContent,
+ critical: true
+ });
res.status(200).json({ message: 'Password reset email sent' });
} catch (error) {
- console.error(error);
- res.status(500).json({ message: 'Server error', error });
+ console.error('Forgot password error:', error);
+ res.status(500).json({ message: 'Failed to send password reset email', error: error.message });
}
};
const verifyPasswordResetEmail = async (req, res) => {
const { token } = req.params;
- const { newPassword } = req.body;
+ const { newPassword, password } = req.body;
+ const passwordToSet = newPassword || password;
try {
+ if (!passwordToSet) {
+ return res.status(400).json({ message: 'New password is required' });
+ }
+
// Step 1: Find user by token and check if token is still valid
const foundUser = await user.findOne({
resetPasswordToken: token,
@@ -376,9 +404,9 @@ const verifyPasswordResetEmail = async (req, res) => {
return res.status(400).json({ message: 'Invalid or expired verification token' });
}
- // Step 2: Verify the email
+ // Step 2: Set raw password - pre('save') hook handles hashing!
foundUser.emailVerified = true;
- foundUser.password = newPassword;
+ foundUser.password = passwordToSet;
foundUser.resetPasswordToken = undefined;
foundUser.resetPasswordExpires = undefined;
@@ -387,75 +415,108 @@ const verifyPasswordResetEmail = async (req, res) => {
res.status(200).json({ message: 'Password Reset Successfully' });
} catch (error) {
- console.error('Error reseting password:', error);
- res.status(500).json({ message: 'Server error', error });
+ console.error('Error resetting password:', error);
+ res.status(500).json({ message: 'Server error', error: error.message });
}
};
const resetPassword = async (req, res) => {
- const { token } = req.params;
- const { password } = req.body;
+ const token = req.params.token || req.body.token;
+ const { password, newPassword } = req.body;
+ const passwordToSet = password || newPassword;
try {
+ if (!token) {
+ return res.status(400).json({ message: 'Reset token is required' });
+ }
+
+ if (!passwordToSet) {
+ return res.status(400).json({ message: 'New password is required' });
+ }
+
// Step 1: Find user by reset token and check if it's expired
const foundUser = await user.findOne({
resetPasswordToken: token,
- resetPasswordExpires: { $gt: Date.now() } // Check if token is not expired
+ resetPasswordExpires: { $gt: Date.now() }
});
if (!foundUser) {
return res.status(400).json({ message: 'Invalid or expired token' });
}
- // Step 2: Hash the new password and save it
- const hashedPassword = await bcrypt.hash(password, 10);
- foundUser.password = hashedPassword;
-
- // Step 3: Invalidate the reset token and reset expiration time
+ // Step 2: Assign raw password - userSchema pre('save') hook handles hashing!
+ foundUser.password = passwordToSet;
foundUser.resetPasswordToken = undefined;
foundUser.resetPasswordExpires = undefined;
-
- // Step 4: Mark the institute email as unverified
- foundUser.emailVerified = false;
+ foundUser.emailVerified = true;
await foundUser.save();
- // Step 5: Send re-verification email
- const verificationToken = crypto.randomBytes(32).toString('hex');
- foundUser.verificationToken = verificationToken;
+ res.status(200).json({ message: 'Password reset successfully' });
- const verificationUrl = `${req.headers.referer}auth/verify/${verificationToken}`;
- const mailOptions = {
- from: process.env.EMAIL_ID,
- to: foundUser.instituteEmail,
- subject: 'Re-verify your Email',
- html: `
-
-
-
-
-
Dear ${foundUser.fullName},
-
Your password has been successfully reset. Please verify your email again using the link below:
-
- Verify Your Email
-
-
Thanks, Team NEXUS
-
- `
- };
+ } catch (error) {
+ console.error('Reset password error:', error);
+ res.status(500).json({ message: 'Server error', error: error.message });
+ }
+};
+
+const resendVerificationEmail = async (req, res) => {
+ const { admissionNumber, email } = req.body;
+
+ try {
+ if (!admissionNumber && !email) {
+ return res.status(400).json({ message: 'Please provide your admission number or email' });
+ }
+
+ const query = admissionNumber
+ ? { admissionNumber }
+ : { $or: [{ personalEmail: email }, { instituteEmail: email }] };
+
+ const foundUser = await user.findOne(query);
+ if (!foundUser) {
+ return res.status(404).json({ message: 'User not found' });
+ }
- // CHANGE: await transporter.sendMail(mailOptions);
- await sendEmail(mailOptions);
+ if (foundUser.emailVerified) {
+ return res.status(400).json({ message: 'Email is already verified' });
+ }
- res.status(200).json({ message: 'Password reset successfully. Verification email sent.' });
+ const verificationToken = crypto.randomBytes(32).toString('hex');
+ foundUser.verificationToken = verificationToken;
+ await foundUser.save();
+ const baseUrl = getFrontendBaseUrl(req);
+ if (foundUser.isAlumni) {
+ const verificationUrl = `${baseUrl}/auth/verify/alumni/${verificationToken}`;
+ const emailContent = alumniEmailVerificationTemplate({
+ fullName: foundUser.fullName,
+ verificationUrl
+ });
+ await sendEmail({
+ to: foundUser.personalEmail,
+ ...emailContent,
+ critical: true
+ });
+ } else {
+ const verificationUrl = `${baseUrl}/auth/verify/${verificationToken}`;
+ const emailContent = signupEmailTemplate({
+ fullName: foundUser.fullName,
+ verificationUrl
+ });
+ await sendEmail({
+ to: foundUser.instituteEmail || foundUser.personalEmail,
+ ...emailContent,
+ critical: true
+ });
+ }
+
+ res.status(200).json({ message: 'Verification email sent successfully' });
} catch (error) {
- console.error(error);
- res.status(500).json({ message: 'Server error', error });
+ console.error('Resend verification error:', error);
+ res.status(500).json({ message: 'Failed to resend verification email', error: error.message });
}
};
-
module.exports = {
loginUser,
signupUser,
@@ -465,4 +526,5 @@ module.exports = {
forgotPassword,
verifyPasswordResetEmail,
resetPassword,
+ resendVerificationEmail,
};
\ No newline at end of file
diff --git a/server/controllers/commentController.js b/server/controllers/commentController.js
index b177d1d..3309375 100644
--- a/server/controllers/commentController.js
+++ b/server/controllers/commentController.js
@@ -16,19 +16,26 @@ const createComment = async (req, res) => {
post.comments.push(savedComment._id);
await post.save();
- if (post.author.personalEmail) {
- const commentAuthor = await User.findById(author);
- const emailContent = newCommentTemplate(
- post.author,
- post.title,
- content,
- commentAuthor,
- post._id
- );
- await sendEmail({
- to: post.author.personalEmail,
- ...emailContent
- });
+ // Send email notification to post author (non-blocking, skip if commenting on own post)
+ try {
+ const recipient = post?.author?.personalEmail || post?.author?.instituteEmail;
+ if (recipient && post.author._id.toString() !== author) {
+ const commentAuthor = await User.findById(author);
+ const emailContent = newCommentTemplate(
+ post.author,
+ post.title,
+ content,
+ commentAuthor,
+ post._id
+ );
+ await sendEmail({
+ to: recipient,
+ ...emailContent,
+ critical: false
+ });
+ }
+ } catch (emailErr) {
+ console.error("Non-blocking error sending comment email:", emailErr);
}
res.status(201).json(savedComment);
diff --git a/server/controllers/postController.js b/server/controllers/postController.js
index bbf2f59..ef9e0d6 100644
--- a/server/controllers/postController.js
+++ b/server/controllers/postController.js
@@ -69,14 +69,20 @@ const createPost = async (req, res) => {
companyRecord.posts.push(savedPost._id);
await companyRecord.save();
- // Send email notification to author
- const author = await User.findById(req.user.id);
- if (author.personalEmail) {
- const emailContent = postCreationTemplate(author, title, savedPost._id);
- await sendEmail({
- to: author.personalEmail,
- ...emailContent
- });
+ // Send email notification to author (non-blocking)
+ try {
+ const author = await User.findById(req.user.id);
+ const recipient = author?.personalEmail || author?.instituteEmail;
+ if (recipient) {
+ const emailContent = postCreationTemplate(author, title, savedPost._id);
+ await sendEmail({
+ to: recipient,
+ ...emailContent,
+ critical: false
+ });
+ }
+ } catch (emailErr) {
+ console.error("Non-blocking error sending post creation email:", emailErr);
}
res.status(201).json(savedPost);
@@ -231,14 +237,20 @@ const updatePost = async (req, res) => {
const updatedPost = await Post.findByIdAndUpdate(postId, updatedData, { new: true });
- // Send email notification about post update
- const author = await User.findById(req.user.id);
- if (author.personalEmail) {
- const emailContent = postEditTemplate(author, updatedPost.title, updatedPost._id);
- await sendEmail({
- to: author.personalEmail,
- ...emailContent
- });
+ // Send email notification about post update (non-blocking)
+ try {
+ const author = await User.findById(req.user.id);
+ const recipient = author?.personalEmail || author?.instituteEmail;
+ if (recipient) {
+ const emailContent = postEditTemplate(author, updatedPost.title, updatedPost._id);
+ await sendEmail({
+ to: recipient,
+ ...emailContent,
+ critical: false
+ });
+ }
+ } catch (emailErr) {
+ console.error("Non-blocking error sending post update email:", emailErr);
}
res.status(200).json(updatedPost);
diff --git a/server/controllers/questionController.js b/server/controllers/questionController.js
index c12a013..30d55d1 100644
--- a/server/controllers/questionController.js
+++ b/server/controllers/questionController.js
@@ -17,20 +17,26 @@ const createQuestion = async (req, res) => {
post.questions.push(savedQuestion._id);
await post.save();
- // Send email notification to post author
- if (post.author.personalEmail) {
- const askedByUser = await User.findById(author);
- const emailContent = newQuestionTemplate(
- post.author,
- post.title,
- question,
- askedByUser,
- post._id
- );
- await sendEmail({
- to: post.author.personalEmail,
- ...emailContent
- });
+ // Send email notification to post author (non-blocking, skip if asking on own post)
+ try {
+ const recipient = post?.author?.personalEmail || post?.author?.instituteEmail;
+ if (recipient && post.author._id.toString() !== author) {
+ const askedByUser = await User.findById(author);
+ const emailContent = newQuestionTemplate(
+ post.author,
+ post.title,
+ question,
+ askedByUser,
+ post._id
+ );
+ await sendEmail({
+ to: recipient,
+ ...emailContent,
+ critical: false
+ });
+ }
+ } catch (emailErr) {
+ console.error("Non-blocking error sending question notification email:", emailErr);
}
res.status(201).json(savedQuestion);
diff --git a/server/routes/authRoutes.js b/server/routes/authRoutes.js
index e1f4c55..2bc693b 100644
--- a/server/routes/authRoutes.js
+++ b/server/routes/authRoutes.js
@@ -1,5 +1,5 @@
-const express = require('express')
-const router = express.Router()
+const express = require('express');
+const router = express.Router();
const {
loginUser,
signupUser,
@@ -8,22 +8,23 @@ const {
resetPassword,
verifyAlumniEmail,
verifyEmail,
- verifyPasswordResetEmail
-} = require('../controllers/authController.js')
+ verifyPasswordResetEmail,
+ resendVerificationEmail
+} = require('../controllers/authController.js');
// Auth
router.post('/login', loginUser);
router.post('/signup', signupUser);
router.get('/verify/:token', verifyEmail);
+router.post('/resend-verification', resendVerificationEmail);
// Alumni
-router.post('/alumni/signup', signUpAlumni)
-router.get('/alumni/verify/:token', verifyAlumniEmail)
+router.post('/alumni/signup', signUpAlumni);
+router.get('/alumni/verify/:token', verifyAlumniEmail);
// Password flows
router.post('/forgot-password', forgotPassword);
router.post('/reset-password/:token', verifyPasswordResetEmail);
-router.put('/reset-password', resetPassword);
+router.put('/reset-password/:token?', resetPassword);
-
-module.exports = router
+module.exports = router;
diff --git a/server/utils/emailTemplates.js b/server/utils/emailTemplates.js
index 590ee80..8a2e080 100644
--- a/server/utils/emailTemplates.js
+++ b/server/utils/emailTemplates.js
@@ -1,4 +1,4 @@
-const link = 'https://www.nexus-svnit.in';
+const getLink = () => (process.env.FRONTEND_URL || 'https://www.nexus-svnit.in').replace(/\/+$/, '');
const LINK_COLOR = '#4fc3f7';
const emailWrapperTemplate = (content) => (
@@ -47,7 +47,7 @@ const emailWrapperTemplate = (content) => (
Follow us:
@@ -75,7 +75,7 @@ const postCreationTemplate = (author, postTitle, id) => ({
You can check your submission status using the button below:
- View Your Post
+ View Your Post
Best regards, Team NEXUS
`)
@@ -94,7 +94,7 @@ const newQuestionTemplate = (author, postTitle, question, askedBy, id) => ({
Login to your account to answer this question.
- View Post & Answer
+ View Post & Answer
Thanks, Team NEXUS
`)
@@ -112,7 +112,7 @@ const newCommentTemplate = (author, postTitle, comment, commentedBy, id) => ({
${comment}
- View Post
+ View Post
Thanks, Team NEXUS
`)
@@ -134,7 +134,7 @@ const newAnswerTemplate = (author, postTitle, question, answer, answeredBy, id)
${answer}
- View Full Discussion
+ View Full Discussion
Thanks, Team NEXUS
`)
@@ -152,7 +152,7 @@ const postEditTemplate = (user, postTitle, postId) => ({
Your post will be visible to others after verification by our team.
- View Post
+ View Post
Thanks, Team NEXUS
`)
@@ -244,7 +244,7 @@ const achievementSubmissionTemplate = (name) => ({
We appreciate your contributions and look forward to sharing your success with the community!
- View Achievement Bulletin
+ View Achievement Bulletin
Thanks, Team NEXUS
`)
diff --git a/server/utils/emailUtils.js b/server/utils/emailUtils.js
index 1e1b262..138e28d 100644
--- a/server/utils/emailUtils.js
+++ b/server/utils/emailUtils.js
@@ -1,77 +1,90 @@
const nodemailer = require('nodemailer');
/**
- * Create email transporter with better error handling for Gmail authentication
- *
- * For Gmail accounts:
- * 1. You need to use an App Password if 2FA is enabled (recommended)
- * - Go to your Google Account > Security > App passwords
- * - Create a new app password for "Mail" and "Other (Custom name)"
- * - Use this password in your .env file
- *
- * 2. OR enable "Less secure app access" (not recommended)
- * - This option is being deprecated by Google
- *
- * 3. OR set up OAuth2 authentication (most secure but requires more setup)
+ * Create nodemailer transporter optimized for serverless environments (Vercel)
+ * - Uses direct SSL on port 465 (reliable on cloud networks)
+ * - Automatically strips spaces from App Password if copied with spaces
+ * - Explicit timeouts to prevent serverless function hangs
*/
const createTransporter = () => {
- // For regular SMTP with app password
+ const user = process.env.EMAIL_ID?.trim();
+ const pass = process.env.EMAIL_PASSWORD?.replace(/\s+/g, '');
+
return nodemailer.createTransport({
- service: 'gmail',
+ host: 'smtp.gmail.com',
+ port: 465,
+ secure: true, // Direct SSL
auth: {
- user: process.env.EMAIL_ID,
- pass: process.env.EMAIL_PASSWORD
+ user,
+ pass
},
- // Increase debugging when NODE_ENV is development
- ...(process.env.NODE_ENV === 'development' && { debug: true })
+ connectionTimeout: 10000, // 10 seconds
+ greetingTimeout: 5000,
+ socketTimeout: 10000
});
};
-// Function to send email with better error handling
-const sendEmail = async ({ to, subject, text, html, attachments }) => {
+/**
+ * Send an email with robust error handling and optional critical error propagation.
+ *
+ * @param {Object} options
+ * @param {string} options.to - Recipient email
+ * @param {string} options.subject - Email subject
+ * @param {string} [options.text] - Plaintext body
+ * @param {string} [options.html] - HTML body
+ * @param {Array} [options.attachments] - Email attachments
+ * @param {boolean} [options.critical=false] - When true, throws on failure. When false, logs and returns null safely.
+ */
+const sendEmail = async ({ to, subject, text, html, attachments, critical = false }) => {
+ const emailId = process.env.EMAIL_ID?.trim();
+ const emailPassword = process.env.EMAIL_PASSWORD?.replace(/\s+/g, '');
+
+ if (!emailId || !emailPassword) {
+ const warning = '[EmailService] Missing EMAIL_ID or EMAIL_PASSWORD environment variables. Email was not sent.';
+ console.warn(warning);
+ if (critical) {
+ throw new Error(warning);
+ }
+ return null;
+ }
+
+ if (!to) {
+ const warning = '[EmailService] Recipient email ("to") is undefined or empty. Email was not sent.';
+ console.warn(warning);
+ if (critical) {
+ throw new Error(warning);
+ }
+ return null;
+ }
+
try {
const transporter = createTransporter();
-
+
const mailOptions = {
- from: `"Team Nexus" <${process.env.EMAIL_ID}>`,
- to,
- subject,
+ from: `"Team Nexus" <${emailId}>`,
+ to,
+ subject,
text,
html,
attachments
};
- // Verify SMTP connection configuration
- await new Promise((resolve, reject) => {
- // Only verify in development mode to avoid unnecessary connection attempts
- if (process.env.NODE_ENV === 'development') {
- transporter.verify(function (error, success) {
- if (error) {
- reject(error);
- } else {
- resolve(success);
- }
- });
- } else {
- resolve(true);
- }
- });
-
- // Send the email
const info = await transporter.sendMail(mailOptions);
return info;
-
} catch (error) {
- // Provide more helpful error messages based on common issues
+ console.error(`[EmailService] Failed to send email to "${to}":`, error.message);
+
if (error.code === 'EAUTH') {
- console.error('EMAIL AUTHENTICATION ERROR: Check your email/password or create an app-specific password');
- console.error('Visit https://support.google.com/accounts/answer/185833 for more info');
+ console.error('[EmailService] AUTHENTICATION ERROR: Invalid Gmail credentials or expired App Password.');
+ console.error('[EmailService] Check https://support.google.com/accounts/answer/185833');
+ } else if (error.code === 'ESOCKET' || error.code === 'ETIMEDOUT') {
+ console.error('[EmailService] CONNECTION ERROR: Could not connect to Gmail SMTP server.');
}
- if (error.code === 'ESOCKET') {
- console.error('EMAIL CONNECTION ERROR: Could not connect to email server');
+
+ if (critical) {
+ throw error;
}
-
- throw error;
+ return null;
}
};