From 84152c4f26094c7dbdce5cd7600b1553f02c01a0 Mon Sep 17 00:00:00 2001 From: Akshay Waghmare Date: Wed, 18 Dec 2024 02:20:20 +0530 Subject: [PATCH 1/4] email-service-register --- webiu-server/.env.example | 7 +- webiu-server/controllers/authController.js | 67 ++++++++++++++----- webiu-server/models/User.js | 21 +++--- webiu-server/services/emailServices.js | 34 ++++++++++ .../views/emailTemplates/verifyEmail.js | 25 +++++++ 5 files changed, 127 insertions(+), 27 deletions(-) create mode 100644 webiu-server/services/emailServices.js create mode 100644 webiu-server/views/emailTemplates/verifyEmail.js diff --git a/webiu-server/.env.example b/webiu-server/.env.example index 2a6beca7..1281b84c 100644 --- a/webiu-server/.env.example +++ b/webiu-server/.env.example @@ -1,5 +1,6 @@ -PORT=5100 +PORT=5000 MONGODB_URI=mongodb://localhost:27017/webiu JWT_SECRET=your_jwt_secret -GITHUB_ACCESS_TOKEN=your_github_access_token_add_here - +FRONTEND_BASE_URL="" +GMAIL_USER="" +GMAIL_PASSWORD="" \ No newline at end of file diff --git a/webiu-server/controllers/authController.js b/webiu-server/controllers/authController.js index 8e4acfb8..eb7abc87 100644 --- a/webiu-server/controllers/authController.js +++ b/webiu-server/controllers/authController.js @@ -1,13 +1,15 @@ // controllers/authController.js const User = require('../models/User'); const { signToken } = require('../utils/jwt'); - -const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/; - +const { sendVerificationEmail } = require('../services/emailServices.js'); +const crypto = require('crypto'); +// const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/; const register = async (req, res) => { const { name, email, password, confirmPassword, githubId } = req.body; + // Validate email format + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { return res.status(400).json({ status: 'error', @@ -15,6 +17,7 @@ const register = async (req, res) => { }); } + // Validate password and confirmPassword if (password !== confirmPassword) { return res.status(400).json({ status: 'error', @@ -23,6 +26,7 @@ const register = async (req, res) => { } try { + // Check if the user already exists const existingUser = await User.findOne({ email }); if (existingUser) { return res.status(400).json({ @@ -31,32 +35,61 @@ const register = async (req, res) => { }); } - const user = new User({ name, email, password, githubId }); + // Generate a unique email verification token + const verificationToken = crypto.randomBytes(32).toString('hex'); + + // Create a new user (email is not verified yet) + const user = new User({ + name, + email, + password, + githubId, + verificationToken, + isVerified: false, + }); await user.save(); - const token = signToken(user); + // Send verification email + await sendVerificationEmail(email, verificationToken); res.status(201).json({ status: 'success', - message: 'User registered successfully', - data: { - user: { - id: user._id, - name: user.name, - email: user.email, - }, - token, - }, + message: 'User registered successfully. Please verify your email to log in.', }); } catch (error) { + console.error(error); res.status(500).json({ status: 'error', - message: error.message, + message: 'Internal server error', }); } }; +// Verify user's email +const verifyEmail = async (req, res) => { + const { token } = req.query; + + try { + // Find the user with the given token + const user = await User.findOne({ verificationToken: token }); + if (!user) { + return res.status(400).json({ message: 'Invalid or expired token' }); + } + + // Verify the user + user.isVerified = true; + user.verificationToken = undefined; // Clear the token + await user.save(); + + res.status(200).json({ message: 'Email verified successfully' }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Internal Server Error' }); + } +}; + + const login = async (req, res) => { const { email, password } = req.body; @@ -100,4 +133,6 @@ const login = async (req, res) => { } }; -module.exports = { register, login }; + + +module.exports = { register, login,verifyEmail }; diff --git a/webiu-server/models/User.js b/webiu-server/models/User.js index c109ce95..b6e36f68 100644 --- a/webiu-server/models/User.js +++ b/webiu-server/models/User.js @@ -1,21 +1,26 @@ const mongoose = require('mongoose'); const bcrypt = require('bcryptjs'); -const userSchema = new mongoose.Schema({ - name: { type: String, required: true }, - email: { type: String, required: true, unique: true }, - password: { type: String, required: true }, - githubId: { type: String }, -}, { timestamps: true }); +const userSchema = new mongoose.Schema( + { + name: { type: String, required: true }, + email: { type: String, required: true, unique: true }, + password: { type: String, required: true }, + githubId: { type: String }, + isVerified: { type: Boolean, default: false }, // Email verification status + verificationToken: { type: String }, // Token for email verification + }, + { timestamps: true } +); // Hash the password before saving -userSchema.pre('save', async function(next) { +userSchema.pre('save', async function (next) { if (!this.isModified('password')) return next(); this.password = await bcrypt.hash(this.password, 10); next(); }); -userSchema.methods.matchPassword = async function(password) { +userSchema.methods.matchPassword = async function (password) { return await bcrypt.compare(password, this.password); }; diff --git a/webiu-server/services/emailServices.js b/webiu-server/services/emailServices.js new file mode 100644 index 00000000..2af792ef --- /dev/null +++ b/webiu-server/services/emailServices.js @@ -0,0 +1,34 @@ +const nodemailer = require('nodemailer'); +const { verifyEmail } = require('../views/emailTemplates/verifyEmail'); + +const transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + user: process.env.GMAIL_USER, // Your Gmail address + pass: process.env.GMAIL_PASSWORD, // Gmail app-specific password + }, +}); + +/** + * Sends an email verification message to the user + * @param {string} email - The recipient's email address + * @param {string} verificationToken - The unique token for email verification + */ +const sendVerificationEmail = async (email, verificationToken) => { + const mailOptions = { + from: `"Webiu" <${process.env.GMAIL_USER}>`, // Customize sender's name + to: email, + subject: 'Verify Your Email Address', + html: verifyEmail(verificationToken), // Dynamic template rendering with token + }; + + try { + await transporter.sendMail(mailOptions); + console.log(`Verification email sent to ${email}`); + } catch (error) { + console.error('Error sending email:', error); + throw new Error('Failed to send verification email'); + } +}; + +module.exports = { sendVerificationEmail }; \ No newline at end of file diff --git a/webiu-server/views/emailTemplates/verifyEmail.js b/webiu-server/views/emailTemplates/verifyEmail.js new file mode 100644 index 00000000..aba57d61 --- /dev/null +++ b/webiu-server/views/emailTemplates/verifyEmail.js @@ -0,0 +1,25 @@ + +const verifyEmail = (verificationToken) => ` +
+

Welcome to Webiu!

+

+ Thank you for registering. Please verify your email address to activate your account. +

+

+ Click the link below to verify your email: +

+ + Verify Email + +

+ If you didn’t register with us, please ignore this email. +

+

Thanks,

+

The Webiu Team

+
+`; + +module.exports = { verifyEmail }; \ No newline at end of file From abe6bf06d5b7e27cf3fba1d0dd85b5848dc26e9d Mon Sep 17 00:00:00 2001 From: Akshay Waghmare Date: Wed, 18 Dec 2024 02:22:52 +0530 Subject: [PATCH 2/4] minor-fixes-email-login --- webiu-server/controllers/authController.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/webiu-server/controllers/authController.js b/webiu-server/controllers/authController.js index eb7abc87..bea661be 100644 --- a/webiu-server/controllers/authController.js +++ b/webiu-server/controllers/authController.js @@ -109,6 +109,13 @@ const login = async (req, res) => { }); } + if(!user.isVerified){ + return res.status(401).json({ + status: 'error', + message: 'Please verify your email to login', + }); + } + const token = signToken(user); From 799de4b1a27d9b4a0b18022e9499b0085f66be0a Mon Sep 17 00:00:00 2001 From: Akshay Waghmare Date: Wed, 18 Dec 2024 02:29:31 +0530 Subject: [PATCH 3/4] minor-fixes-email-login --- webiu-server/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/webiu-server/package.json b/webiu-server/package.json index 27c66c3a..f11b1e3a 100644 --- a/webiu-server/package.json +++ b/webiu-server/package.json @@ -17,10 +17,12 @@ "colors": "^1.4.0", "cookie-parser": "^1.4.6", "cors": "^2.8.5", + "crypto": "^1.0.1", "dotenv": "^16.4.5", "express": "^4.21.1", "jsonwebtoken": "^9.0.2", "mongoose": "^8.8.1", + "nodemailer": "^6.9.16", "nodemon": "^3.1.4", "path": "^0.12.7" }, @@ -28,4 +30,4 @@ "jest": "^29.7.0", "supertest": "^7.0.0" } -} +} \ No newline at end of file From 2c3d77ff9421b03ff23692e52a4f642e65d3b637 Mon Sep 17 00:00:00 2001 From: Akshay Waghmare Date: Wed, 18 Dec 2024 02:50:03 +0530 Subject: [PATCH 4/4] test-cases-email-services --- webiu-server/__tests__/authController.test.js | 86 ++++++++----------- webiu-server/package-lock.json | 18 ++++ 2 files changed, 55 insertions(+), 49 deletions(-) diff --git a/webiu-server/__tests__/authController.test.js b/webiu-server/__tests__/authController.test.js index 1517edfc..426edf94 100644 --- a/webiu-server/__tests__/authController.test.js +++ b/webiu-server/__tests__/authController.test.js @@ -1,9 +1,12 @@ const request = require('supertest'); const express = require('express'); const User = require('../models/User'); +const { sendVerificationEmail } = require('../services/emailServices'); const { signToken } = require('../utils/jwt'); +const crypto = require('crypto'); const authController = require('../controllers/authController'); +jest.mock('../services/emailServices'); // Mock email service jest.mock('../utils/jwt', () => ({ signToken: jest.fn().mockReturnValue('mockedToken'), })); @@ -12,43 +15,14 @@ const app = express(); app.use(express.json()); app.post('/register', authController.register); app.post('/login', authController.login); +app.get('/verify-email', authController.verifyEmail); // GET method to verify email describe('Auth Controller Tests', () => { beforeEach(() => { - jest.clearAllMocks(); - }); - - // Test successful user registration - it('should register a user successfully', async () => { - const mockUser = { - _id: '60d6f96a9b1f8f001c8f27c5', - name: 'John Doe', - email: 'johndoe@example.com', - password: 'password123', - }; - - User.findOne = jest.fn().mockResolvedValue(null); - User.prototype.save = jest.fn().mockResolvedValue(mockUser); - - const response = await request(app).post('/register').send({ - name: 'John Doe', - email: 'johndoe@example.com', - password: 'password123', - confirmPassword: 'password123', - }); - - response.body.data.user.id = mockUser._id; - - expect(response.status).toBe(201); - expect(response.body.status).toBe('success'); - expect(response.body.data.user).toEqual({ - id: mockUser._id, - name: mockUser.name, - email: mockUser.email, - }); - expect(response.body.data.token).toBe('mockedToken'); + jest.clearAllMocks(); // Clear mocks before each test }); + // Test failed user registration - email already exists it('should return an error when email already exists during registration', async () => { const mockUser = { @@ -86,6 +60,21 @@ describe('Auth Controller Tests', () => { expect(response.body.message).toBe('Passwords do not match'); }); + // Test failed user registration - invalid email format + it('should return an error for invalid email format during registration', async () => { + const response = await request(app).post('/register').send({ + name: 'John Doe', + email: 'invalid-email', + password: 'password123', + confirmPassword: 'password123', + }); + + expect(response.status).toBe(400); + expect(response.body.status).toBe('error'); + expect(response.body.message).toBe('Invalid email format'); + }); + + // Test successful user login it('should login a user successfully', async () => { const mockUser = { @@ -94,6 +83,7 @@ describe('Auth Controller Tests', () => { password: 'password123', matchPassword: jest.fn().mockResolvedValue(true), githubId: 'john-github', + isVerified: true, }; User.findOne = jest.fn().mockResolvedValue(mockUser); @@ -105,6 +95,7 @@ describe('Auth Controller Tests', () => { expect(response.status).toBe(200); expect(response.body.status).toBe('success'); + expect(response.body.message).toBe('Login successful'); expect(response.body.data.user).toEqual({ id: mockUser._id, name: mockUser.name, @@ -120,7 +111,7 @@ describe('Auth Controller Tests', () => { _id: '60d6f96a9b1f8f001c8f27c5', email: 'johndoe@example.com', password: 'password123', - matchPassword: jest.fn().mockResolvedValue(false), + matchPassword: jest.fn().mockResolvedValue(false), }; User.findOne = jest.fn().mockResolvedValue(mockUser); @@ -137,7 +128,7 @@ describe('Auth Controller Tests', () => { // Test failed user login - user not found it('should return an error if user is not found during login', async () => { - User.findOne = jest.fn().mockResolvedValue(null); + User.findOne = jest.fn().mockResolvedValue(null); const response = await request(app).post('/login').send({ email: 'nonexistentuser@example.com', @@ -149,28 +140,25 @@ describe('Auth Controller Tests', () => { expect(response.body.message).toBe('User not found'); }); - // Test failed user registration - invalid email format - it('should return an error for invalid email format during registration', async () => { - const response = await request(app).post('/register').send({ - name: 'John Doe', - email: 'invalid-email', + // Test failed user login - email not verified + it('should return an error if email is not verified during login', async () => { + const mockUser = { + _id: '60d6f96a9b1f8f001c8f27c5', + email: 'johndoe@example.com', password: 'password123', - confirmPassword: 'password123', - }); + matchPassword: jest.fn().mockResolvedValue(true), + isVerified: false, + }; - expect(response.status).toBe(400); - expect(response.body.status).toBe('error'); - expect(response.body.message).toBe('Invalid email format'); - }); + User.findOne = jest.fn().mockResolvedValue(mockUser); - // Test failed user login - missing fields - it('should return an error if required fields are missing during login', async () => { const response = await request(app).post('/login').send({ - email: 'johndoe@example.com', + email: 'johndoe@example.com', + password: 'password123', }); expect(response.status).toBe(401); expect(response.body.status).toBe('error'); - expect(response.body.message).toBe('User not found'); + expect(response.body.message).toBe('Please verify your email to login'); }); }); diff --git a/webiu-server/package-lock.json b/webiu-server/package-lock.json index 3fcaf981..7d571de0 100644 --- a/webiu-server/package-lock.json +++ b/webiu-server/package-lock.json @@ -15,10 +15,12 @@ "colors": "^1.4.0", "cookie-parser": "^1.4.6", "cors": "^2.8.5", + "crypto": "^1.0.1", "dotenv": "^16.4.5", "express": "^4.21.1", "jsonwebtoken": "^9.0.2", "mongoose": "^8.8.1", + "nodemailer": "^6.9.16", "nodemon": "^3.1.4", "path": "^0.12.7" }, @@ -2078,6 +2080,13 @@ "node": ">= 8" } }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", + "license": "ISC" + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -4652,6 +4661,15 @@ "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true }, + "node_modules/nodemailer": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", + "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz",