From 12ca45a0c48102977a2dc3d495065864a60966f6 Mon Sep 17 00:00:00 2001 From: gracemugwanezak <107988849+gracemugwanezak@users.noreply.github.com> Date: Mon, 29 Apr 2024 16:05:27 +0200 Subject: [PATCH] finishes 187354245 logout api --- src/controllers/authController.ts | 44 +++++++++------ src/controllers/logoutController.ts | 41 ++++++++++++++ ...20240428210949-create-blacklisted-token.js | 28 ++++++++++ src/database/models/blackListedToken.ts | 33 ++++++++++++ src/docs/auth.yaml | 54 +++++++++++++++++-- src/helpers/tokenExtractor.ts | 16 ++++++ src/routes/authRoute.ts | 6 +++ 7 files changed, 202 insertions(+), 20 deletions(-) create mode 100644 src/controllers/logoutController.ts create mode 100644 src/database/migrations/20240428210949-create-blacklisted-token.js create mode 100644 src/database/models/blackListedToken.ts create mode 100644 src/helpers/tokenExtractor.ts diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index e601588c..d135ac72 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -5,6 +5,7 @@ import bcrypt from 'bcrypt'; import User, { UserAttributes } from '../database/models/user'; import { sendInternalErrorResponse, validateFields } from '../validations'; import logger from '../logs/config'; +import { extractTokenMiddleware } from '../helpers/tokenExtractor'; const authenticateViaGoogle = (req: Request, res: Response, next: NextFunction) => { passport.authenticate('google', (err: unknown, user: UserAttributes | null) => { @@ -17,14 +18,7 @@ const authenticateViaGoogle = (req: Request, res: Response, next: NextFunction) return; } - const token = jwt.sign({ id: user.id }, process.env.SECRET_KEY as string, { - expiresIn: process.env.JWT_EXPIRATION as string, - }); - - res.status(200).json({ - ok: true, - token: token, - }); + generateAndSendToken(user.id, res, req); // Pass req here })(req, res, next); }; @@ -78,15 +72,7 @@ const login = async (req: Request, res: Response): Promise => { return; } - // Authenticate user with jwt - const token = jwt.sign({ id: user.id }, process.env.SECRET_KEY as string, { - expiresIn: process.env.JWT_EXPIRATION as string, - }); - - res.status(200).json({ - ok: true, - token: token, - }); + generateAndSendToken(user.id, res, req); } catch (err: any) { const message = (err as Error).message; logger.error(message); @@ -94,4 +80,28 @@ const login = async (req: Request, res: Response): Promise => { } }; +const generateAndSendToken = (userId: string, res: Response, req: Request) => { + extractTokenMiddleware(req, res, () => { + // Pass the req object here + const token = (res as any).token; + jwt.verify(token, process.env.JWT_SECRET as string, (err: any) => { + if (err) { + res.status(401).json({ + ok: false, + message: 'Invalid token', + }); + return; + } + const generatedToken = jwt.sign({ id: userId }, process.env.SECRET_KEY as string, { + expiresIn: process.env.JWT_EXPIRATION as string, + }); + + res.status(200).json({ + ok: true, + token: generatedToken, + }); + }); + }); +}; + export { login, authenticateViaGoogle }; diff --git a/src/controllers/logoutController.ts b/src/controllers/logoutController.ts new file mode 100644 index 00000000..de03637d --- /dev/null +++ b/src/controllers/logoutController.ts @@ -0,0 +1,41 @@ +import { Request, Response } from 'express'; +import jwt from 'jsonwebtoken'; +import { sendInternalErrorResponse } from '../validations'; +import logger from '../logs/config'; +import BlacklistedToken from '../database/models/blackListedToken'; +import { extractTokenMiddleware } from '../helpers/tokenExtractor'; // Import the middleware + +const logout = async (req: Request, res: Response): Promise => { + try { + extractTokenMiddleware(req, res, () => { + const token = (req as any).token; + + jwt.verify(token, process.env.JWT_SECRET as string, (err: any) => { + if (err) { + res.status(401).json({ + ok: false, + message: 'Invalid token', + }); + return; + } + + BlacklistedToken.create({ token }) + .then(() => { + res.status(200).json({ + ok: true, + message: 'Logged out successfully', + }); + }) + .catch(error => { + throw error; + }); + }); + }); + } catch (err: any) { + const message = (err as Error).message; + logger.error(message); + sendInternalErrorResponse(res, err); + } +}; + +export { logout, extractTokenMiddleware }; diff --git a/src/database/migrations/20240428210949-create-blacklisted-token.js b/src/database/migrations/20240428210949-create-blacklisted-token.js new file mode 100644 index 00000000..8d06d111 --- /dev/null +++ b/src/database/migrations/20240428210949-create-blacklisted-token.js @@ -0,0 +1,28 @@ +'use strict'; +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('BlacklistedTokens', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER, + }, + token: { + type: Sequelize.STRING, + allowNull: false, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('BlacklistedTokens'); + }, +}; diff --git a/src/database/models/blackListedToken.ts b/src/database/models/blackListedToken.ts new file mode 100644 index 00000000..9131eb67 --- /dev/null +++ b/src/database/models/blackListedToken.ts @@ -0,0 +1,33 @@ +import { Model, DataTypes, Sequelize } from 'sequelize'; +import sequelize from './index'; + +class BlacklistedToken extends Model { + public id!: number; + public token!: string; + + // timestamps! + public readonly createdAt!: Date; + public readonly updatedAt!: Date; +} + +BlacklistedToken.init( + { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + token: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + sequelize: sequelize as Sequelize, + modelName: 'BlacklistedToken', + tableName: 'blacklisted_tokens', + timestamps: true, + } +); + +export default BlacklistedToken; diff --git a/src/docs/auth.yaml b/src/docs/auth.yaml index 7e86524c..d3ed82f6 100644 --- a/src/docs/auth.yaml +++ b/src/docs/auth.yaml @@ -53,6 +53,54 @@ paths: 500: description: Internal server error -tags: - - name: Login - description: Login a user + + /auth/logout: + post: + summary: Logout User + tags: + - Logout + description: Invalidates the authentication token of a logged-in user, effectively logging them out. + consumes: + - application/json + produces: + - application/json + parameters: + - in: header + name: Authorization + description: Bearer token obtained during login + required: true + type: string + responses: + '200': + description: User logged out successfully + schema: + type: object + properties: + success: + type: boolean + example: true + message: + type: string + example: User logged out successfully + '400': + description: Bad request, token not provided + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: Token not provided + '401': + description: Unauthorized, invalid or expired token + schema: + type: object + properties: + success: + type: boolean + example: false + error: + type: string + example: Invalid or expired token \ No newline at end of file diff --git a/src/helpers/tokenExtractor.ts b/src/helpers/tokenExtractor.ts new file mode 100644 index 00000000..8fb13090 --- /dev/null +++ b/src/helpers/tokenExtractor.ts @@ -0,0 +1,16 @@ +import { Request, Response, NextFunction } from 'express'; + +const extractTokenMiddleware = (req: Request, res: Response, next: NextFunction) => { + const token = req.headers.authorization?.split(' ')[1]; + if (!token) { + res.status(400).json({ + ok: false, + message: 'Token not provided', + }); + return; + } + (req as any).token = token; + next(); +}; + +export { extractTokenMiddleware }; diff --git a/src/routes/authRoute.ts b/src/routes/authRoute.ts index d6c7fdc4..cdaab9ea 100644 --- a/src/routes/authRoute.ts +++ b/src/routes/authRoute.ts @@ -2,6 +2,8 @@ import { Router } from 'express'; import passport from 'passport'; import { authenticateViaGoogle } from '../controllers/authController'; import { login } from '../controllers/authController'; +import { logout } from '../controllers/logoutController'; +import { signupUser } from '../controllers/userController'; const router = Router(); // redirect user to google for authentication @@ -15,7 +17,11 @@ router.get( // authenticated user and store user data router.get('/google/callback', authenticateViaGoogle); +//signup a user +router.post('/signup', signupUser); + // Route to login a user router.post('/login', login); +router.post('/logout', logout); export default router;