diff --git a/__test__/user.test.ts b/__test__/user.test.ts index 90ab460..bcb268d 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/users"; 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 0681afb..9bf345e 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,10 @@ "@eslint/js": "^9.0.0", "@types/bcryptjs": "^2.4.6", "@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", @@ -43,11 +45,14 @@ "typescript-eslint": "^7.7.0" }, "dependencies": { + "@types/bcrypt": "^5.0.2", + "bcrypt": "^5.1.1", "bcryptjs": "^2.4.3", "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 9e46e75..47a61f5 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"; import { createUserService, getUserByEmail } from "../services/user.service"; export const fetchAllUsers = async (req: Request, res: Response) => { @@ -27,6 +30,32 @@ 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 + }); + }; + }; +}; + export const createUserController = async (req: Request, res: Response) => { try { @@ -48,4 +77,4 @@ export const createUserController = async (req: Request, res: Response) => { } res.status(500).json({ error: err }); } -}; \ No newline at end of file +}; diff --git a/src/docs/swagger.ts b/src/docs/swagger.ts index 8b172de..15c2d3f 100644 --- a/src/docs/swagger.ts +++ b/src/docs/swagger.ts @@ -1,7 +1,13 @@ import express from "express"; import { serve, setup } from "swagger-ui-express"; import { env } from "../utils/env"; -import { createUsers, getUsers, userSchema } from "./users"; +import { + createUsers, + getUsers, + loginAsUser, + userSchema, + loginSchema + } from "./users"; const docRouter = express.Router(); @@ -32,11 +38,15 @@ const options = { get: getUsers, post: createUsers }, + "/api/v1/users/login": { + post: loginAsUser + }, }, components: { schemas: { User: userSchema, + Login:loginSchema, }, securitySchemes: { bearerAuth: { diff --git a/src/docs/users.ts b/src/docs/users.ts index 9888ae9..ca2f301 100644 --- a/src/docs/users.ts +++ b/src/docs/users.ts @@ -19,6 +19,18 @@ export const userSchema = { }, } +export const loginSchema ={ + properties :{ + email: { + type: "string", + format: "email", + }, + password: { + type: "string", + }, + } +} + export const getUsers = { tags: ["Users"], summary: "Get all users", @@ -69,4 +81,24 @@ export const getUsers = { }, }, } + + export const loginAsUser ={ + tags: ["Users"], + summary: "Login as user", + requestBody: { + required: true, + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/Login" + } + } + } + }, + responses: { + 200: { + description: "OK", + } + } + }; \ No newline at end of file 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 2704bc3..b327bab 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -1,12 +1,14 @@ import { Router } from "express"; import { fetchAllUsers, - createUserController } + createUserController, + userLogin } from "../controllers/userControllers"; const userRoutes = Router(); -userRoutes.get("/", fetchAllUsers); +userRoutes.get("/", fetchAllUsers); +userRoutes.post('/login',userLogin); userRoutes.post("/", createUserController) diff --git a/src/services/user.service.ts b/src/services/user.service.ts index e44f2bf..0f12e3d 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -15,6 +15,20 @@ export const getAllUsers = async () => { } }; +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); +}; +}; export const createUserService = async (name: string, email: string, username: string, password: string): Promise => { const existingUser = await User.findOne({ where: { email } }); if (existingUser) { @@ -28,4 +42,4 @@ export const createUserService = async (name: string, email: string, username: s export const getUserByEmail = async (email: string): Promise => { const user = await User.findOne({ where: { email } }); return user; -}; \ No newline at end of file +}; 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. */