Skip to content

Commit

Permalink
chore: create logout developer endpoint; add test #751 (#752)
Browse files Browse the repository at this point in the history
* chore: create logout developer endpoint; add test #751

* chore: add logout business logic #751

* Update src/middleware/authenticated.ts

Co-authored-by: Ijemma Onwuzulike <[email protected]>

* Update src/middleware/authenticated.ts

Co-authored-by: Ijemma Onwuzulike <[email protected]>

* Update src/middleware/authenticated.ts

Co-authored-by: Ijemma Onwuzulike <[email protected]>

* Update src/routers/router.ts

Co-authored-by: Ijemma Onwuzulike <[email protected]>

* chore: sync updates #751

---------

Co-authored-by: Ijemma Onwuzulike <[email protected]>
  • Loading branch information
davydocsurg and ijemmao authored Oct 5, 2023
1 parent 7a3f647 commit cc81be4
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 30 deletions.
48 changes: 44 additions & 4 deletions __tests__/__mocks__/documentData.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,55 @@ const developerData = {
password: 'password',
};

// Generate a unique name using a UUID without hyphens
const generateUniqueName = () => uuid().replace(/-/g, '');

const generateUniqueEmail = () => {
// Generate a unique email using a UUID without hyphens and without numbers
const email = `${uuid().replace(/-/g, '')}@testing.com`;
// Remove numbers from the email
return email.replace(/\d/g, '');
};

// Generate a unique password using a UUID without hyphens
const generateUniquePassword = () => uuid().replace(/-/g, '');

const newDeveloperData = {
name: `${uuid().replace(/-/g, '')}`,
email: `${uuid().replace(/-/g, '')}@testing.com`,
password: `${uuid()}`,
name: generateUniqueName(),
email: generateUniqueEmail(),
password: generateUniquePassword(),
};

const developerOneData = {
name: generateUniqueName(),
email: generateUniqueEmail(),
password: generateUniquePassword(),
};

const developerTwoData = {
name: generateUniqueName(),
email: generateUniqueEmail(),
password: generateUniquePassword(),
};

const anotherDeveloperData = {
name: generateUniqueName(),
email: generateUniqueEmail(),
password: generateUniquePassword(),
};

const malformedDeveloperData = {
email: '[email protected]',
password: 'password',
};

export { wordId, exampleId, developerData, newDeveloperData, malformedDeveloperData };
export {
wordId,
exampleId,
developerData,
newDeveloperData,
developerOneData,
developerTwoData,
anotherDeveloperData,
malformedDeveloperData,
};
55 changes: 51 additions & 4 deletions __tests__/auth.test.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,64 @@
import { newDeveloperData } from './__mocks__/documentData';
import { createDeveloper, loginDeveloper } from './shared/commands';
import { anotherDeveloperData, developerOneData, newDeveloperData } from './__mocks__/documentData';
import { createDeveloper, loginDeveloper, logoutDeveloper } from './shared/commands';

describe('login', () => {
it('should successfully log a developer in', async () => {
await createDeveloper(newDeveloperData);
const developer = await createDeveloper(newDeveloperData);
expect(developer.status).toEqual(200);

const data = {
email: newDeveloperData.email,
password: newDeveloperData.password,
};

const loginRes = await loginDeveloper(data);

expect(loginRes.status).toEqual(200);
expect(loginRes.body.developer).toMatchObject(loginRes.body.developer);
});

it('should not log a developer in with an incorrect password', async () => {
const developer = await createDeveloper(developerOneData);
expect(developer.status).toEqual(200);

const data = {
email: developerOneData.email,
password: 'incorrect',
};

const loginRes = await loginDeveloper(data);
expect(loginRes.status).toEqual(400);
expect(loginRes.body.error).toEqual(loginRes.body.error);
});

it('should not log a developer in with an non-existent email', async () => {
const data = {
email: anotherDeveloperData.email,
password: anotherDeveloperData.password,
};

const loginRes = await loginDeveloper(data);
expect(loginRes.status).toEqual(400);
expect(loginRes.body.error).toEqual(loginRes.body.error);
});
});

describe('logout', () => {
it('should successfully log a developer out', async () => {
const developer = await createDeveloper(anotherDeveloperData);
expect(developer.status).toEqual(200);

const data = {
email: anotherDeveloperData.email,
password: anotherDeveloperData.password,
};

const loginRes = await loginDeveloper(data);
expect(loginRes.status).toEqual(200);

const logoutRes = await logoutDeveloper({ token: loginRes.body.token });
expect(logoutRes.status).toEqual(200);
expect(logoutRes.body).toMatchObject({
message: 'Logged out successfully',
});
});
});
4 changes: 4 additions & 0 deletions __tests__/shared/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,7 @@ export const getDeveloper = (options = {}) =>

/** Login a developer */
export const loginDeveloper = (data) => server.post(`${API_ROUTE}/login`).send(data);

/** Logout a developer */
export const logoutDeveloper = (options = {}) =>
server.post(`${API_ROUTE}/logout`).set('Authorization', `Bearer ${options.token || ''}`);
40 changes: 30 additions & 10 deletions src/controllers/auth/login.ts → src/controllers/auth.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Response } from 'express';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import { Developer as DeveloperType, Express } from '../../types';
import { isProduction, isTest } from '../../config';
import { createDbConnection, handleCloseConnection } from '../../services/database';
import { developerSchema } from '../../models/Developer';
import { TEST_EMAIL } from '../../shared/constants/Developers';
import { JWT_SECRET, cookieOptions } from '../../siteConstants';
import { Developer as DeveloperType, Express } from '../types';
import { isProduction, isTest } from '../config';
import { createDbConnection, handleCloseConnection } from '../services/database';
import { developerSchema } from '../models/Developer';
import { TEST_EMAIL } from '../shared/constants/Developers';
import { JWT_SECRET, cookieOptions } from '../siteConstants';

/**
* Compares a hashed password with a plaintext password to check for a match.
Expand All @@ -29,9 +29,6 @@ const checkPassword = async (password: string, hash: string) => {
* @throws {Error} If an error occurs during the token signing process.
*/
const signToken = (email: string) => {
console.info('JWT_SECRET');
console.log(JWT_SECRET);

const token = jwt.sign({ email }, JWT_SECRET, { expiresIn: '1d' });
return token;
};
Expand Down Expand Up @@ -79,7 +76,7 @@ const loginDeveloperWithEmailAndPassword = async (email: string, password: strin
/**
* Handles the login process for a developer.
*
* @param {Express.Request} req - The Express request object.
* @param {Express.IgboAPIRequest} req - The Express request object.
* @param {Express.Response} res - The Express response object.
* @param {Express.NextFunction} next - The next middleware function.
* @returns {Promise<void>} A Promise that resolves when the login process is complete.
Expand All @@ -104,3 +101,26 @@ export const login: Express.MiddleWare = async (req, res, next) => {
return next(error);
}
};

/**
* Handles the logout process for a developer.
* @param {Express.IgboAPIRequest} req - The Express request object.
* @param {Express.Response} res - The Express response object.
* @param {Express.NextFunction} next - The next middleware function.
* @returns {Promise<void>} A Promise that resolves when the logout process is complete.
*/
export const logout: Express.MiddleWare = async (req, res, next) => {
try {
res.cookie('jwt', '', { expires: new Date(), httpOnly: true });

const message = 'Logged out successfully';
return res.status(200).send({
message,
});
} catch (error) {
if (!isTest) {
console.trace(error);
}
return next(error);
}
};
49 changes: 49 additions & 0 deletions src/middleware/authenticate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import jwt from 'jsonwebtoken';
import { JWT_SECRET } from '../siteConstants';
import { Express } from '../types';
import { createDbConnection, handleCloseConnection } from '../services/database';
import { developerSchema } from '../models/Developer';

interface DeveloperDataType {
email: string;
iat?: number;
exp?: number;
}

export const authenticate: Express.MiddleWare = async (req, res, next) => {
let token: string | undefined;
// Check if token is set
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
[, token] = req.headers.authorization.split(' ');
}

if (!token) {
return next(new Error('Unauthorized. Please login to continue.'));
}

let developer: DeveloperDataType;

// Verify token
try {
developer = jwt.verify(token, JWT_SECRET) as DeveloperDataType;
} catch (error: unknown) {
if (error instanceof Error) {
return next(new Error(error.message));
}
return next(new Error('Invalid token'));
}

// Check if developer still exists in the database
const connection = createDbConnection();
const Developer = connection.model('Developer', developerSchema);
const { email } = developer;
const currentUser = await Developer.findOne({ email });
await handleCloseConnection(connection);
if (!currentUser) {
return next(new Error('This User does not exist'));
}

// Grant access
req.developer = currentUser;
return next();
};
16 changes: 9 additions & 7 deletions src/middleware/validateApiKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ const isSameDate = (first: Date, second: Date) =>
/* Increments usage count and updates usage date */
const handleDeveloperUsage = async (developer: DeveloperDocument) => {
const updatedDeveloper = developer;
const isNewDay = !isSameDate(updatedDeveloper.usage.date, new Date());
updatedDeveloper.usage.date = new Date();
if (updatedDeveloper.usage) {
const isNewDay = !isSameDate(updatedDeveloper.usage.date || new Date(), new Date());
updatedDeveloper.usage.date = new Date();

if (isNewDay) {
updatedDeveloper.usage.count = 0;
} else {
updatedDeveloper.usage.count += 1;
if (isNewDay) {
updatedDeveloper.usage.count = 0;
} else {
updatedDeveloper.usage.count += 1;
}
}

return updatedDeveloper.save();
Expand Down Expand Up @@ -54,7 +56,7 @@ const validateApiKey: Express.MiddleWare = async (req, res, next) => {
const developer = await findDeveloper(apiKey);

if (developer) {
if (developer.usage.count >= determineLimit(apiLimit)) {
if (developer.usage!.count >= determineLimit(apiLimit)) {
res.status(403);
return res.send({ error: 'You have exceeded your limit of requests for the day' });
}
Expand Down
4 changes: 3 additions & 1 deletion src/routers/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import validateApiKey from '../middleware/validateApiKey';
import validateAdminApiKey from '../middleware/validateAdminApiKey';
import attachRedisClient from '../middleware/attachRedisClient';
import analytics from '../middleware/analytics';
import { login } from '../controllers/auth/login';
import { login, logout } from '../controllers/auth';
import { authenticate } from '../middleware/authenticate';

const router = express.Router();

Expand All @@ -35,5 +36,6 @@ router.get('/developers/account', attachRedisClient, getDeveloper);

router.get('/stats', validateAdminApiKey, attachRedisClient, getStats);
router.post('/login', login);
router.post('/logout', authenticate, logout);

export default router;
6 changes: 4 additions & 2 deletions src/types/developer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ export interface Developer {
apiKey: string;
email: string;
password: string;
usage: {
usage?: {
date: Date;
count: number;
};
createdAt?: NativeDate;
updatedAt?: NativeDate;
}

export interface DeveloperDocument extends Developer, Document<any> {
id: Types.ObjectId;
id?: Types.ObjectId;
}
6 changes: 4 additions & 2 deletions src/types/express.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Request as ExpressRequest, Response, NextFunction } from 'express';
import { RedisClientType } from 'redis';
import { DeveloperDocument } from './developer';

export type Query = {
export interface Query {
dialects: string;
examples: string;
filter: string;
Expand All @@ -13,12 +14,13 @@ export type Query = {
tags: string;
wordClasses: string;
apiLimit: string;
};
}

export interface IgboAPIRequest extends ExpressRequest {
query: Partial<Query>;
isUsingMainKey?: boolean;
redisClient?: RedisClientType;
developer?: DeveloperDocument;
}

export interface MiddleWare {
Expand Down

0 comments on commit cc81be4

Please sign in to comment.