Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 97 additions & 5 deletions controllers/impersonationRequests.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import {
ERROR_WHILE_CREATING_REQUEST,
FEATURE_NOT_IMPLEMENTED,
REQUEST_CREATED_SUCCESSFULLY
ERROR_WHILE_FETCHING_REQUEST,
REQUEST_FETCHED_SUCCESSFULLY,
REQUEST_CREATED_SUCCESSFULLY,
REQUEST_DOES_NOT_EXIST
} from "../constants/requests";
import { createImpersonationRequestService } from "../services/impersonationRequests";
import { getImpersonationRequestById, getImpersonationRequests } from "../models/impersonationRequests";
import {
CreateImpersonationRequest,
CreateImpersonationRequestBody,
ImpersonationRequestResponse
ImpersonationRequestResponse,
GetImpersonationControllerRequest,
GetImpersonationRequestByIdRequest
} from "../types/impersonationRequest";
import { getPaginatedLink } from "../utils/helper";
import { NextFunction } from "express";
const logger = require("../utils/logger");

Expand All @@ -18,7 +24,7 @@ const logger = require("../utils/logger");
* @param {CreateImpersonationRequest} req - Express request object with user and body data.
* @param {ImpersonationRequestResponse} res - Express response object.
* @param {NextFunction} next - Express next middleware function.
* @returns {Promise<ImpersonationRequestResponse | void>}
* @returns {Promise<ImpersonationRequestResponse | void>} Returns the created request or passes error to next middleware.
*/
export const createImpersonationRequestController = async (
req: CreateImpersonationRequest,
Expand Down Expand Up @@ -47,4 +53,90 @@ export const createImpersonationRequestController = async (
logger.error(ERROR_WHILE_CREATING_REQUEST, error);
next(error);
}
};
};

/**
* Controller to fetch an impersonation request by its ID.
*
* @param {GetImpersonationRequestByIdRequest} req - Express request object containing `id` parameter.
* @param {ImpersonationRequestResponse} res - Express response object.
* @returns {Promise<ImpersonationRequestResponse>} Returns the request if found, or 404 if it doesn't exist.
*/
export const getImpersonationRequestByIdController = async (
req: GetImpersonationRequestByIdRequest,
res: ImpersonationRequestResponse
): Promise<ImpersonationRequestResponse> => {
const id = req.params.id;
try {
const request = await getImpersonationRequestById(id);

if (!request) {
return res.status(404).json({
message: REQUEST_DOES_NOT_EXIST,
});
}

return res.status(200).json({
message: REQUEST_FETCHED_SUCCESSFULLY,
data: request,
});

} catch (error) {
logger.error(ERROR_WHILE_FETCHING_REQUEST, error);
return res.boom.badImplementation(ERROR_WHILE_FETCHING_REQUEST);
}
};

/**
* Controller to fetch impersonation requests with optional filtering and pagination.
*
* @param {GetImpersonationControllerRequest} req - Express request object containing query parameters.
* @param {ImpersonationRequestResponse} res - Express response object.
* @returns {Promise<ImpersonationRequestResponse>} Returns paginated impersonation request data or 204 if none found.
*/
export const getImpersonationRequestsController = async (
req: GetImpersonationControllerRequest,
res: ImpersonationRequestResponse
): Promise<ImpersonationRequestResponse> => {
try {
const { query } = req;

const requests = await getImpersonationRequests(query);
if (!requests || requests.allRequests.length === 0) {
return res.status(204).send();
}

const { allRequests, next, prev } = requests;
const count = allRequests.length;

let nextUrl = null;
let prevUrl = null;
if (next) {
nextUrl = getPaginatedLink({
endpoint: "/impersonation/requests",
query,
cursorKey: "next",
docId: next,
});
}
if (prev) {
prevUrl = getPaginatedLink({
endpoint: "/impersonation/requests",
query,
cursorKey: "prev",
docId: prev,
});
}

return res.status(200).json({
message: REQUEST_FETCHED_SUCCESSFULLY,
data: allRequests,
next: nextUrl,
prev: prevUrl,
count,
});
} catch (err) {
logger.error(ERROR_WHILE_FETCHING_REQUEST, err);
return res.boom.badImplementation(ERROR_WHILE_FETCHING_REQUEST);
}
};
68 changes: 66 additions & 2 deletions middlewares/validators/impersonationRequests.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import joi from "joi";
import { NextFunction } from "express";
import { CreateImpersonationRequest, ImpersonationRequestResponse } from "../../types/impersonationRequest";
import { CreateImpersonationRequest,GetImpersonationControllerRequest,GetImpersonationRequestByIdRequest,ImpersonationRequestResponse } from "../../types/impersonationRequest";
import { REQUEST_STATE } from "../../constants/requests";
const logger = require("../../utils/logger");

/**
Expand Down Expand Up @@ -34,4 +35,67 @@ export const createImpersonationRequestValidator = async (
logger.error(`Error while validating request payload : ${errorMessages}`);
return res.boom.badRequest(errorMessages);
}
};
};

/**
* Middleware to validate query parameters for fetching impersonation requests.
*
* @param {GetImpersonationControllerRequest} req - Express request object.
* @param {ImpersonationRequestResponse} res - Express response object.
* @param {NextFunction} next - Express next middleware function.
*/
export const getImpersonationRequestsValidator = async (
req: GetImpersonationControllerRequest,
res: ImpersonationRequestResponse,
next: NextFunction
) => {
const schema = joi.object().keys({
dev: joi.string().optional(), // TODO: Remove this validator once feature is tested and ready to be used
createdBy: joi.string().insensitive().optional(),
createdFor: joi.string().insensitive().optional(),
status: joi
.string()
.valid(REQUEST_STATE.APPROVED, REQUEST_STATE.PENDING, REQUEST_STATE.REJECTED)
.optional(),
next: joi.string().optional(),
prev: joi.string().optional(),
size: joi.number().integer().positive().min(1).max(100).optional(),
});

try {
await schema.validateAsync(req.query, { abortEarly: false });
next();
} catch ( error ) {
const errorMessages = error.details.map((detail: { message: string }) => detail.message);
logger.error(`Error while validating request payload : ${errorMessages}`);
return res.boom.badRequest(errorMessages);
}
};

/**
* Middleware to validate route parameters for fetching an impersonation request by ID.
*
* @param {GetImpersonationRequestByIdRequest} req - Express request object containing route params.
* @param {ImpersonationRequestResponse} res - Express response object.
* @param {NextFunction} next - Express next middleware function.
* @returns {Promise<void>} Resolves and calls `next()` if validation passes, otherwise sends a badRequest response.
*/
export const getImpersonationRequestByIdValidator = async (
req: GetImpersonationRequestByIdRequest,
res: ImpersonationRequestResponse,
next: NextFunction
): Promise<void> => {
const schema = joi.object().keys({
dev: joi.string().optional(),
id: joi.string().max(100).pattern(/^[a-zA-Z0-9-_]+$/).required(),
});

try {
await schema.validateAsync(req.params, { abortEarly: false });
next();
} catch (error) {
const errorMessages = error.details.map((detail: { message: string }) => detail.message);
logger.error(`Error while validating request payload : ${errorMessages}`);
return res.boom.badRequest(errorMessages);
}
};
121 changes: 117 additions & 4 deletions models/impersonationRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import {
ERROR_WHILE_CREATING_REQUEST,
IMPERSONATION_NOT_COMPLETED,
REQUEST_ALREADY_PENDING,
REQUEST_STATE
REQUEST_STATE,
ERROR_WHILE_FETCHING_REQUEST
} from "../constants/requests";
import { Timestamp } from "firebase-admin/firestore";
import { CreateImpersonationRequestModelDto, ImpersonationRequest } from "../types/impersonationRequest";
import { Query, CollectionReference } from '@google-cloud/firestore';
import { CreateImpersonationRequestModelDto, ImpersonationRequest, PaginatedImpersonationRequests,ImpersonationRequestQuery} from "../types/impersonationRequest";
import { Forbidden } from "http-errors";
const logger = require("../utils/logger");

const impersonationRequestModel = firestore.collection("impersonationRequests");
const DEFAULT_PAGE_SIZE = 5;

/**
* Creates a new impersonation request in Firestore.
Expand Down Expand Up @@ -55,4 +57,115 @@ export const createImpersonationRequest = async (
logger.error(ERROR_WHILE_CREATING_REQUEST, { error, requestData: body });
throw error;
}
};
};

/**
* Retrieves an impersonation request by its ID.
* @param {string} id - The ID of the impersonation request to retrieve.
* @returns {Promise<ImpersonationRequest|null>} The found impersonation request or null if not found.
* @throws {Error} Logs and rethrows any error encountered during fetch.
*/
export const getImpersonationRequestById = async (
id: string
): Promise<ImpersonationRequest | null> => {
try {
const requestDoc = await impersonationRequestModel.doc(id).get();
if (!requestDoc.exists) {
return null;
}
const data = requestDoc.data() as ImpersonationRequest;
return {
id: requestDoc.id,
...data,
};
} catch (error) {
logger.error(`${ERROR_WHILE_FETCHING_REQUEST} for ID: ${id}`, error);
throw error;
}
};

/**
* Retrieves a paginated list of impersonation requests based on query filters.
* @param {object} query - The query filters.
* @param {string} [query.createdBy] - Filter by the username of the request creator.
* @param {string} [query.createdFor] - Filter by the username of the user the request is created for.
* @param {string} [query.status] - Filter by request status (e.g., "APPROVED", "PENDING", "REJECTED").
* @param {string} [query.prev] - Document ID to use as the ending point for backward pagination.
* @param {string} [query.next] - Document ID to use as the starting point for forward pagination.
* @param {string} [query.size] - Number of results per page.
* @returns {Promise<PaginatedImpersonationRequests|null>} The paginated impersonation requests or null if none found.
* @throws Logs and rethrows any error encountered during fetch.
*/
export const getImpersonationRequests = async (
query
): Promise<PaginatedImpersonationRequests | null> => {

let { createdBy, createdFor, status, prev, next, size = DEFAULT_PAGE_SIZE } = query;

size = size ? Number.parseInt(size) : DEFAULT_PAGE_SIZE;


try {
let requestQuery: Query<ImpersonationRequest> = impersonationRequestModel as CollectionReference<ImpersonationRequest>;

if (createdBy) {
requestQuery = requestQuery.where("createdBy", "==", createdBy);
}
if (status) {
requestQuery = requestQuery.where("status", "==", status);
}
if (createdFor) {
requestQuery = requestQuery.where("createdFor", "==", createdFor);
Comment on lines +111 to +118
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will we need to create index for the queries?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, These queries require indexes I have created an issue link for this and tagged you - ISSUE LINK

}

requestQuery = requestQuery.orderBy("createdAt", "desc");
let requestQueryDoc = requestQuery;

if (prev) {
requestQueryDoc = requestQueryDoc.limitToLast(size);
} else {
requestQueryDoc = requestQueryDoc.limit(size);
}

if (next) {
const doc = await impersonationRequestModel.doc(next).get();
requestQueryDoc = requestQueryDoc.startAt(doc);
} else if (prev) {
const doc = await impersonationRequestModel.doc(prev).get();
requestQueryDoc = requestQueryDoc.endAt(doc);
}

const snapshot = await requestQueryDoc.get();
let nextDoc;
let prevDoc;

if (!snapshot.empty) {
const first = snapshot.docs[0];
prevDoc = await requestQuery.endBefore(first).limitToLast(1).get();
const last = snapshot.docs[snapshot.docs.length - 1];
nextDoc = await requestQuery.startAfter(last).limit(1).get();
}

const allRequests = snapshot.empty
? []
: snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));

if (allRequests.length === 0) {
return null;
}

const count = allRequests.length;
return {
allRequests,
prev: prevDoc && !prevDoc.empty ? prevDoc.docs[0].id : null,
next: nextDoc && !nextDoc.empty ? nextDoc.docs[0].id : null,
count,
};
} catch (error) {
logger.error(ERROR_WHILE_FETCHING_REQUEST, error);
throw error;
}
}
18 changes: 16 additions & 2 deletions routes/impersonation.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import express from "express";
import { createImpersonationRequestValidator } from "../middlewares/validators/impersonationRequests";
import { createImpersonationRequestValidator, getImpersonationRequestByIdValidator, getImpersonationRequestsValidator } from "../middlewares/validators/impersonationRequests";
const router = express.Router();
const authorizeRoles = require("../middlewares/authorizeRoles");
const { SUPERUSER } = require("../constants/roles");
import authenticate from "../middlewares/authenticate";
import { createImpersonationRequestController } from "../controllers/impersonationRequests";
import { createImpersonationRequestController, getImpersonationRequestByIdController, getImpersonationRequestsController } from "../controllers/impersonationRequests";

router.post(
"/requests",
Expand All @@ -14,4 +14,18 @@
createImpersonationRequestController
);

router.get(
"/requests",
authenticate,
getImpersonationRequestsValidator,
getImpersonationRequestsController
);

router.get(
"/requests/:id",
authenticate,
getImpersonationRequestByIdValidator,
getImpersonationRequestByIdController
);

module.exports = router;
Loading
Loading