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:

- -

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) => (
Contact: nexus@coed.svnit.ac.in | - nexus-svnit.in + nexus-svnit.in
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; } };