Skip to content

Commit

Permalink
Merge pull request #19 from atlp-rwanda/ft-user-login-#187419169
Browse files Browse the repository at this point in the history
#187419169 user can login via email and password
  • Loading branch information
teerenzo authored Apr 19, 2024
2 parents f554f6b + 3dd5dda commit b9bd13e
Show file tree
Hide file tree
Showing 13 changed files with 177 additions and 71 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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
59 changes: 0 additions & 59 deletions __test__/user.route.test.ts

This file was deleted.

56 changes: 55 additions & 1 deletion __test__/user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,74 @@ 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: '[email protected]',
password:'test1234',
};

const loginData:any = {
email:'[email protected]',
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: '[email protected]', 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");
const response = await request(app).get("/api/v1/users");
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();
});
})
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
33 changes: 30 additions & 3 deletions src/controllers/userControllers.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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 {
Expand All @@ -48,4 +75,4 @@ export const createUserController = async (req: Request, res: Response) => {
}
res.status(500).json({ error: err });
}
};
};
18 changes: 15 additions & 3 deletions src/docs/swagger.ts
Original file line number Diff line number Diff line change
@@ -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();

Expand Down Expand Up @@ -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: {
Expand Down
32 changes: 32 additions & 0 deletions src/docs/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
}
}
};

4 changes: 4 additions & 0 deletions src/helpers/comparePassword.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import bcrypt from 'bcrypt'
export const comparePasswords = async(plainPassword: string, hashedPassword: string): Promise<boolean> => {
return await bcrypt.compare(plainPassword, hashedPassword);
}
8 changes: 5 additions & 3 deletions src/routes/userRoutes.ts
Original file line number Diff line number Diff line change
@@ -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;
16 changes: 15 additions & 1 deletion src/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<User | null> => {
const existingUser = await User.findOne({ where: { email } });
if (existingUser) {
Expand All @@ -28,4 +42,4 @@ export const createUserService = async (name: string, email: string, username: s
export const getUserByEmail = async (email: string): Promise<User | null> => {
const user = await User.findOne({ where: { email } });
return user;
};
};
1 change: 1 addition & 0 deletions src/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
11 changes: 11 additions & 0 deletions src/utils/jsonwebtoken.ts
Original file line number Diff line number Diff line change
@@ -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;
}

2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down

0 comments on commit b9bd13e

Please sign in to comment.