From 7002dc701b9025b9b59b1b7976bad183ecfb8596 Mon Sep 17 00:00:00 2001 From: niyobertin Date: Fri, 19 Apr 2024 11:14:09 +0200 Subject: [PATCH] ft(login) user can log into the E-commerce application via email & password -user login with email and password and return jwt as result of successifull login -unit test for user login endpont both successifull and unsuccessfull login event -swagger documentation for user login endpoint [Finishes #187419169] --- __test__/user.test.ts | 32 ++++++++++++++++++++++++++++++ package.json | 5 +++++ src/controllers/userControllers.ts | 30 ++++++++++++++++++++++++++++ src/helpers/comparePassword.ts | 4 ++++ src/routes/userRoutes.ts | 3 ++- src/services/user.service.ts | 15 ++++++++++++++ src/utils/env.ts | 5 +++++ src/utils/jsonwebtoken.ts | 11 ++++++++++ tsconfig.json | 2 +- 9 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 src/helpers/comparePassword.ts create mode 100644 src/utils/jsonwebtoken.ts diff --git a/__test__/user.test.ts b/__test__/user.test.ts index e6490e1..aee6fcf 100644 --- a/__test__/user.test.ts +++ b/__test__/user.test.ts @@ -4,6 +4,7 @@ import app from "../src/utils/server"; import User from "../src/sequelize/models/user"; import * as userServices from "../src/services/user.service"; import sequelize, { connect } from "../src/config/dbConnection"; +import {env} from "../src/utils/env"; describe("Testing user Routes", () => { beforeAll(async () => { @@ -21,4 +22,35 @@ describe("Testing user Routes", () => { expect(spy).toHaveBeenCalled(); expect(spy2).toHaveBeenCalled(); }, 20000); + + test("Should return status 200 to indicate that user logged in ",async() =>{ + const loggedInUser ={ + email:env.email, + password:env.test_password, + }; + const spyonOne = jest.spyOn(User,"findOne").mockResolvedValueOnce({ + //@ts-ignore + email:env.email, + password:env.hashed_password, + }); + const response = await request(app).post("/api/v1/users/login") + .send(loggedInUser) + expect(response.status).toBe(200); + spyonOne.mockRestore(); + }) + test("Should return status 401 to indicate Unauthorized user",async() =>{ + const loggedInUser ={ + email:env.email, + password:env.test_incorrect_password, + }; + const spyonOne = jest.spyOn(User,"findOne").mockResolvedValueOnce({ + //@ts-ignore + email:env.email, + password:env.hashed_password, + }); + const response = await request(app).post("/api/v1/users/login") + .send(loggedInUser) + expect(response.status).toBe(401); + spyonOne.mockRestore(); + }); }); diff --git a/package.json b/package.json index 70d022b..294a97e 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,10 @@ "license": "MIT", "devDependencies": { "@types/cors": "^2.8.17", + "@types/dotenv": "^8.2.0", "@types/express": "^4.17.21", "@types/jest": "^29.5.12", + "@types/jsonwebtoken": "^9.0.6", "@types/node": "^20.12.7", "@types/supertest": "^6.0.2", "@types/swagger-ui-express": "^4.1.6", @@ -39,10 +41,13 @@ "typescript": "^5.4.5" }, "dependencies": { + "@types/bcrypt": "^5.0.2", + "bcrypt": "^5.1.1", "cors": "^2.8.5", "cross-env": "^7.0.3", "dotenv": "^16.4.5", "express": "^4.19.2", + "jsonwebtoken": "^9.0.2", "path": "^0.12.7", "pg": "^8.11.5", "pg-hstore": "^2.3.4", diff --git a/src/controllers/userControllers.ts b/src/controllers/userControllers.ts index 7f5ced5..42d066e 100644 --- a/src/controllers/userControllers.ts +++ b/src/controllers/userControllers.ts @@ -1,5 +1,8 @@ import { Request, Response } from "express"; import * as userService from "../services/user.service"; +import { generateToken } from "../utils/jsonwebtoken"; +import { comparePasswords } from "../helpers/comparePassword"; +import { loggedInUser} from "../services/user.service"; export const fetchAllUsers = async (req: Request, res: Response) => { try { @@ -25,3 +28,30 @@ export const fetchAllUsers = async (req: Request, res: Response) => { }); } }; + +export const userLogin = async(req:Request,res:Response) =>{ + const {email, password} = req.body; + const user = await loggedInUser(email); + const accessToken = await generateToken(user); + if(!user){ + res.status(404).json({ + status:404, + message:'User Not Found ! Please Register new ancount' + }); + }else{ + const match = await comparePasswords(password,user.password); + if(!match){ + res.status(401).json({ + status:401, + message:' User email or password is incorrect!' + }); + }else{ + res.status(200).json({ + status:200, + message:"Logged in", + token:accessToken + }); + }; + }; +}; + diff --git a/src/helpers/comparePassword.ts b/src/helpers/comparePassword.ts new file mode 100644 index 0000000..79654ba --- /dev/null +++ b/src/helpers/comparePassword.ts @@ -0,0 +1,4 @@ +import bcrypt from 'bcrypt' +export const comparePasswords = async(plainPassword: string, hashedPassword: string): Promise => { + return await bcrypt.compare(plainPassword, hashedPassword); + } \ No newline at end of file diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index ec2abd9..ea499fc 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -1,8 +1,9 @@ import { Router } from "express"; -import { fetchAllUsers } from "../controllers/userControllers"; +import { fetchAllUsers,userLogin } from "../controllers/userControllers"; const userRoutes = Router(); userRoutes.get("/", fetchAllUsers); +userRoutes.post('/login',userLogin); export default userRoutes; diff --git a/src/services/user.service.ts b/src/services/user.service.ts index ee850c9..06ec779 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -13,3 +13,18 @@ export const getAllUsers = async () => { throw new Error(error.message); } }; + +export const loggedInUser = async(email:string) => { + try{ + const user:any = await User.findOne({ + where: { email: email } + }); + if(!user){ + return false; + }else{ + return user; + } +}catch(err:any){ + throw new Error(err.message); +}; +}; diff --git a/src/utils/env.ts b/src/utils/env.ts index 33e5f09..0fe9208 100644 --- a/src/utils/env.ts +++ b/src/utils/env.ts @@ -5,4 +5,9 @@ export const env = { port: process.env.PORT || 3000, db_url: process.env.DB_CONNECTION as string, test_db_url: process.env.TEST_DB as string, + jwt_secret:process.env.JWT_SECRET, + email:process.env.TESR_EMAIL, + test_password:process.env.TEST_PASSWORD, + test_incorrect_password:process.env.TEST_INCORRECT_PASSWORD, + hashed_password:process.env.HASHED_PASSWORD }; diff --git a/src/utils/jsonwebtoken.ts b/src/utils/jsonwebtoken.ts new file mode 100644 index 0000000..53e9af0 --- /dev/null +++ b/src/utils/jsonwebtoken.ts @@ -0,0 +1,11 @@ +import { IUser } from "../types"; +import { env } from "../utils/env"; +import { sign,verify } from "jsonwebtoken"; + +export const generateToken = async(user:IUser) =>{ + const accessToken = sign({email:user.email,password:user.password}, + `${env.jwt_secret}`,{expiresIn:'72h'} + ); + return accessToken; +} + diff --git a/tsconfig.json b/tsconfig.json index a56cef6..b003173 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,7 +31,7 @@ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "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'. */ + "typeRoots": ["node_modules/@types", "./typings"], /* Specify multiple folders that act like './node_modules/@types'. */ // "types": [], /* 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. */