-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ft<login> storing token in cookies and adding token validator
- Loading branch information
Showing
15 changed files
with
285 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { NextFunction, Request, Response } from 'express'; | ||
import { dbConnection } from '../startups/dbConnection'; | ||
import { getConnection } from 'typeorm'; | ||
import { User } from '../entities/User'; | ||
import { v4 as uuid } from 'uuid'; | ||
import jwt from 'jsonwebtoken'; | ||
import { isTokenValide } from '../middlewares'; | ||
|
||
jest.mock('../utils/response.utils'); | ||
let reqMock: Partial<Request>; | ||
let resMock: Partial<Response>; | ||
let nextMock: NextFunction; | ||
|
||
const activeUserId = uuid(); | ||
|
||
beforeAll(async () => { | ||
const connection = await dbConnection(); | ||
const userRepository = connection?.getRepository(User); | ||
|
||
const activeUser = new User(); | ||
activeUser.id = activeUserId; | ||
activeUser.firstName = 'John2'; | ||
activeUser.lastName = 'Doe'; | ||
activeUser.email = 'active.doe@example.com'; | ||
activeUser.password = 'password'; | ||
activeUser.gender = 'Male'; | ||
activeUser.phoneNumber = '12347'; | ||
activeUser.photoUrl = 'https://example.com/photo.jpg'; | ||
await userRepository?.save(activeUser); | ||
}); | ||
afterAll(async () => { | ||
const connection = getConnection(); | ||
const userRepository = connection.getRepository(User); | ||
Check warning on line 33 in src/__test__/isValide.test.ts GitHub Actions / build-lint-test-coverage
Check warning on line 33 in src/__test__/isValide.test.ts GitHub Actions / build-lint-test-coverage
Check warning on line 33 in src/__test__/isValide.test.ts GitHub Actions / build-lint-test-coverage
|
||
// Close the connection to the test database | ||
await connection.close(); | ||
}); | ||
|
||
const data = { | ||
userType: 'Buyer', | ||
id: activeUserId, | ||
email: 'active.doe@example.com', | ||
}; | ||
describe('Middleware - check user token', () => { | ||
beforeEach(() => { | ||
reqMock = {}; | ||
resMock = { | ||
status: jest.fn().mockReturnThis(), | ||
json: jest.fn(), | ||
}; | ||
nextMock = jest.fn(); | ||
}); | ||
|
||
it('should validate a token', async () => { | ||
const token = jwt.sign(data, process.env.JWT_SECRET as string, { expiresIn: '1h' }); | ||
|
||
// Mock the request | ||
reqMock = { | ||
cookies: { | ||
token: token, | ||
}, | ||
body: { | ||
email: 'active.doe@example.com', | ||
}, | ||
}; | ||
|
||
await isTokenValide(reqMock as Request, resMock as Response, nextMock); | ||
|
||
expect(nextMock).toHaveBeenCalled(); | ||
}); | ||
|
||
it('should return 401 if user is not found', async () => { | ||
reqMock = { | ||
cookies: { | ||
token: jwt.sign({ id: uuid(), email: 'nonexistent@example.com' }, process.env.JWT_SECRET as string, { | ||
expiresIn: '1h', | ||
}), | ||
}, | ||
}; | ||
|
||
await isTokenValide(reqMock as Request, resMock as Response, nextMock); | ||
|
||
expect(resMock.status).toHaveBeenCalledWith(404); | ||
expect(resMock.json).toHaveBeenCalledWith({ Message: 'User not found' }); | ||
}); | ||
|
||
it('should return 401 if no token is provided', async () => { | ||
reqMock = { cookies: {} }; | ||
|
||
await isTokenValide(reqMock as Request, resMock as Response, nextMock); | ||
|
||
expect(resMock.status).toHaveBeenCalledWith(401); | ||
expect(resMock.json).toHaveBeenCalledWith({ Message: 'Sorry, You are not authorized' }); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import request from 'supertest'; | ||
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(); | ||
server.close(); | ||
}); | ||
|
||
describe('POST /user/logout', () => { | ||
it('should logout a user', async () => { | ||
// sign up a user | ||
const registerUser = { | ||
firstName: 'Ndevu', | ||
lastName: 'Elisa', | ||
email: 'ndevukumurindi@gmail.com', | ||
gender: 'male', | ||
phoneNumber: '078907987443', | ||
photoUrl: 'https://example.com/images/photo.jpg', | ||
userType: 'vender', | ||
verified: true, | ||
status: 'active', | ||
password: process.env.TEST_USER_LOGIN_PASS, | ||
}; | ||
|
||
await request(app).post('/user/register').send(registerUser); | ||
|
||
const loginUser = { | ||
email: registerUser.email, | ||
password: process.env.TEST_USER_LOGIN_PASS, | ||
}; | ||
|
||
const userRepository = getRepository(User); | ||
const user = await userRepository.findOne({ where: { email: registerUser.email } }); | ||
if (user) { | ||
const verifyRes = await request(app).get(`/user/verify/${user.id}`); | ||
|
||
if (!verifyRes) throw new Error(`Test User verification failed for ${user.email}`); | ||
|
||
const loginResponse = await request(app).post('/user/login').send(loginUser); | ||
const setCookie = loginResponse.headers['set-cookie']; | ||
|
||
if (!setCookie) { | ||
throw new Error('No cookies set in login response'); | ||
} | ||
|
||
const resp = await request(app).post('/user/logout').set('Cookie', setCookie); | ||
expect(resp.status).toBe(200); | ||
expect(resp.body).toEqual({ Message: 'Logged out successfully' }); | ||
|
||
// Clean up: delete the test user | ||
await userRepository.remove(user); | ||
} | ||
}); | ||
|
||
it('should not logout a user who is not logged in or with no token', async () => { | ||
const fakeEmail = 'ndevukkkk@gmail.com'; | ||
const loginUser = { | ||
email: fakeEmail, | ||
password: process.env.TEST_USER_LOGIN_PASS, | ||
}; | ||
const token = ''; | ||
const res = await request(app).post('/user/logout').send(loginUser).set('Cookie', token); | ||
expect(res.status).toBe(400); | ||
expect(res.body).toEqual({ Message: 'Access denied. You must be logged in' }); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import jwt from 'jsonwebtoken'; | ||
import dotenv from 'dotenv'; | ||
|
||
dotenv.config(); | ||
|
||
const jwtSecretKey = process.env.JWT_SECRET; | ||
|
||
if (!jwtSecretKey) { | ||
throw new Error('JWT_SECRET is not defined in the environment variables.'); | ||
} | ||
|
||
export const verifiedToken = (token: string): any => { | ||
try { | ||
return jwt.verify(token, jwtSecretKey); | ||
} catch (err) { | ||
console.log('ERROR: jwt must be provided'); | ||
return null; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export * from "./errorHandler" | ||
export * from './roleCheck'; | ||
export * from './isValid'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { Request, Response, NextFunction, RequestHandler } from 'express'; | ||
import { verifiedToken } from '../helper/verify'; | ||
import { getRepository } from 'typeorm'; | ||
import { User } from '../entities/User'; | ||
|
||
export interface DecodedUser { | ||
userType: string; | ||
id: string; | ||
email: string; | ||
} | ||
|
||
export const isTokenValide: RequestHandler = async (req: Request, res: Response, next: NextFunction): Promise<void> => { | ||
try { | ||
const token = req.cookies.token; | ||
const userPaylod = verifiedToken(token); | ||
if (!userPaylod) { | ||
res.status(401).json({ Message: 'Sorry, You are not authorized' }); | ||
return; | ||
} | ||
const userRepository = getRepository(User); | ||
const user = await userRepository.findOne({ where: { id: userPaylod.id } }); | ||
if (!user) { | ||
res.status(404).json({ Message: 'User not found' }); | ||
return; | ||
} | ||
req.user = user; | ||
return next(); | ||
} catch (error) { | ||
console.error('Error in token Validation middleware:\n', error); | ||
res.status(401).json({ Message: 'Sorry, Something went wrong' }); | ||
return; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,32 @@ | ||
import { Router } from 'express'; | ||
import { disable2FA, enable2FA, login, resendOTP, sendPasswordResetLink, userPasswordReset , userRegistration, userVerification, verifyOTP} from '../controllers'; | ||
|
||
|
||
|
||
import { | ||
disable2FA, | ||
enable2FA, | ||
login, | ||
resendOTP, | ||
sendPasswordResetLink, | ||
userPasswordReset, | ||
userRegistration, | ||
userVerification, | ||
verifyOTP, | ||
sampleAPI, | ||
logout, | ||
} from '../controllers'; | ||
import { isTokenValide } from '../middlewares/index'; | ||
|
||
const router = Router(); | ||
|
||
router.post('/register', userRegistration); | ||
router.get('/verify/:id', userVerification); | ||
router.post('/login', login); | ||
router.post('/logout', logout); | ||
router.post('/enable-2fa', enable2FA); | ||
router.post('/disable-2fa', disable2FA); | ||
router.post('/verify-otp', verifyOTP); | ||
router.post('/resend-otp', resendOTP); | ||
router.post("/password/reset", userPasswordReset); | ||
router.post("/password/reset/link", sendPasswordResetLink); | ||
// sample usage of isValide middleware. it validate a token and permit to continue if token is valide | ||
router.get('/verifyMiddleware', isTokenValide, sampleAPI); | ||
router.post('/password/reset', userPasswordReset); | ||
router.post('/password/reset/link', sendPasswordResetLink); | ||
|
||
export default router; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { Request, Response } from 'express'; | ||
|
||
// logout method | ||
export const logoutService = async (req: Request, res: Response): Promise<void> => { | ||
try { | ||
const token = req.cookies['token'] || null; | ||
if (!token) { | ||
res.status(400).json({ Message: 'Access denied. You must be logged in' }); | ||
return; | ||
} | ||
|
||
res.clearCookie('token'); | ||
res.status(200).json({ Message: 'Logged out successfully' }); | ||
} catch (error) { | ||
console.error('Error logging out:', error); | ||
res.status(500).json({ error: 'Sorry, Token required.' }); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.