From 042d25e057cfbc3ace373de8584a5c0e8e7ed928 Mon Sep 17 00:00:00 2001 From: yash-pouranik Date: Wed, 4 Mar 2026 15:27:15 +0530 Subject: [PATCH 1/4] feat: harden developer auth (JWT expiry, OTP hashing, brute-force protection, password reset) --- backend/Dockerfile | 9 +- backend/app.js | 7 +- backend/controllers/auth.controller.js | 145 ++++++++++++++++++++----- backend/middleware/auth_limiter.js | 13 +++ backend/models/otp.js | 6 +- backend/routes/auth.js | 13 ++- backend/utils/input.validation.js | 7 ++ docker-compose.yml | 4 - 8 files changed, 162 insertions(+), 42 deletions(-) create mode 100644 backend/middleware/auth_limiter.js diff --git a/backend/Dockerfile b/backend/Dockerfile index 0a502eff9..87cb41809 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,15 +1,18 @@ -# ── Stage: Production ───────────────────────────────────────────────────────── +# Stage: Production FROM node:20-alpine # Set working directory WORKDIR /app # Install dependencies first (layer caching) -COPY package*.json ./ +COPY --chown=node:node package*.json ./ RUN npm ci --omit=dev # Copy source -COPY . . +COPY --chown=node:node . . + +# Switch to non-root user (security best practice) +USER node # Expose backend port EXPOSE 1234 diff --git a/backend/app.js b/backend/app.js index 5a2bfdbe2..a283c75eb 100644 --- a/backend/app.js +++ b/backend/app.js @@ -28,6 +28,9 @@ const dashboardLimiter = rateLimit({ skip: (req) => process.env.NODE_ENV === 'development', }); +// FIX 3: Strict limiter for auth endpoints (login / register) +const { authLimiter } = require('./middleware/auth_limiter'); + const adminWhitelist = ['https://urbackend.bitbros.in']; @@ -74,7 +77,9 @@ const storageRoute = require('./routes/storage'); const schemaRoute = require('./routes/schemas'); // ROUTES SETUP -app.use('/api/auth', dashboardLimiter, authRoute); // Developer Auth +app.use('/api/auth', dashboardLimiter, authRoute); // Developer Auth (general) +app.use('/api/auth/login', authLimiter); // Strict limiter on login +app.use('/api/auth/register', authLimiter); // Strict limiter on register app.use('/api/projects', dashboardLimiter, projectRoute); // Project Mgmt app.use('/api/userAuth', limiter, logger, userAuthRoute); app.use('/api/data', limiter, cors(adminCorsOptions), logger, dataRoute); diff --git a/backend/controllers/auth.controller.js b/backend/controllers/auth.controller.js index 30a57ad47..c4e3d2758 100644 --- a/backend/controllers/auth.controller.js +++ b/backend/controllers/auth.controller.js @@ -9,9 +9,12 @@ const { loginSchema, changePasswordSchema, deleteAccountSchema, - onlyEmailSchema + onlyEmailSchema, + resetPasswordSchema } = require("../utils/input.validation"); +const JWT_EXPIRES_IN = '7d'; +const OTP_MAX_ATTEMPTS = 5; module.exports.register = async (req, res) => { @@ -47,19 +50,20 @@ module.exports.login = async (req, res) => { const validPass = await bcrypt.compare(password, dev.password); if (!validPass) return res.status(400).json({ error: "Invalid password" }); - const token = jwt.sign({ _id: dev._id, isVerified: dev.isVerified }, process.env.JWT_SECRET); + // FIX 1: JWT now expires in 7 days + const token = jwt.sign( + { _id: dev._id, isVerified: dev.isVerified }, + process.env.JWT_SECRET, + { expiresIn: JWT_EXPIRES_IN } + ); res.json({ token }); } catch (err) { - console.log("LOGIN ERROR CAUGHT:", err.message); - console.log("Is ZodError?", err instanceof z.ZodError); - if (err instanceof z.ZodError) { return res.status(400).json({ error: "Validation Failed", details: err.issues }); } - console.error("Server Error:", err); res.status(500).json({ error: "Internal Server Error" }); } @@ -114,24 +118,24 @@ module.exports.deleteAccount = async (req, res) => { module.exports.sendOtp = async (req, res) => { try { const { email } = onlyEmailSchema.parse(req.body); - const otp = Math.floor(100000 + Math.random() * 900000); - // Secure: OTP logging removed + const otp = Math.floor(100000 + Math.random() * 900000).toString(); + const existingUser = await Developer.findOne({ email }); if (!existingUser) return res.status(400).json({ error: "User not found" }); if (existingUser.isVerified) return res.status(400).json({ error: "User already verified" }); - const existingOtp = await otpSchema.findOne({ userId: existingUser._id }); - if (existingOtp) { - await existingOtp.deleteOne(); - } - const newOtp = new otpSchema( - { - userId: existingUser._id, - otp - }); - newOtp.save(); - await sendOtp(email, otp); + // Delete any existing OTP for this user + await otpSchema.deleteOne({ userId: existingUser._id }); + + // FIX 2: Hash OTP before storing + const salt = await bcrypt.genSalt(10); + const hashedOtp = await bcrypt.hash(otp, salt); + + const newOtp = new otpSchema({ userId: existingUser._id, otp: hashedOtp }); + await newOtp.save(); + + await sendOtp(email, otp); // Send raw OTP to user's email res.json({ message: "OTP sent successfully" }); } catch (err) { console.error(err); @@ -140,30 +144,115 @@ module.exports.sendOtp = async (req, res) => { } - module.exports.verifyOtp = async (req, res) => { try { const { email } = onlyEmailSchema.parse(req.body); const { otp } = req.body; - const existingUser = await Developer.findOne({ email }).sort({ createdAt: -1 }); + + const existingUser = await Developer.findOne({ email }); if (!existingUser) return res.status(400).json({ error: "User not found" }); - const existingOtp = await otpSchema.findOne({ userId: existingUser._id }); - if (!existingOtp) return res.status(400).json({ error: "You havn't requested an OTP" }); + const otpDoc = await otpSchema.findOne({ userId: existingUser._id }); + if (!otpDoc) return res.status(400).json({ error: "No OTP found. Please request a new one." }); - // Secure: OTP verification logging removed - if (existingOtp.otp !== otp) return res.status(400).json({ error: "Incorrect OTP" }); + // FIX 4: OTP attempt tracking + if (otpDoc.attempts >= OTP_MAX_ATTEMPTS) { + await otpDoc.deleteOne(); + return res.status(429).json({ error: "Too many incorrect attempts. Please request a new OTP." }); + } - await existingOtp.deleteOne(); + // FIX 2: Compare against bcrypt hash + const isMatch = await bcrypt.compare(otp.toString(), otpDoc.otp); + if (!isMatch) { + otpDoc.attempts += 1; + await otpDoc.save(); + const remaining = OTP_MAX_ATTEMPTS - otpDoc.attempts; + return res.status(400).json({ error: `Incorrect OTP. ${remaining} attempt(s) remaining.` }); + } + + await otpDoc.deleteOne(); existingUser.isVerified = true; await existingUser.save(); - // Generate new token with isVerified: true - const token = jwt.sign({ _id: existingUser._id, isVerified: existingUser.isVerified }, process.env.JWT_SECRET); + // FIX 1: JWT with expiry + const token = jwt.sign( + { _id: existingUser._id, isVerified: true }, + process.env.JWT_SECRET, + { expiresIn: JWT_EXPIRES_IN } + ); res.status(200).json({ message: "OTP verified successfully", token }); } catch (err) { console.error(err); res.status(500).json({ error: "Internal Server Error" }); } +} + + +// FIX 5: Forgot Password — generate + send reset OTP +module.exports.forgotPassword = async (req, res) => { + try { + const { email } = onlyEmailSchema.parse(req.body); + + const dev = await Developer.findOne({ email }); + // Return same message regardless of whether email exists (prevents user enumeration) + if (!dev) return res.status(200).json({ message: "If this email is registered, an OTP has been sent." }); + + const otp = Math.floor(100000 + Math.random() * 900000).toString(); + + // Delete any existing OTP + await otpSchema.deleteOne({ userId: dev._id }); + + // Hash OTP before storing + const salt = await bcrypt.genSalt(10); + const hashedOtp = await bcrypt.hash(otp, salt); + + await new otpSchema({ userId: dev._id, otp: hashedOtp }).save(); + + await sendOtp(email, otp, { subject: "Password Reset OTP — urBackend" }); + res.status(200).json({ message: "If this email is registered, an OTP has been sent." }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Internal Server Error" }); + } +} + + +// FIX 5: Reset Password — verify OTP then set new password +module.exports.resetPassword = async (req, res) => { + try { + const { email, otp, newPassword } = resetPasswordSchema.parse(req.body); + + const dev = await Developer.findOne({ email }); + if (!dev) return res.status(400).json({ error: "User not found" }); + + const otpDoc = await otpSchema.findOne({ userId: dev._id }); + if (!otpDoc) return res.status(400).json({ error: "No OTP found. Please request a new one." }); + + // Attempt tracking + if (otpDoc.attempts >= OTP_MAX_ATTEMPTS) { + await otpDoc.deleteOne(); + return res.status(429).json({ error: "Too many incorrect attempts. Please request a new OTP." }); + } + + const isMatch = await bcrypt.compare(otp.toString(), otpDoc.otp); + if (!isMatch) { + otpDoc.attempts += 1; + await otpDoc.save(); + const remaining = OTP_MAX_ATTEMPTS - otpDoc.attempts; + return res.status(400).json({ error: `Incorrect OTP. ${remaining} attempt(s) remaining.` }); + } + + // OTP matched — update password + await otpDoc.deleteOne(); + const salt = await bcrypt.genSalt(10); + dev.password = await bcrypt.hash(newPassword, salt); + await dev.save(); + + res.status(200).json({ message: "Password reset successfully. Please log in with your new password." }); + } catch (err) { + if (err instanceof z.ZodError) return res.status(400).json({ error: err.errors }); + console.error(err); + res.status(500).json({ error: "Internal Server Error" }); + } } \ No newline at end of file diff --git a/backend/middleware/auth_limiter.js b/backend/middleware/auth_limiter.js new file mode 100644 index 000000000..540e06732 --- /dev/null +++ b/backend/middleware/auth_limiter.js @@ -0,0 +1,13 @@ +const rateLimit = require('express-rate-limit'); + +// Strict limiter for sensitive auth endpoints (login, register) +const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 10, + message: { error: "Too many attempts. Please try again in 15 minutes." }, + skip: (req) => process.env.NODE_ENV === 'development', + standardHeaders: true, + legacyHeaders: false, +}); + +module.exports = { authLimiter }; diff --git a/backend/models/otp.js b/backend/models/otp.js index ebc318bd1..5d8efd9de 100644 --- a/backend/models/otp.js +++ b/backend/models/otp.js @@ -1,9 +1,11 @@ const mongoose = require('mongoose'); const otpSchema = new mongoose.Schema({ - userId: { type: mongoose.Schema.Types.ObjectId, required: true, ref: 'User' }, - otp: { type: String, required: true }, + userId: { type: mongoose.Schema.Types.ObjectId, required: true, ref: 'Developer' }, + otp: { type: String, required: true }, // bcrypt hash (FIX 2) + attempts: { type: Number, default: 0 }, // brute-force guard (FIX 4) createdAt: { type: Date, default: Date.now, expires: '10m' } // MongoDB TTL index }); module.exports = mongoose.model('Otp', otpSchema); + diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 965d50deb..a66063e8a 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -7,7 +7,9 @@ const { changePassword, deleteAccount, sendOtp, - verifyOtp + verifyOtp, + forgotPassword, + resetPassword } = require('../controllers/auth.controller'); @@ -23,9 +25,12 @@ router.put('/change-password', authorization, changePassword); // DELETE ACCOUNT (Protected - Danger Zone) router.delete('/delete-account', authorization, deleteAccount); +// OTP (email verification) router.post('/send-otp', sendOtp); - - router.post('/verify-otp', verifyOtp); -module.exports = router; \ No newline at end of file +// PASSWORD RESET (FIX 5) +router.post('/forgot-password', forgotPassword); +router.post('/reset-password', resetPassword); + +module.exports = router; diff --git a/backend/utils/input.validation.js b/backend/utils/input.validation.js index 8acaa4fe1..60a99120b 100644 --- a/backend/utils/input.validation.js +++ b/backend/utils/input.validation.js @@ -38,6 +38,13 @@ module.exports.onlyEmailSchema = z.object({ email: z.string().email("Invalid email format") }); +module.exports.resetPasswordSchema = z.object({ + email: z.string().email("Invalid email format"), + otp: z.string().min(6, "OTP is required"), + newPassword: z.string().min(6, "Password must be at least 6 characters").max(100, "Password is too long.") +}); + + module.exports.createProjectSchema = z.object({ name: z.string().min(1, "Project name is required"), description: z.string().optional() diff --git a/docker-compose.yml b/docker-compose.yml index 0e6a8d50c..e4aeb6e9f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,8 +5,6 @@ services: image: mongo:7 container_name: urbackend_mongo restart: unless-stopped - ports: - - '27017:27017' volumes: - mongo_data:/data/db @@ -15,8 +13,6 @@ services: image: redis:latest container_name: urbackend_redis restart: unless-stopped - ports: - - '6379:6379' volumes: - redis_data:/data From e14b3ecf30f1ee9103ff2af659d375abf3e7d370 Mon Sep 17 00:00:00 2001 From: yash-pouranik Date: Wed, 4 Mar 2026 15:49:12 +0530 Subject: [PATCH 2/4] fix: address PR review comments (authLimiter order, crypto.randomInt, OTP helper refactor, strict zod validation) --- backend/app.js | 2 +- backend/controllers/auth.controller.js | 104 ++++++++++++------------- backend/utils/input.validation.js | 7 +- 3 files changed, 55 insertions(+), 58 deletions(-) diff --git a/backend/app.js b/backend/app.js index a283c75eb..447a2c321 100644 --- a/backend/app.js +++ b/backend/app.js @@ -77,9 +77,9 @@ const storageRoute = require('./routes/storage'); const schemaRoute = require('./routes/schemas'); // ROUTES SETUP -app.use('/api/auth', dashboardLimiter, authRoute); // Developer Auth (general) app.use('/api/auth/login', authLimiter); // Strict limiter on login app.use('/api/auth/register', authLimiter); // Strict limiter on register +app.use('/api/auth', dashboardLimiter, authRoute); // Developer Auth (general) app.use('/api/projects', dashboardLimiter, projectRoute); // Project Mgmt app.use('/api/userAuth', limiter, logger, userAuthRoute); app.use('/api/data', limiter, cors(adminCorsOptions), logger, dataRoute); diff --git a/backend/controllers/auth.controller.js b/backend/controllers/auth.controller.js index c4e3d2758..d273dd64c 100644 --- a/backend/controllers/auth.controller.js +++ b/backend/controllers/auth.controller.js @@ -5,17 +5,54 @@ const bcrypt = require("bcryptjs"); const z = require("zod"); const jwt = require("jsonwebtoken"); const sendOtp = require("../utils/emailService"); +const crypto = require("crypto"); const { loginSchema, changePasswordSchema, deleteAccountSchema, onlyEmailSchema, - resetPasswordSchema + resetPasswordSchema, + verifyOtpSchema } = require("../utils/input.validation"); const JWT_EXPIRES_IN = '7d'; const OTP_MAX_ATTEMPTS = 5; +// HELPER: Generate, hash, and store OTP +async function createAndStoreOtp(userId) { + const otp = crypto.randomInt(100000, 1000000).toString(); + + // Delete any existing OTP for this user + await otpSchema.deleteOne({ userId }); + + // Hash OTP before storing + const salt = await bcrypt.genSalt(10); + const hashedOtp = await bcrypt.hash(otp, salt); + + await new otpSchema({ userId, otp: hashedOtp }).save(); + return otp; +} + +// HELPER: Validate OTP with attempt tracking +async function validateOtp(userId, passedOtp) { + const otpDoc = await otpSchema.findOne({ userId }); + if (!otpDoc) throw { status: 400, message: "No OTP found. Please request a new one." }; + + if (otpDoc.attempts >= OTP_MAX_ATTEMPTS) { + await otpDoc.deleteOne(); + throw { status: 429, message: "Too many incorrect attempts. Please request a new OTP." }; + } + + const isMatch = await bcrypt.compare(passedOtp.toString(), otpDoc.otp); + if (!isMatch) { + otpDoc.attempts += 1; + await otpDoc.save(); + const remaining = OTP_MAX_ATTEMPTS - otpDoc.attempts; + throw { status: 400, message: `Incorrect OTP. ${remaining} attempt(s) remaining.` }; + } + + return otpDoc; +} module.exports.register = async (req, res) => { try { @@ -118,26 +155,18 @@ module.exports.deleteAccount = async (req, res) => { module.exports.sendOtp = async (req, res) => { try { const { email } = onlyEmailSchema.parse(req.body); - const otp = Math.floor(100000 + Math.random() * 900000).toString(); const existingUser = await Developer.findOne({ email }); if (!existingUser) return res.status(400).json({ error: "User not found" }); if (existingUser.isVerified) return res.status(400).json({ error: "User already verified" }); - // Delete any existing OTP for this user - await otpSchema.deleteOne({ userId: existingUser._id }); - - // FIX 2: Hash OTP before storing - const salt = await bcrypt.genSalt(10); - const hashedOtp = await bcrypt.hash(otp, salt); - - const newOtp = new otpSchema({ userId: existingUser._id, otp: hashedOtp }); - await newOtp.save(); + const otp = await createAndStoreOtp(existingUser._id); await sendOtp(email, otp); // Send raw OTP to user's email res.json({ message: "OTP sent successfully" }); } catch (err) { + if (err instanceof z.ZodError) return res.status(400).json({ error: err.errors }); console.error(err); res.status(500).json({ error: "Internal Server Error" }); } @@ -146,29 +175,12 @@ module.exports.sendOtp = async (req, res) => { module.exports.verifyOtp = async (req, res) => { try { - const { email } = onlyEmailSchema.parse(req.body); - const { otp } = req.body; + const { email, otp } = verifyOtpSchema.parse(req.body); const existingUser = await Developer.findOne({ email }); if (!existingUser) return res.status(400).json({ error: "User not found" }); - const otpDoc = await otpSchema.findOne({ userId: existingUser._id }); - if (!otpDoc) return res.status(400).json({ error: "No OTP found. Please request a new one." }); - - // FIX 4: OTP attempt tracking - if (otpDoc.attempts >= OTP_MAX_ATTEMPTS) { - await otpDoc.deleteOne(); - return res.status(429).json({ error: "Too many incorrect attempts. Please request a new OTP." }); - } - - // FIX 2: Compare against bcrypt hash - const isMatch = await bcrypt.compare(otp.toString(), otpDoc.otp); - if (!isMatch) { - otpDoc.attempts += 1; - await otpDoc.save(); - const remaining = OTP_MAX_ATTEMPTS - otpDoc.attempts; - return res.status(400).json({ error: `Incorrect OTP. ${remaining} attempt(s) remaining.` }); - } + const otpDoc = await validateOtp(existingUser._id, otp); await otpDoc.deleteOne(); existingUser.isVerified = true; @@ -183,6 +195,8 @@ module.exports.verifyOtp = async (req, res) => { res.status(200).json({ message: "OTP verified successfully", token }); } catch (err) { + if (err.status) return res.status(err.status).json({ error: err.message }); + if (err instanceof z.ZodError) return res.status(400).json({ error: err.errors }); console.error(err); res.status(500).json({ error: "Internal Server Error" }); } @@ -198,20 +212,12 @@ module.exports.forgotPassword = async (req, res) => { // Return same message regardless of whether email exists (prevents user enumeration) if (!dev) return res.status(200).json({ message: "If this email is registered, an OTP has been sent." }); - const otp = Math.floor(100000 + Math.random() * 900000).toString(); - - // Delete any existing OTP - await otpSchema.deleteOne({ userId: dev._id }); - - // Hash OTP before storing - const salt = await bcrypt.genSalt(10); - const hashedOtp = await bcrypt.hash(otp, salt); - - await new otpSchema({ userId: dev._id, otp: hashedOtp }).save(); + const otp = await createAndStoreOtp(dev._id); await sendOtp(email, otp, { subject: "Password Reset OTP — urBackend" }); res.status(200).json({ message: "If this email is registered, an OTP has been sent." }); } catch (err) { + if (err instanceof z.ZodError) return res.status(400).json({ error: err.errors }); console.error(err); res.status(500).json({ error: "Internal Server Error" }); } @@ -226,22 +232,7 @@ module.exports.resetPassword = async (req, res) => { const dev = await Developer.findOne({ email }); if (!dev) return res.status(400).json({ error: "User not found" }); - const otpDoc = await otpSchema.findOne({ userId: dev._id }); - if (!otpDoc) return res.status(400).json({ error: "No OTP found. Please request a new one." }); - - // Attempt tracking - if (otpDoc.attempts >= OTP_MAX_ATTEMPTS) { - await otpDoc.deleteOne(); - return res.status(429).json({ error: "Too many incorrect attempts. Please request a new OTP." }); - } - - const isMatch = await bcrypt.compare(otp.toString(), otpDoc.otp); - if (!isMatch) { - otpDoc.attempts += 1; - await otpDoc.save(); - const remaining = OTP_MAX_ATTEMPTS - otpDoc.attempts; - return res.status(400).json({ error: `Incorrect OTP. ${remaining} attempt(s) remaining.` }); - } + const otpDoc = await validateOtp(dev._id, otp); // OTP matched — update password await otpDoc.deleteOne(); @@ -251,6 +242,7 @@ module.exports.resetPassword = async (req, res) => { res.status(200).json({ message: "Password reset successfully. Please log in with your new password." }); } catch (err) { + if (err.status) return res.status(err.status).json({ error: err.message }); if (err instanceof z.ZodError) return res.status(400).json({ error: err.errors }); console.error(err); res.status(500).json({ error: "Internal Server Error" }); diff --git a/backend/utils/input.validation.js b/backend/utils/input.validation.js index 60a99120b..306f1ad6a 100644 --- a/backend/utils/input.validation.js +++ b/backend/utils/input.validation.js @@ -38,9 +38,14 @@ module.exports.onlyEmailSchema = z.object({ email: z.string().email("Invalid email format") }); +module.exports.verifyOtpSchema = z.object({ + email: z.string().email("Invalid email format"), + otp: z.string().length(6, "OTP must be 6 digits") +}); + module.exports.resetPasswordSchema = z.object({ email: z.string().email("Invalid email format"), - otp: z.string().min(6, "OTP is required"), + otp: z.string().length(6, "OTP must be 6 digits"), newPassword: z.string().min(6, "Password must be at least 6 characters").max(100, "Password is too long.") }); From d717f2335a3d530e51d49177702104d63b72b19d Mon Sep 17 00:00:00 2001 From: yash-pouranik Date: Wed, 4 Mar 2026 16:04:35 +0530 Subject: [PATCH 3/4] fixed parameter issue in sendotp-method --- backend/controllers/auth.controller.js | 1 - backend/utils/emailService.js | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/controllers/auth.controller.js b/backend/controllers/auth.controller.js index d273dd64c..5ae7f2296 100644 --- a/backend/controllers/auth.controller.js +++ b/backend/controllers/auth.controller.js @@ -18,7 +18,6 @@ const { const JWT_EXPIRES_IN = '7d'; const OTP_MAX_ATTEMPTS = 5; -// HELPER: Generate, hash, and store OTP async function createAndStoreOtp(userId) { const otp = crypto.randomInt(100000, 1000000).toString(); diff --git a/backend/utils/emailService.js b/backend/utils/emailService.js index dfb5c72d0..73a88a93f 100644 --- a/backend/utils/emailService.js +++ b/backend/utils/emailService.js @@ -5,12 +5,12 @@ const dotenv = require('dotenv'); dotenv.config(); const resend = new Resend(process.env.RESEND_API_KEY || 're_dummy_key_for_testing'); -async function sendOtp(email, otp) { +async function sendOtp(email, otp, {subject = "Verify your urBackend account"}) { try { const { data, error } = await resend.emails.send({ from: 'urBackend ', to: email, - subject: 'Verify your urBackend account', + subject: subject, html: `
From f117d4e6e48dff1c1b2cb201f6ce309eb0944ca8 Mon Sep 17 00:00:00 2001 From: Yash Pouranik Date: Wed, 4 Mar 2026 16:20:02 +0530 Subject: [PATCH 4/4] Update backend/utils/emailService.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- backend/utils/emailService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/utils/emailService.js b/backend/utils/emailService.js index 73a88a93f..7a0eee2bb 100644 --- a/backend/utils/emailService.js +++ b/backend/utils/emailService.js @@ -5,7 +5,7 @@ const dotenv = require('dotenv'); dotenv.config(); const resend = new Resend(process.env.RESEND_API_KEY || 're_dummy_key_for_testing'); -async function sendOtp(email, otp, {subject = "Verify your urBackend account"}) { +async function sendOtp(email, otp, { subject = "Verify your urBackend account" } = {}) { try { const { data, error } = await resend.emails.send({ from: 'urBackend ',