From 10615970609b6b74aaa56c4107dc26816eff9125 Mon Sep 17 00:00:00 2001 From: Mitesh Patil Date: Tue, 24 Feb 2026 22:25:05 +0530 Subject: [PATCH 1/2] start fix --- .env.example | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 02f5cee..14bff9e 100644 --- a/.env.example +++ b/.env.example @@ -38,9 +38,9 @@ CORS_ORIGIN=* EMAIL_ENABLED=true SMTP_HOST=smtp.gmail.com SMTP_PORT=587 -SMTP_USER=padmanimayank12@gmail.com -SMTP_PASS=mztu mhzs hhbw qbcc -EMAIL_FROM="Code Duel " +SMTP_USER=your_email_address +SMTP_PASS=your_app_password +SMTP_FROM=your_email_address # Email Reminder Cron Configuration # Daily reminder time (default: 6 PM daily) From ffb453e2ade4c2ad4098cf1e32d1cb1a3373fa94 Mon Sep 17 00:00:00 2001 From: Mitesh Patil Date: Fri, 27 Feb 2026 11:06:20 +0530 Subject: [PATCH 2/2] fix : Email Verification --- .env.example | 1 + README.md | 1 + package-lock.json | 108 +++++++++++++++++------------ package.json | 7 +- prisma/schema.prisma | 3 + src/app.js | 19 ++--- src/config/env.js | 2 + src/controllers/auth.controller.js | 13 ++++ src/routes/auth.routes.js | 15 ++-- src/services/auth.service.js | 67 +++++++++++++++--- src/services/email.service.js | 73 +++++++++++++++++++ 11 files changed, 233 insertions(+), 76 deletions(-) diff --git a/.env.example b/.env.example index 14bff9e..39380d4 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ # Environment Configuration NODE_ENV=development PORT=3000 +BACKEND_URL=http://localhost:3000 # Database Configuration # PostgreSQL connection string diff --git a/README.md b/README.md index 30eb359..38b6c95 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,7 @@ When creating a challenge, configure: - Input validation on all endpoints - SQL injection protection via Prisma ORM - Environment variable validation on startup +- Email verification before account activation ## 📝 Development Notes diff --git a/package-lock.json b/package-lock.json index 653889e..ce935e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,6 @@ "jsonwebtoken": "^9.0.2", "node-cron": "^3.0.3", "nodemailer": "^8.0.1", - "prisma": "^5.8.0", "prisma": "^5.22.0", "winston": "^3.11.0" }, @@ -53,6 +52,7 @@ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", "hasInstallScript": true, + "license": "Apache-2.0", "engines": { "node": ">=16.13" }, @@ -68,13 +68,15 @@ "node_modules/@prisma/debug": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", - "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==" + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "license": "Apache-2.0" }, "node_modules/@prisma/engines": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { "@prisma/debug": "5.22.0", "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", @@ -85,12 +87,14 @@ "node_modules/@prisma/engines-version": { "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", - "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==" + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "license": "Apache-2.0", "dependencies": { "@prisma/debug": "5.22.0", "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", @@ -101,6 +105,7 @@ "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "license": "Apache-2.0", "dependencies": { "@prisma/debug": "5.22.0" } @@ -167,22 +172,25 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/bcryptjs": { "version": "2.4.3", @@ -228,14 +236,16 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -378,13 +388,6 @@ "node": ">= 0.8" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -422,9 +425,9 @@ "license": "MIT" }, "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "license": "MIT", "dependencies": { "object-assign": "^4", @@ -432,6 +435,10 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/debug": { @@ -592,6 +599,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -637,6 +645,7 @@ "version": "8.2.1", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "license": "MIT", "dependencies": { "ip-address": "10.0.1" }, @@ -654,6 +663,7 @@ "version": "7.3.1", "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz", "integrity": "sha512-IGenaSf+DnWc69lKuqlRE9/i/2t5/16VpH5bXoqdxWz1aCpRvEdrBuu1y95i/iL5QP8ZYVATiwLFhwk3EDl5vg==", + "license": "MIT", "dependencies": { "lodash": "^4.17.21", "validator": "~13.15.23" @@ -942,6 +952,7 @@ "version": "10.0.1", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", "engines": { "node": ">= 12" } @@ -1069,9 +1080,9 @@ "license": "MIT" }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash.includes": { @@ -1209,16 +1220,19 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/ms": { @@ -1258,16 +1272,16 @@ } }, "node_modules/nodemon": { - "version": "3.1.11", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", - "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", "dev": true, "license": "MIT", "dependencies": { "chokidar": "^3.5.2", "debug": "^4", "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", + "minimatch": "^10.2.1", "pstree.remy": "^1.1.8", "semver": "^7.5.3", "simple-update-notifier": "^2.0.0", @@ -1396,6 +1410,8 @@ "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/engines": "5.22.0" }, @@ -1436,9 +1452,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -1537,9 +1553,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" diff --git a/package.json b/package.json index d152cc3..e1e8624 100644 --- a/package.json +++ b/package.json @@ -19,22 +19,17 @@ "author": "", "license": "ISC", "dependencies": { - "@prisma/client": "^5.8.0", "@prisma/client": "^5.22.0", "axios": "^1.6.5", "bcryptjs": "^2.4.3", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", - "express-validator": "^7.0.1", - "jsonwebtoken": "^9.0.2", - "node-cron": "^3.0.3", - "nodemailer": "^8.0.1", - "prisma": "^5.8.0", "express-rate-limit": "^8.2.1", "express-validator": "^7.3.1", "jsonwebtoken": "^9.0.2", "node-cron": "^3.0.3", + "nodemailer": "^8.0.1", "prisma": "^5.22.0", "winston": "^3.11.0" }, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 24667b0..2c77a2b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -18,6 +18,9 @@ model User { leetcodeUsername String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + isEmailVerified Boolean @default(false) + emailVerificationToken String? + emailVerificationExpires DateTime? // Email Preferences emailPreferences Json? @default("{\"welcomeEmail\": true, \"streakReminder\": true, \"streakBroken\": true, \"weeklySummary\": true}") diff --git a/src/app.js b/src/app.js index 45ac8c4..b6c14ef 100644 --- a/src/app.js +++ b/src/app.js @@ -17,21 +17,22 @@ const { apiLimiter, authLimiter } = require('./middlewares/rateLimiter.middlewar const createApp = () => { const app = express(); - // 1. Security Middlewares (Team T066 Implementation) - // Apply global rate limiting to all API routes - app.use('/api/', apiLimiter); - - // Apply strict limiting specifically to auth routes - app.use('/api/auth/', authLimiter); - - // 2. CORS configuration + // 1. CORS configuration (must come before rate limiters so preflight requests are handled) app.use( cors({ + // if corsOrigin is wildcard we avoid setting credentials since browsers reject wildcard with credentials origin: config.corsOrigin, - credentials: true, + ...(config.corsOrigin === '*' ? {} : { credentials: true }), }) ); + // 2. Security Middlewares (Team T066 Implementation) + // Apply global rate limiting to all API routes + app.use('/api/', apiLimiter); + + // Apply strict limiting specifically to auth routes + app.use('/api/auth/', authLimiter); + // 3. Body parser middleware app.use(express.json()); app.use(express.urlencoded({ extended: true })); diff --git a/src/config/env.js b/src/config/env.js index 9e0844b..96f1c17 100644 --- a/src/config/env.js +++ b/src/config/env.js @@ -7,6 +7,8 @@ const config = { // Server Configuration port: process.env.PORT || 3000, nodeEnv: process.env.NODE_ENV || "development", + backendUrl:process.env.BACKEND_URL || "http://localhost:3000", + // Database Configuration databaseUrl: process.env.DATABASE_URL, diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index cd52a0b..e0fde78 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -39,6 +39,18 @@ const validateLogin = [ * Register a new user * POST /api/auth/register */ + +const verifyEmail = asyncHandler(async (req, res) => { + const { token } = req.query; + + const result = await authService.verifyEmail(token); + + res.status(200).json({ + success: true, + message: result.message, + }); +}); + const register = asyncHandler(async (req, res) => { // Check for validation errors const errors = validationResult(req); @@ -132,4 +144,5 @@ module.exports = { updateProfile, validateRegister, validateLogin, + verifyEmail, }; diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js index f7ace29..785d4b7 100644 --- a/src/routes/auth.routes.js +++ b/src/routes/auth.routes.js @@ -7,7 +7,7 @@ const { authenticate } = require("../middlewares/auth.middleware"); * @route POST /api/auth/register * @desc Register a new user * @access Public - */ +*/ router.post( "/register", authController.validateRegister, @@ -18,21 +18,28 @@ router.post( * @route POST /api/auth/login * @desc Login user * @access Public - */ +*/ router.post("/login", authController.validateLogin, authController.login); /** * @route GET /api/auth/profile * @desc Get current user profile * @access Private - */ +*/ router.get("/profile", authenticate, authController.getProfile); /** * @route PUT /api/auth/profile * @desc Update user profile * @access Private - */ +*/ router.put("/profile", authenticate, authController.updateProfile); +/** + * @route PUT /api/auth/verify-email + * @desc Verifies user email + * @access Public +*/ +router.get("/verify-email", authController.verifyEmail); + module.exports = router; diff --git a/src/services/auth.service.js b/src/services/auth.service.js index be998fc..200affe 100644 --- a/src/services/auth.service.js +++ b/src/services/auth.service.js @@ -3,8 +3,8 @@ const { prisma } = require("../config/prisma"); const { generateToken } = require("../utils/jwt"); const { AppError } = require("../middlewares/error.middleware"); const logger = require("../utils/logger"); -const { sendWelcomeEmail } = require("./email.service"); - +const { sendWelcomeEmail, sendVerificationEmail } = require("./email.service"); +const crypto = require('crypto'); /** * Register a new user * @param {Object} userData - User registration data @@ -29,16 +29,21 @@ const register = async (userData) => { } } - // Hash password + const hashedPassword = await bcrypt.hash(password, 12); - // Create user + const verificationToken = crypto.randomBytes(32).toString("hex"); + const tokenExpiry = new Date(Date.now() + 60 * 60 * 1000); // 1 hour + const user = await prisma.user.create({ data: { email, username, password: hashedPassword, leetcodeUsername: leetcodeUsername || null, + isEmailVerified: false, + emailVerificationToken: verificationToken, + emailVerificationExpires: tokenExpiry, }, select: { id: true, @@ -49,21 +54,53 @@ const register = async (userData) => { }, }); - // Generate JWT token - const token = generateToken({ userId: user.id }); - logger.info(`New user registered: ${user.username} (${user.email})`); - // Send welcome email (non-blocking) - sendWelcomeEmail(user.email, user.username).catch((err) => { - logger.error(`Failed to send welcome email: ${err.message}`); + sendVerificationEmail( + user.email, + user.username, + verificationToken + ).catch((err) => { + logger.error(`Failed to send verification email: ${err.message}`); }); return { user, - token, + message: "Registration successful. Please verify your email.", }; }; +// VerifyEmail +const verifyEmail = async (token) => { + const user = await prisma.user.findFirst({ + where: { + emailVerificationToken: token, + emailVerificationExpires: { + gte: new Date(), + }, + }, + }); + + if (!user) { + throw new AppError("Invalid or expired verification token", 400); + } + + await prisma.user.update({ + where: { id: user.id }, + data: { + isEmailVerified: true, + emailVerificationToken: null, + emailVerificationExpires: null, + }, + }); + + sendWelcomeEmail(user.email, user.username).catch((err) => { + logger.error(`Failed to send welcome email: ${err.message}`); + }); + + logger.info(`Email verified for user: ${user.username}`); + + return { message: "Email verified successfully" }; +}; /** * Login user @@ -90,6 +127,13 @@ const login = async (emailOrUsername, password) => { throw new AppError("Invalid credentials", 401); } + if (!user.isEmailVerified) { + throw new AppError( + "Please verify your email before logging in.", + 403 + ); +} + // Generate JWT token const token = generateToken({ userId: user.id }); @@ -215,4 +259,5 @@ module.exports = { login, getProfile, updateProfile, + verifyEmail, }; diff --git a/src/services/email.service.js b/src/services/email.service.js index 1b176eb..f7c266c 100644 --- a/src/services/email.service.js +++ b/src/services/email.service.js @@ -259,6 +259,47 @@ const templates = { `, }), + verification: (username, verificationLink) => ({ + subject: "Verify Your Code Duel Account 🔐", + html: ` + + + + + + +
+
+

Email Verification 🔐

+
+
+

Hey ${username}! 👋

+

Thanks for registering with Code Duel.

+

Please verify your email address by clicking the button below:

+ + Verify Email + +

This link will expire in 1 hour.

+

If you didn’t create this account, you can safely ignore this email.

+ +

See you inside! 🚀

+

The Code Duel Team

+
+ +
+ + + `, + }), }; /** @@ -285,6 +326,37 @@ const sendWelcomeEmail = async (email, username) => { } }; +/** + * Send email verification link + * @param {string} email - User email + * @param {string} username - Username + * @param {string} token - Verification token + */ +const sendVerificationEmail = async (email, username, token) => { + try { + const baseUrl = process.env.BACKEND_URL || "http://localhost:3000"; + + const verificationLink = `${baseUrl}/api/auth/verify-email?token=${token}`; + + const template = templates.verification(username, verificationLink); + + const result = await sendEmail({ + to: email, + subject: template.subject, + html: template.html, + }); + + if (result.success) { + logger.info(`Verification email sent to ${email}`); + } + + return result; + } catch (error) { + logger.error(`Failed to send verification email to ${email}:`, error); + return { success: false, reason: error.message }; + } +}; + /** * Send streak reminder email * @param {string} email - User email @@ -565,6 +637,7 @@ const getLeaderboardRank = async (challengeId, memberId) => { module.exports = { sendWelcomeEmail, + sendVerificationEmail, sendStreakReminder, sendStreakBrokenNotification, sendWeeklySummary,