diff --git a/.env.example b/.env.example index 4ced912..5aa3b0b 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,4 @@ PORT = 3000 DB_CONNECTION = "" --> TODO: put your own connection string here +TEST_DB = ""-->TODO: put your own testing database connection string here +JWT_SECRET = ""-->TODO: put your own jsonwebtoken scret here \ No newline at end of file diff --git a/__test__/user.route.test.ts b/__test__/user.route.test.ts deleted file mode 100644 index 5a3d26e..0000000 --- a/__test__/user.route.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import request from 'supertest'; -import { beforeAll, afterAll, beforeEach, afterEach, test } from '@jest/globals'; -import app from '../src/utils/server'; -import User from '../src/sequelize/models/users'; -import sequelize, { connect } from '../src/config/dbConnection'; - - -describe('Testing User route', () => { - beforeAll(async () => { - try { await - connect(); -} -catch (error) { sequelize.close(); } }, 20000); - -afterAll(async () => { - await sequelize.close(); }); - beforeEach(async () => { - await User.destroy({ truncate: true }); -}); - -test('should return 201 and create a new user when registering successfully', async () => { - const userData = { - name: 'yvanna', - username: 'testuser', - email: 'test1@gmail.com', - password: 'test1234', -}; -const response = await request(app) -.post('/api/v1/users') -.send(userData); -expect(response.status).toBe(201); }, 20000); - -test('should return 409 when registering with an existing email', async () => { await User.create({ - name: 'yvanna', - username: 'testuser', - email: 'test1@gmail.com', - password: 'test1234', - }); - - const userData = { - name: 'yvanna', - username: 'testuser', - email: 'test1@gmail.com', - password: 'test1234', - }; - - const response = await request(app) - .post('/api/v1/users') - .send(userData); - expect(response.status).toBe(409); }, 20000); - -test('should return 500 when registering with an invalid credential', async () => { - const userData = { - email: 'test@mail.com', name: "", username: 'existinguser', }; - const response = await request(app) - .post('/api/v1/users') - .send(userData); - - expect(response.status).toBe(500); }, 20000); }); \ No newline at end of file diff --git a/__test__/user.test.ts b/__test__/user.test.ts index 90ab460..739538e 100644 --- a/__test__/user.test.ts +++ b/__test__/user.test.ts @@ -5,15 +5,54 @@ import User from "../src/sequelize/models/users"; import * as userServices from "../src/services/user.service"; import sequelize, { connect } from "../src/config/dbConnection"; +const userData:any = { + name: 'yvanna', + username: 'testuser', + email: 'test1@gmail.com', + password:'test1234', + }; + + const loginData:any = { + email:'test1@gmail.com', + password:"test1234" + } describe("Testing user Routes", () => { beforeAll(async () => { try { await connect(); + await User.destroy({truncate:true}) } catch (error) { sequelize.close(); } }, 20000); + afterAll(async () => { + await User.destroy({ truncate: true }); + await sequelize.close(); +}); +describe("Testing user authentication", () => { +test('should return 201 and create a new user when registering successfully', async () => { +const response = await request(app) +.post('/api/v1/users/register') +.send(userData); +expect(response.status).toBe(201); }, 20000); + +test('should return 409 when registering with an existing email', async () => { + User.create(userData) + const response = await request(app) + .post('/api/v1/users/register') + .send(userData); + expect(response.status).toBe(409); }, 20000); + +test('should return 500 when registering with an invalid credential', async () => { + const userData = { + email: 'test@mail.com', name: "", username: 'existinguser', }; + const response = await request(app) + .post('/api/v1/users/register') + .send(userData); + + expect(response.status).toBe(500); }, 20000); }); + test("should return all users in db --> given '/api/v1/users'", async () => { const spy = jest.spyOn(User, "findAll"); const spy2 = jest.spyOn(userServices, "getAllUsers"); @@ -21,4 +60,19 @@ describe("Testing user Routes", () => { expect(spy).toHaveBeenCalled(); expect(spy2).toHaveBeenCalled(); }, 20000); -}); + test("Should return status 401 to indicate Unauthorized user",async() =>{ + const loggedInUser ={ + email:userData.email, + password:"test", + }; + const spyonOne = jest.spyOn(User,"findOne").mockResolvedValueOnce({ + //@ts-ignore + email:userData.email, + password:loginData.password, + }); + const response = await request(app).post("/api/v1/users/login") + .send(loggedInUser) + expect(response.body.status).toBe(401); + spyonOne.mockRestore(); + }); +}) diff --git a/package.json b/package.json index 0681afb..e8bd346 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,15 @@ "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", + "cryptr": "^6.3.0", "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..bf37e46 100644 --- a/src/controllers/userControllers.ts +++ b/src/controllers/userControllers.ts @@ -1,11 +1,12 @@ 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) => { try { - // const users = await userService.getAllUsers(); - const users = await userService.getAllUsers(); if (users.length <= 0) { @@ -27,6 +28,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 +75,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..e7f82ec 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(); @@ -29,14 +35,20 @@ const options = { paths: { "/api/v1/users": { - get: getUsers, - post: createUsers + get: getUsers }, + "/api/v1/users/register": { + 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..3b60d5d 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -1,13 +1,15 @@ import { Router } from "express"; import { fetchAllUsers, - createUserController } + createUserController, + userLogin } from "../controllers/userControllers"; const userRoutes = Router(); -userRoutes.get("/", fetchAllUsers); -userRoutes.post("/", createUserController) +userRoutes.get("/", fetchAllUsers); +userRoutes.post('/login',userLogin); +userRoutes.post("/register", createUserController) export default userRoutes; 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..5d15773 100644 --- a/src/utils/env.ts +++ b/src/utils/env.ts @@ -5,4 +5,5 @@ 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, }; 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. */