diff --git a/.env.example b/.env.example index 0a48e9f..3dff852 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,20 @@ PORT= ******************************** APP_ENV= ******************************** -PDN_DB_NAME= ***************************** + +TEST_DB_HOST= ******************************** +TEST_DB_PORT= ******************************** +TEST_DB_USER= ******************************** +TEST_DB_PASS= ******************************** +TEST_DB_NAME= ******************************** + DEV_DB_HOST= ******************************** DEV_DB_PORT= ******************************** DEV_DB_USER= ******************************** DEV_DB_PASS= ***************************** -DEV_DB_TYPE= ******************************* +DEV_DB_NAME= ******************************* PDN_DB_HOST= ******************************** PDN_DB_PORT= ******************************** PDN_DB_USER= ******************************** -PDN_DB_PASS= ******************************** \ No newline at end of file +PDN_DB_PASS= ******************************** +PDN_DB_NAME= ***************************** \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index 7d487c3..2607339 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,10 +3,7 @@ module.exports = { root: true, parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint'], - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended' - ], + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], rules: { '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': [ @@ -28,8 +25,14 @@ module.exports = { 'space-before-function-paren': ['warn', 'always'], 'func-style': ['warn', 'declaration', { allowArrowFunctions: true }], 'camelcase': 'warn', - '@typescript-eslint/explicit-function-return-type': ['warn', { allowExpressions: true }], - '@typescript-eslint/explicit-member-accessibility': ['off', { accessibility: 'explicit' }], + '@typescript-eslint/explicit-function-return-type': [ + 'warn', + { allowExpressions: true }, + ], + '@typescript-eslint/explicit-member-accessibility': [ + 'off', + { accessibility: 'explicit' }, + ], 'no-unused-vars': 'warn', 'no-extra-semi': 'warn', }, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99db3dd..e3b23d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,11 +3,11 @@ name: knights-ecomm-be CI on: [push, pull_request] env: - DEV_DB_HOST: ${{secrets.DEV_DB_HOST}} - DEV_DB_PORT: ${{secrets.DEV_DB_PORT}} - DEV_DB_USER: ${{secrets.DEV_DB_USER}} - DEV_DB_PASS: ${{secrets.DEV_DB_PASS}} - DEV_DB_NAME: ${{secrets.DEV_DB_NAME}} + TEST_DB_HOST: ${{secrets.TEST_DB_HOST}} + TEST_DB_PORT: ${{secrets.TEST_DB_PORT}} + TEST_DB_USER: ${{secrets.TEST_DB_USER}} + TEST_DB_PASS: ${{secrets.TEST_DB_PASS}} + TEST_DB_NAME: ${{secrets.TEST_DB_NAME}} jobs: build-lint-test-coverage: diff --git a/.gitignore b/.gitignore index 66612c2..1500c37 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ package-lock.json coverage/ dist /src/logs -.DS_Stor \ No newline at end of file +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 782f032..58ad41f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # E-commerse Backend API -[![knights-ecomm-be CI](https://github.com/atlp-rwanda/knights-ecomm-be/actions/workflows/ci.yml/badge.svg)](https://github.com/atlp-rwanda/knights-ecomm-be/actions/workflows/ci.yml)    [![Coverage Status](https://coveralls.io/repos/github/atlp-rwanda/knights-ecomm-be/badge.svg?branch=ch-ci-setup)](https://coveralls.io/github/atlp-rwanda/knights-ecomm-be?branch=ch-ci-setup)    [![Version](https://img.shields.io/badge/version-1.0.0-blue)](https://github.com/your-username/your-repo-name/releases/tag/v1.0.0) + +[![Coverage Status](https://coveralls.io/repos/github/atlp-rwanda/knights-ecomm-be/badge.svg?branch=develop)](https://coveralls.io/github/atlp-rwanda/knights-ecomm-be?branch=develop) +   +[![Coverage Status](https://coveralls.io/repos/github/atlp-rwanda/knights-ecomm-be/badge.svg?branch=ch-ci-setup)](https://coveralls.io/github/atlp-rwanda/knights-ecomm-be?branch=ch-ci-setup) +   +[![Version](https://img.shields.io/badge/version-1.0.0-blue)](https://github.com/your-username/your-repo-name/releases/tag/v1.0.0) ## Description diff --git a/migrations/1714595134552-UserMigration.ts b/migrations/1714595134552-UserMigration.ts new file mode 100644 index 0000000..7eb7953 --- /dev/null +++ b/migrations/1714595134552-UserMigration.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateUserMigration1614495123940 implements MigrationInterface { + public async up (queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "user" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "firstName" character varying NOT NULL, + "lastName" character varying NOT NULL, + "email" character varying NOT NULL, + "password" character varying NOT NULL, + "gender" character varying NOT NULL, + "phoneNumber" character varying NOT NULL, + "photoUrl" character varying, + "verified" boolean NOT NULL, + "status" character varying NOT NULL CHECK (status IN ('active', 'suspended')), + "userType" character varying NOT NULL DEFAULT 'Buyer' CHECK (userType IN ('Admin', 'Buyer', 'Vendor')), + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), + CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id") + ) + `); + } + + public async down (queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "user"`); + } +} diff --git a/ormconfig.js b/ormconfig.js index a4d78a5..bc7acdf 100644 --- a/ormconfig.js +++ b/ormconfig.js @@ -1,24 +1,39 @@ -module.exports = { - "type": "postgres", - "host": `${process.env.DEV_DB_HOST}`, - "port": `${process.env.DEV_DB_PORT}`, - "username": `${process.env.DEV_DB_USER}`, - "password": `${process.env.DEV_DB_PASS}`, - "database": `${process.env.DEV_DB_NAME}`, - "synchronize": true, - "logging": false, - "entities": [ - "src/entities/**/*.ts" - ], - "migrations": [ - "src/migrations/**/*.ts" - ], - "subscribers": [ - "src/subscribers/**/*.ts" - ], - "cli": { - "entitiesDir": "src/entities", - "migrationsDir": "src/migrations", - "subscribersDir": "src/subscribers" - } -}; \ No newline at end of file +const devConfig = { + type: 'postgres', + host: process.env.DEV_DB_HOST, + port: process.env.DEV_DB_PORT, + username: process.env.DEV_DB_USER, + password: process.env.DEV_DB_PASS, + database: process.env.DEV_DB_NAME, + synchronize: true, + logging: false, + entities: ['src/entities/**/*.ts'], + migrations: ['src/migrations/**/*.ts'], + subscribers: ['src/subscribers/**/*.ts'], + cli: { + entitiesDir: 'src/entities', + migrationsDir: 'src/migrations', + subscribersDir: 'src/subscribers', + }, +}; + +const testConfig = { + type: 'postgres', + host: process.env.TEST_DB_HOST, + port: process.env.TEST_DB_PORT, + username: process.env.TEST_DB_USER, + password: process.env.TEST_DB_PASS, + database: process.env.TEST_DB_NAME, + synchronize: true, + logging: false, + entities: ['src/entities/**/*.ts'], + migrations: ['src/migrations/**/*.ts'], + subscribers: ['src/subscribers/**/*.ts'], + cli: { + entitiesDir: 'src/entities', + migrationsDir: 'src/migrations', + subscribersDir: 'src/subscribers', + }, +}; + +module.exports = process.env.NODE_ENV === 'test' ? testConfig : devConfig; diff --git a/package.json b/package.json index 87ca304..f24cac8 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,15 @@ "description": "E-commerce backend", "main": "index.js", "scripts": { - "test": "jest --coverage --detectOpenHandles --verbose --runInBand", - "dev": "nodemon src/index.ts", + "test": "cross-env APP_ENV=test jest --coverage --detectOpenHandles --verbose --runInBand ", + "dev": "cross-env APP_ENV=dev nodemon src/index.ts", "build": "tsc -p .", "start": "node dist/index.js", "lint": "eslint .", "lint:fix": "eslint --fix .", - "format": "prettier --write ." + "format": "prettier --write .", + "typeorm": "typeorm-ts-node-commonjs", + "migration": " npm run typeorm migration:run -- -d ./ormconfig.js" }, "keywords": [], "author": "Scrum master", @@ -19,13 +21,17 @@ "@types/express-winston": "^4.0.0", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", + "bcrypt": "^5.1.1", + "class-validator": "^0.14.1", "cors": "^2.8.5", + "cross-env": "^7.0.3", "dotenv": "^16.4.5", "express": "^4.19.2", - "highlight.js": "^11.9.0", "express-winston": "^4.2.0", + "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", @@ -42,6 +48,7 @@ }, "devDependencies": { "@eslint/js": "^9.1.1", + "@types/bcrypt": "^5.0.2", "@types/body-parser": "^1.19.5", "@types/cors": "^2.8.17", "@types/dotenv": "^8.2.0", @@ -50,8 +57,10 @@ "@types/express": "^4.17.21", "@types/jest": "^29.5.12", "@types/jsend": "^1.0.32", - "@types/node": "^20.12.7", "@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", "@typescript-eslint/eslint-plugin": "^7.7.1", @@ -62,6 +71,7 @@ "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.7.0", + "jest-mock-extended": "^3.0.6", "prettier": "^3.2.5", "supertest": "^7.0.0", "ts-jest": "^29.1.2", diff --git a/src/__test__/route.test.ts b/src/__test__/route.test.ts index 401f5b8..67b28cd 100644 --- a/src/__test__/route.test.ts +++ b/src/__test__/route.test.ts @@ -1,24 +1,118 @@ import request from 'supertest'; -import { app, server } from '../index'; // update this with the path to your app file - -import { createConnection, getConnection, getConnectionOptions } from 'typeorm'; +import { app, server } from '../index'; +import { createConnection, getConnection, getConnectionOptions, getRepository } from 'typeorm'; +import { User } from '../entities/User'; beforeAll(async () => { // Connect to the test database const connectionOptions = await getConnectionOptions(); + await createConnection({ ...connectionOptions, name: 'testConnection' }); }); + afterAll(async () => { - await getConnection('testConnection').close(); + const connection = getConnection('testConnection'); + const userRepository = connection.getRepository(User); + + // Delete all records from the User + await userRepository.clear(); + + // Close the connection to the test database + await connection.close(); + server.close(); }); describe('GET /', () => { - // afterAll(done => { - // server.close(done); - // }); + it('This is a testing route that returns', done => { + request(app) + .get('/api/v1/status') + .expect(200) + .expect('Content-Type', /json/) + .expect( + { + status: 'success', + data: { + code: 200, + message: 'This is a testing route.', + }, + }, + done + ); + }); +}); +describe('POST /user/register', () => { + it('should register a new user', async () => { + // Arrange + const newUser = { + firstName: 'John', + lastName: 'Doe', + email: 'johndoe06@example.com', + password: 'password', + gender: 'Male', + phoneNumber: '123678116', + userType: 'Buyer', + photoUrl: 'https://example.com/photo.jpg', + }; - it('responds with "Knights Ecommerce API"', done => { - request(app).get('/').expect(200, 'Knights Ecommerce API', done); + // Act + const res = await request(app).post('/user/register').send(newUser); + console.log(res.body); + // Assert + expect(res.status).toBe(201); + expect(res.body).toEqual({ + status: 'success', + data: { + code: 201, + 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: 'john.doe1@example.com', + 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); + + 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('

User verified successfully

'); + + // 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); + } + + } + + if (user) { + await userRepository.remove(user); + } + }); +}); \ No newline at end of file diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts new file mode 100644 index 0000000..627db42 --- /dev/null +++ b/src/controllers/authController.ts @@ -0,0 +1,15 @@ +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 { validate } from 'class-validator'; +import { userVerificationService, userRegistrationService } from '../services'; + +export const userRegistration = async (req: Request, res: Response) => { + await userRegistrationService(req, res); +} +export const userVerification = async (req: Request, res: Response) => { + await userVerificationService(req, res); +} + diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 4ba41c9..a03efc6 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,5 +1,3 @@ -// export all controllers -function myFunction (): void { - console.log('Hello'); -} -myFunction(); +import { userRegistration,userVerification } from './authController'; + +export { userRegistration,userVerification }; diff --git a/src/entities/User.ts b/src/entities/User.ts new file mode 100644 index 0000000..f1be411 --- /dev/null +++ b/src/entities/User.ts @@ -0,0 +1,62 @@ +import { Entity, PrimaryGeneratedColumn, Column, Unique, CreateDateColumn, UpdateDateColumn } from 'typeorm'; +import { IsEmail, IsNotEmpty, IsString, IsBoolean, IsIn } from 'class-validator'; + +@Entity() +@Unique(['email']) +export class User { + @PrimaryGeneratedColumn('uuid') + @IsNotEmpty() + id!: string; + + @Column() + @IsNotEmpty() + @IsString() + firstName!: string; + + @Column() + @IsNotEmpty() + @IsString() + lastName!: string; + + @Column() + @IsNotEmpty() + @IsEmail() + email!: string; + + @Column() + @IsNotEmpty() + password!: string; + + @Column() + @IsNotEmpty() + @IsString() + gender!: string; + + @Column() + @IsNotEmpty() + phoneNumber!: string; + + @Column({ nullable: true }) + photoUrl?: string; + + @Column({ default: false }) + @IsNotEmpty() + @IsBoolean() + verified!: boolean; + + @Column({ default: 'active' }) + @IsNotEmpty() + @IsIn(['active', 'suspended']) + status!: 'active' | 'suspended'; + + @Column({ default: 'Buyer' }) + @IsNotEmpty() + @IsIn(['Admin', 'Buyer', 'Vendor']) + userType!: 'Admin' | 'Buyer' | 'Vendor'; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; +} diff --git a/src/index.ts b/src/index.ts index 36b592e..bb5ad3a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import cors from 'cors'; import dotenv from 'dotenv'; import router from './routes'; import { addDocumentation } from './startups/docs'; +import 'reflect-metadata'; import { CustomError, errorHandler } from './middlewares/errorHandler'; import morgan from 'morgan'; @@ -24,6 +25,7 @@ app.all('*', (req: Request, res: Response, next) => { app.use(errorHandler); // Start database connection + dbConnection(); //morgan diff --git a/src/middlewares/errorHandler.ts b/src/middlewares/errorHandler.ts index e4e967f..5e25feb 100644 --- a/src/middlewares/errorHandler.ts +++ b/src/middlewares/errorHandler.ts @@ -1,4 +1,4 @@ -import { Request, Response } from 'express'; +import { NextFunction, Request, Response } from 'express'; class CustomError extends Error { statusCode: number; @@ -12,7 +12,7 @@ class CustomError extends Error { } } -const errorHandler = (err: CustomError, req: Request, res: Response) => { +const errorHandler = (err: CustomError, req: Request, res: Response, next: NextFunction) => { err.statusCode = err.statusCode || 500; err.status = err.status || 'error'; res.status(err.statusCode).json({ diff --git a/src/routes/UserRoutes.ts b/src/routes/UserRoutes.ts new file mode 100644 index 0000000..7fcd3b5 --- /dev/null +++ b/src/routes/UserRoutes.ts @@ -0,0 +1,9 @@ +import { Router } from 'express'; +import { userRegistration, userVerification} from '../controllers/index'; + +const router = Router(); + +router.post('/register', userRegistration); +router.get('/verify/:id', userVerification); + +export default router; diff --git a/src/routes/index.ts b/src/routes/index.ts index 78cb97d..1d95c3d 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,14 +1,13 @@ import { Request, Response, Router } from 'express'; +import userRoutes from './UserRoutes'; import { responseSuccess } from '../utils/response.utils'; + const router = Router(); -router.get("/", (req: Request, res: Response) => { - res.send("Knights Ecommerce API"); -}); + router.get('/api/v1/status', (req: Request, res: Response) => { - return responseSuccess(res, 202, 'This is a testing route that returns: 201'); + return responseSuccess(res, 200, 'This is a testing route.'); }); -// All routes should be imported here and get export after specifying first route -// example router.use("/stock". stockRoutes) =>:: First import stockRoutes and use it here, This shows how the route export will be handled +router.use('/user', userRoutes); export default router; diff --git a/src/services/index.ts b/src/services/index.ts index 73cc501..271665e 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1 +1,3 @@ // export all Services +export * from './userServices/userRegistrationService'; +export * from './userServices/userValidationService'; \ No newline at end of file diff --git a/src/services/userServices/userRegistrationService.ts b/src/services/userServices/userRegistrationService.ts new file mode 100644 index 0000000..9b62908 --- /dev/null +++ b/src/services/userServices/userRegistrationService.ts @@ -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'); + } +}; \ No newline at end of file diff --git a/src/services/userServices/userValidationService.ts b/src/services/userServices/userValidationService.ts new file mode 100644 index 0000000..8d8fea6 --- /dev/null +++ b/src/services/userServices/userValidationService.ts @@ -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('

User verified successfully

'); + +} \ No newline at end of file diff --git a/src/startups/dbConnection.ts b/src/startups/dbConnection.ts index 7fb387c..44887d8 100644 --- a/src/startups/dbConnection.ts +++ b/src/startups/dbConnection.ts @@ -1,9 +1,10 @@ -import { createConnection } from "typeorm"; +import { createConnection } from 'typeorm'; const dbConnection = async () => { try { const connection = await createConnection(); - console.log('Connected to the database'); + console.log(`Connected to the ${process.env.APP_ENV} database`); + return connection; } catch (error) { console.error('Error connecting to the database:', error); } diff --git a/src/utils/response.utils.ts b/src/utils/response.utils.ts index 6f097a7..3be109b 100644 --- a/src/utils/response.utils.ts +++ b/src/utils/response.utils.ts @@ -13,7 +13,7 @@ export const responseSuccess = ( message: string, data?: any ): Response => { - return res.status(200).json( + return res.status(statusCode).json( jsend.success({ code: statusCode, message, @@ -28,7 +28,7 @@ export const responseError = ( message: string, data?: any ): Response => { - return res.status(400).json( + return res.status(statusCode).json( jsend.error({ code: statusCode, message, diff --git a/src/utils/sendMail.ts b/src/utils/sendMail.ts new file mode 100644 index 0000000..11117ab --- /dev/null +++ b/src/utils/sendMail.ts @@ -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: ` + + + + + + +
+

+ Hello ${firstName} ${lastName}, +

+
+

${text}

+

+

${link && `click here to verifie your account`}

+

This message is from: Knights Andela

+
+ +
+ + + ` + + }; + + 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; diff --git a/tsconfig.json b/tsconfig.json index 7d6a779..88326b4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,8 +12,8 @@ "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + "experimentalDecorators": true /* Enable experimental support for legacy experimental decorators. */, + "emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */, // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ @@ -29,7 +29,10 @@ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - "types": ["node", "jest"] /* Specify type package names to be included without being referenced in a source file. */, + "types": [ + "node", + "jest" + ] /* Specify type package names to be included without being referenced in a source file. */, // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */