Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add User Verification Route and Email Notification #59

Merged
merged 2 commits into from
May 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ env:
TEST_DB_USER: ${{secrets.TEST_DB_USER}}
TEST_DB_PASS: ${{secrets.TEST_DB_PASS}}
TEST_DB_NAME: ${{secrets.TEST_DB_NAME}}
HOST: ${{secrets.HOST}}
AUTH_EMAIL: ${{secrets.AUTH_EMAIL}}
AUTH_PASSWORD: ${{secrets.AUTH_PASSWORD}}

jobs:
build-lint-test-coverage:
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"highlight.js": "^11.9.0",
"jsend": "^1.1.0",
"morgan": "^1.10.0",
"nodemailer": "^6.9.13",
"nodemon": "^3.1.0",
"pg": "^8.11.5",
"reflect-metadata": "^0.2.2",
Expand Down Expand Up @@ -58,6 +59,7 @@
"@types/jsend": "^1.0.32",
"@types/morgan": "^1.9.9",
"@types/node": "^20.12.7",
"@types/nodemailer": "^6.4.15",
"@types/reflect-metadata": "^0.1.0",
"@types/supertest": "^6.0.2",
"@types/winston": "^2.4.4",
Expand Down
54 changes: 50 additions & 4 deletions src/__test__/route.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import request from 'supertest';
import { app, server } from '../index';
import { createConnection, getConnection, getConnectionOptions } from 'typeorm';
import { createConnection, getConnection, getConnectionOptions, getRepository } from 'typeorm';
import { User } from '../entities/User';

beforeAll(async () => {
Expand Down Expand Up @@ -47,17 +47,16 @@
const newUser = {
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com',
email: 'johndoe06@example.com',
password: 'password',
gender: 'Male',
phoneNumber: '1234567890',
phoneNumber: '123678116',
userType: 'Buyer',
photoUrl: 'https://example.com/photo.jpg',
};

// Act
const res = await request(app).post('/user/register').send(newUser);

// Assert
expect(res.status).toBe(201);
expect(res.body).toEqual({
Expand All @@ -67,5 +66,52 @@
message: 'User registered successfully',
},
});

// Clean up: delete the test user
const userRepository = getRepository(User);
const user = await userRepository.findOne({ where: { email: newUser.email } });
if (user) {
await userRepository.remove(user);
}
});
});
describe('POST /user/verify/:id', () => {
it('should verify a user', async () => {
// Arrange
const newUser = {
firstName: 'John',
lastName: 'Doe',
email: '[email protected]',
password: 'password',
gender: 'Male',
phoneNumber: '123456789',
userType: 'Buyer',
photoUrl: 'https://example.com/photo.jpg',
};

// Create a new user
const res = await request(app).post('/user/register').send(newUser);

Check warning on line 93 in src/__test__/route.test.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

'res' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 93 in src/__test__/route.test.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

'res' is assigned a value but never used

Check warning on line 93 in src/__test__/route.test.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

'res' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 93 in src/__test__/route.test.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

'res' is assigned a value but never used

const userRepository = getRepository(User);
const user = await userRepository.findOne({ where: { email: newUser.email } });

if(user){
const verifyRes = await request(app).get(`/user/verify/${user.id}`);

// Assert
expect(verifyRes.status).toBe(200);
expect(verifyRes.text).toEqual('<p>User verified successfully</p>');

Check warning on line 104 in src/__test__/route.test.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

Trailing spaces not allowed

Check warning on line 104 in src/__test__/route.test.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

Trailing spaces not allowed
// Check that the user's verified field is now true
const verifiedUser = await userRepository.findOne({ where: { email: newUser.email } });
if (verifiedUser){
expect(verifiedUser.verified).toBe(true);
}

}

Check warning on line 112 in src/__test__/route.test.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

Trailing spaces not allowed

Check warning on line 112 in src/__test__/route.test.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

Trailing spaces not allowed
if (user) {
await userRepository.remove(user);
}
});
});
54 changes: 7 additions & 47 deletions src/controllers/authController.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,15 @@
import { Request, Response } from 'express';
import { User } from '../entities/User';

Check warning on line 2 in src/controllers/authController.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

'User' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 2 in src/controllers/authController.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

'User' is defined but never used

Check warning on line 2 in src/controllers/authController.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

'User' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 2 in src/controllers/authController.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

'User' is defined but never used
import bcrypt from 'bcrypt';

Check warning on line 3 in src/controllers/authController.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

'bcrypt' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 3 in src/controllers/authController.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

'bcrypt' is defined but never used

Check warning on line 3 in src/controllers/authController.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

'bcrypt' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 3 in src/controllers/authController.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

'bcrypt' is defined but never used
import { getRepository } from 'typeorm';

Check warning on line 4 in src/controllers/authController.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

'getRepository' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 4 in src/controllers/authController.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

'getRepository' is defined but never used

Check warning on line 4 in src/controllers/authController.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

'getRepository' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 4 in src/controllers/authController.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

'getRepository' is defined but never used
import { responseError, responseServerError, responseSuccess } from '../utils/response.utils';
import { validate } from 'class-validator';
import { userVerificationService, userRegistrationService } from '../services';

class UserController {
static registerUser = async (req: Request, res: Response) => {
const { firstName, lastName, email, password, gender, phoneNumber, userType, photoUrl } = req.body;

// Validate user input
if (!firstName || !lastName || !email || !password || !gender || !phoneNumber || !photoUrl) {
return responseError(res, 400, 'Please fill all the required fields');
}

const userRepository = getRepository(User);

try {
// Check for existing user
const existingUser = await userRepository.findOneBy({ email });
const existingUserNumber = await userRepository.findOneBy({ phoneNumber });

if (existingUser || existingUserNumber) {
return responseError(res, 409, 'Email or phone number already in use');
}

const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);

// Create user
const user = new User();
user.firstName = firstName;
user.lastName = lastName;
user.email = email;
user.password = hashedPassword;
user.userType = userType;
user.gender = gender;
user.phoneNumber = phoneNumber;
user.photoUrl = photoUrl;

// Save user
await userRepository.save(user);

return responseSuccess(res, 201, 'User registered successfully');
} catch (error) {
if (error instanceof Error) {
return responseServerError(res, error.message);
}

return responseServerError(res, 'Unknown error occurred');
}
};
export const userRegistration = async (req: Request, res: Response) => {
await userRegistrationService(req, res);
}
export { UserController };
export const userVerification = async (req: Request, res: Response) => {
await userVerificationService(req, res);
}

4 changes: 2 additions & 2 deletions src/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { UserController } from './authController';
import { userRegistration,userVerification } from './authController';

export { UserController };
export { userRegistration,userVerification };
7 changes: 3 additions & 4 deletions src/routes/UserRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Router } from 'express';
import { UserController } from '../controllers/index';

const { registerUser } = UserController;
import { userRegistration, userVerification} from '../controllers/index';

const router = Router();

router.post('/register', registerUser);
router.post('/register', userRegistration);
router.get('/verify/:id', userVerification);

export default router;
2 changes: 2 additions & 0 deletions src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
// export all Services
export * from './userServices/userRegistrationService';
export * from './userServices/userValidationService';
74 changes: 74 additions & 0 deletions src/services/userServices/userRegistrationService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@

import { Request, Response } from 'express';
import { User } from '../../entities/User';
import bcrypt from 'bcrypt';
import { getRepository } from 'typeorm';
import { responseError, responseServerError, responseSuccess } from '../../utils/response.utils';
import sendMail from '../../utils/sendMail';
import dotenv from 'dotenv';
dotenv.config();

export const userRegistrationService = async (req: Request, res: Response) => {
const { firstName, lastName, email, password, gender, phoneNumber, userType, photoUrl } = req.body;

// Validate user input
if (!firstName || !lastName || !email || !password || !gender || !phoneNumber || !photoUrl) {
return responseError(res, 400, 'Please fill all the required fields');
}

const userRepository = getRepository(User);

try {
// Check for existing user
const existingUser = await userRepository.findOneBy({ email });
const existingUserNumber = await userRepository.findOneBy({ phoneNumber });

if (existingUser || existingUserNumber) {
return responseError(res, 409, 'Email or phone number already in use');
}

const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);

// Create user
const user = new User();
user.firstName = firstName;
user.lastName = lastName;
user.email = email;
user.password = hashedPassword;
user.userType = userType;
user.gender = gender;
user.phoneNumber = phoneNumber;
user.photoUrl = photoUrl;

// Save user
await userRepository.save(user);
if (process.env.AUTH_EMAIL && process.env.AUTH_PASSWORD) {

const message = {
to: email,
from: process.env.AUTH_EMAIL,
subject: 'Welcome to the knights app',
text: `Welcome to the app, ${firstName} ${lastName}!`,
lastName: lastName,
firstName: firstName,
}
const link = `http://localhost:${process.env.PORT}/user/verify/${user.id}`

sendMail(process.env.AUTH_EMAIL, process.env.AUTH_PASSWORD, message, link);


} else {
// return res.status(500).json({ error: 'Email or password for mail server not configured' });
return responseError(res, 500 , 'Email or password for mail server not configured');
}

return responseSuccess(res, 201, 'User registered successfully');
} catch (error) {
if (error instanceof Error) {
return responseServerError(res, error.message);
}

return responseServerError(res, 'Unknown error occurred');
}
};
28 changes: 28 additions & 0 deletions src/services/userServices/userValidationService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Request, Response } from 'express';
import { User } from '../../entities/User';
import { getRepository } from 'typeorm';



export const userVerificationService = async (req: Request, res: Response) => {
const { id } = req.params;

// Validate user input
if (!id) {
return res.status(400).json({ error: 'Missing user ID' });
}

const userRepository = getRepository(User);
const user = await userRepository.findOneBy({id});

if (!user) {
return res.status(404).json({ error: 'User not found' });
}

user.verified = true;

await userRepository.save(user);

return res.status(200).send('<p>User verified successfully</p>');

}
90 changes: 90 additions & 0 deletions src/utils/sendMail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import nodemailer from 'nodemailer';

const sendMail = async (userAuth: string,
passAuth: string,
message: {from: string,to:string, subject: string, text: string, firstName: string , lastName: string},
link: string = '') => {
const transporter = nodemailer.createTransport({
host: process.env.HOST,
port: 587,
secure: false, // true for 465, false for other ports
auth: {
user: userAuth,
pass: passAuth
},
});

const { from, to, subject, text, firstName, lastName } = message;

const mailOptions = {
from: from,
to: to,
subject: subject,
text: text,
firstName: firstName,
lastName: lastName,
html: `
<!DOCTYPE html>
<html lang="en">
<head>
<style>
/* Reset styles */
body, html {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
}
/* Container styles */
.container {
max-width: 600px;
// margin: 0 auto;
padding: 20px;
background-color: #f9f9f9;
border-radius: 10px;
}

/* Content styles */
.content {
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
/* Footer styles */
.footer {
margin-top: 20px;
text-align: center;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<h3 class="header">
Hello ${firstName} ${lastName},
</h3>
<div class="content">
<p>${text}</p>
<p> </p>
<p>${link && `<a href=${link}>click here to verifie your account</a>`}</p>
<p>This message is from: Knights Andela </p>
</div>
<div class="footer">
<p>&copy; ${new Date().getFullYear()} Knights Andela. All rights reserved.</p>
</div>
</div>
</body>
</html>
`

};

try {
const info = await transporter.sendMail(mailOptions);
console.log('Message sent: %s', info.messageId);
} catch (error) {
console.log('Error occurred while sending email', error);
}
};

export default sendMail;