Skip to content

Commit

Permalink
ft<login> storing token in cookies and adding token validator
Browse files Browse the repository at this point in the history
  • Loading branch information
Ndevu12 committed May 7, 2024
1 parent 70e33ad commit 5f07546
Show file tree
Hide file tree
Showing 15 changed files with 285 additions and 15 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"axios": "^1.6.8",
"bcrypt": "^5.1.1",
"class-validator": "^0.14.1",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"cross-env": "^7.0.3",
"dotenv": "^16.4.5",
Expand Down Expand Up @@ -54,6 +55,7 @@
"@eslint/js": "^9.1.1",
"@types/bcrypt": "^5.0.2",
"@types/body-parser": "^1.19.5",
"@types/cookie-parser": "^1.4.7",
"@types/cors": "^2.8.17",
"@types/dotenv": "^8.2.0",
"@types/eslint": "^8.56.10",
Expand Down
94 changes: 94 additions & 0 deletions src/__test__/isValide.test.ts
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

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

'userRepository' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 33 in src/__test__/isValide.test.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

'userRepository' is assigned a value but never used

Check warning on line 33 in src/__test__/isValide.test.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

'userRepository' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 33 in src/__test__/isValide.test.ts

View workflow job for this annotation

GitHub Actions / build-lint-test-coverage

'userRepository' is assigned a value but never used
// 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' });
});
});
74 changes: 74 additions & 0 deletions src/__test__/logout.test.ts
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' });
});
});
2 changes: 1 addition & 1 deletion src/__test__/userServices.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ describe('start2FAProcess', () => {
const res = await request(app).post('/user/resend-otp').send(data);
expect(res.status).toBe(200);
expect(res.body).toEqual({ status: 'success', data: { message: 'OTP sent successfully' } });
});
}, 10000);

it('should return 400 if not sent email in body on login', async () => {
const data = {};
Expand Down
17 changes: 13 additions & 4 deletions src/controllers/authController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
userDisableTwoFactorAuth,
userValidateOTP,
userResendOtpService,
logoutService,
} from '../services';
import { userPasswordResetService } from '../services/userServices/userPasswordResetService';
import { sendPasswordResetLinkService } from '../services/userServices/sendResetPasswordLinkService';
Expand Down Expand Up @@ -38,9 +39,17 @@ export const verifyOTP = async (req: Request, res: Response) => {
export const resendOTP = async (req: Request, res: Response) => {
await userResendOtpService(req, res);
};

export const sampleAPI = async (req: Request, res: Response) => {
res.status(200).json({ message: 'Token is valid' });
};
export const userPasswordReset = async (req: Request, res: Response) => {
await userPasswordResetService(req, res);
}
await userPasswordResetService(req, res);
};
export const sendPasswordResetLink = async (req: Request, res: Response) => {
await sendPasswordResetLinkService(req, res);
}
await sendPasswordResetLinkService(req, res);
};

export const logout = async (req: Request, res: Response) => {
await logoutService(req, res);
};
19 changes: 19 additions & 0 deletions src/helper/verify.ts
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;
}
};
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import dotenv from 'dotenv';
import router from './routes';
import { addDocumentation } from './startups/docs';
import 'reflect-metadata';
import cookieParser from 'cookie-parser';

import { CustomError, errorHandler } from './middlewares/errorHandler';
import morgan from 'morgan';
Expand All @@ -13,7 +14,7 @@ dotenv.config();
export const app = express();
const port = process.env.PORT || 8000;
app.use(express.json());

app.use(cookieParser());
app.use(cors({ origin: '*' }));
app.use(router);
addDocumentation(app);
Expand Down
1 change: 1 addition & 0 deletions src/middlewares/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./errorHandler"
export * from './roleCheck';
export * from './isValid';
33 changes: 33 additions & 0 deletions src/middlewares/isValid.ts
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;
}
};
25 changes: 19 additions & 6 deletions src/routes/UserRoutes.ts
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;
1 change: 1 addition & 0 deletions src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './userServices/userDisableTwoFactorAuth';
export * from './userServices/userValidateOTP';
export * from './userServices/userLoginService';
export * from './userServices/userResendOTP';
export * from './userServices/logoutServices';
18 changes: 18 additions & 0 deletions src/services/userServices/logoutServices.ts
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.' });
}
};
7 changes: 7 additions & 0 deletions src/services/userServices/userLoginService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ export const userLoginService = async (req: Request, res: Response) => {
process.env.JWT_SECRET as string,
{ expiresIn: '24h' }
);

if (process.env.APP_ENV === 'production') {
res.cookie('token', token, { httpOnly: true, sameSite: false, secure: true });
} else {
res.cookie('token', token, { httpOnly: true, sameSite: 'lax', secure: false });
}

return res.status(200).json({
status: 'success',
data: {
Expand Down
1 change: 0 additions & 1 deletion src/services/userServices/userSendOTPEmail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export const sendOTPEmail = async (subject: string, email: string, content: any)

try {
const info = await transporter.sendMail(mailOptions);
console.log('Message sent: %s', info.messageId);
} catch (error) {
console.log('Error occurred while sending email', error);
}
Expand Down
Loading

0 comments on commit 5f07546

Please sign in to comment.