Skip to content

Commit 8c27d4c

Browse files
Merge pull request #2442 from Suvidh-kaushik/feat/get_impersonation_requests
feat: add API to fetch impersonation requests
1 parent 2e073ec commit 8c27d4c

File tree

5 files changed

+305
-16
lines changed

5 files changed

+305
-16
lines changed

controllers/impersonationRequests.ts

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import {
22
ERROR_WHILE_CREATING_REQUEST,
3-
FEATURE_NOT_IMPLEMENTED,
4-
REQUEST_CREATED_SUCCESSFULLY
3+
ERROR_WHILE_FETCHING_REQUEST,
4+
REQUEST_FETCHED_SUCCESSFULLY,
5+
REQUEST_CREATED_SUCCESSFULLY,
6+
REQUEST_DOES_NOT_EXIST
57
} from "../constants/requests";
68
import { createImpersonationRequestService } from "../services/impersonationRequests";
9+
import { getImpersonationRequestById, getImpersonationRequests } from "../models/impersonationRequests";
710
import {
811
CreateImpersonationRequest,
912
CreateImpersonationRequestBody,
10-
ImpersonationRequestResponse
13+
ImpersonationRequestResponse,
14+
GetImpersonationControllerRequest,
15+
GetImpersonationRequestByIdRequest
1116
} from "../types/impersonationRequest";
17+
import { getPaginatedLink } from "../utils/helper";
1218
import { NextFunction } from "express";
1319
const logger = require("../utils/logger");
1420

@@ -18,7 +24,7 @@ const logger = require("../utils/logger");
1824
* @param {CreateImpersonationRequest} req - Express request object with user and body data.
1925
* @param {ImpersonationRequestResponse} res - Express response object.
2026
* @param {NextFunction} next - Express next middleware function.
21-
* @returns {Promise<ImpersonationRequestResponse | void>}
27+
* @returns {Promise<ImpersonationRequestResponse | void>} Returns the created request or passes error to next middleware.
2228
*/
2329
export const createImpersonationRequestController = async (
2430
req: CreateImpersonationRequest,
@@ -47,4 +53,90 @@ export const createImpersonationRequestController = async (
4753
logger.error(ERROR_WHILE_CREATING_REQUEST, error);
4854
next(error);
4955
}
50-
};
56+
};
57+
58+
/**
59+
* Controller to fetch an impersonation request by its ID.
60+
*
61+
* @param {GetImpersonationRequestByIdRequest} req - Express request object containing `id` parameter.
62+
* @param {ImpersonationRequestResponse} res - Express response object.
63+
* @returns {Promise<ImpersonationRequestResponse>} Returns the request if found, or 404 if it doesn't exist.
64+
*/
65+
export const getImpersonationRequestByIdController = async (
66+
req: GetImpersonationRequestByIdRequest,
67+
res: ImpersonationRequestResponse
68+
): Promise<ImpersonationRequestResponse> => {
69+
const id = req.params.id;
70+
try {
71+
const request = await getImpersonationRequestById(id);
72+
73+
if (!request) {
74+
return res.status(404).json({
75+
message: REQUEST_DOES_NOT_EXIST,
76+
});
77+
}
78+
79+
return res.status(200).json({
80+
message: REQUEST_FETCHED_SUCCESSFULLY,
81+
data: request,
82+
});
83+
84+
} catch (error) {
85+
logger.error(ERROR_WHILE_FETCHING_REQUEST, error);
86+
return res.boom.badImplementation(ERROR_WHILE_FETCHING_REQUEST);
87+
}
88+
};
89+
90+
/**
91+
* Controller to fetch impersonation requests with optional filtering and pagination.
92+
*
93+
* @param {GetImpersonationControllerRequest} req - Express request object containing query parameters.
94+
* @param {ImpersonationRequestResponse} res - Express response object.
95+
* @returns {Promise<ImpersonationRequestResponse>} Returns paginated impersonation request data or 204 if none found.
96+
*/
97+
export const getImpersonationRequestsController = async (
98+
req: GetImpersonationControllerRequest,
99+
res: ImpersonationRequestResponse
100+
): Promise<ImpersonationRequestResponse> => {
101+
try {
102+
const { query } = req;
103+
104+
const requests = await getImpersonationRequests(query);
105+
if (!requests || requests.allRequests.length === 0) {
106+
return res.status(204).send();
107+
}
108+
109+
const { allRequests, next, prev } = requests;
110+
const count = allRequests.length;
111+
112+
let nextUrl = null;
113+
let prevUrl = null;
114+
if (next) {
115+
nextUrl = getPaginatedLink({
116+
endpoint: "/impersonation/requests",
117+
query,
118+
cursorKey: "next",
119+
docId: next,
120+
});
121+
}
122+
if (prev) {
123+
prevUrl = getPaginatedLink({
124+
endpoint: "/impersonation/requests",
125+
query,
126+
cursorKey: "prev",
127+
docId: prev,
128+
});
129+
}
130+
131+
return res.status(200).json({
132+
message: REQUEST_FETCHED_SUCCESSFULLY,
133+
data: allRequests,
134+
next: nextUrl,
135+
prev: prevUrl,
136+
count,
137+
});
138+
} catch (err) {
139+
logger.error(ERROR_WHILE_FETCHING_REQUEST, err);
140+
return res.boom.badImplementation(ERROR_WHILE_FETCHING_REQUEST);
141+
}
142+
};

middlewares/validators/impersonationRequests.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import joi from "joi";
22
import { NextFunction } from "express";
3-
import { CreateImpersonationRequest, ImpersonationRequestResponse } from "../../types/impersonationRequest";
3+
import { CreateImpersonationRequest,GetImpersonationControllerRequest,GetImpersonationRequestByIdRequest,ImpersonationRequestResponse } from "../../types/impersonationRequest";
4+
import { REQUEST_STATE } from "../../constants/requests";
45
const logger = require("../../utils/logger");
56

67
/**
@@ -34,4 +35,67 @@ export const createImpersonationRequestValidator = async (
3435
logger.error(`Error while validating request payload : ${errorMessages}`);
3536
return res.boom.badRequest(errorMessages);
3637
}
37-
};
38+
};
39+
40+
/**
41+
* Middleware to validate query parameters for fetching impersonation requests.
42+
*
43+
* @param {GetImpersonationControllerRequest} req - Express request object.
44+
* @param {ImpersonationRequestResponse} res - Express response object.
45+
* @param {NextFunction} next - Express next middleware function.
46+
*/
47+
export const getImpersonationRequestsValidator = async (
48+
req: GetImpersonationControllerRequest,
49+
res: ImpersonationRequestResponse,
50+
next: NextFunction
51+
) => {
52+
const schema = joi.object().keys({
53+
dev: joi.string().optional(), // TODO: Remove this validator once feature is tested and ready to be used
54+
createdBy: joi.string().insensitive().optional(),
55+
createdFor: joi.string().insensitive().optional(),
56+
status: joi
57+
.string()
58+
.valid(REQUEST_STATE.APPROVED, REQUEST_STATE.PENDING, REQUEST_STATE.REJECTED)
59+
.optional(),
60+
next: joi.string().optional(),
61+
prev: joi.string().optional(),
62+
size: joi.number().integer().positive().min(1).max(100).optional(),
63+
});
64+
65+
try {
66+
await schema.validateAsync(req.query, { abortEarly: false });
67+
next();
68+
} catch ( error ) {
69+
const errorMessages = error.details.map((detail: { message: string }) => detail.message);
70+
logger.error(`Error while validating request payload : ${errorMessages}`);
71+
return res.boom.badRequest(errorMessages);
72+
}
73+
};
74+
75+
/**
76+
* Middleware to validate route parameters for fetching an impersonation request by ID.
77+
*
78+
* @param {GetImpersonationRequestByIdRequest} req - Express request object containing route params.
79+
* @param {ImpersonationRequestResponse} res - Express response object.
80+
* @param {NextFunction} next - Express next middleware function.
81+
* @returns {Promise<void>} Resolves and calls `next()` if validation passes, otherwise sends a badRequest response.
82+
*/
83+
export const getImpersonationRequestByIdValidator = async (
84+
req: GetImpersonationRequestByIdRequest,
85+
res: ImpersonationRequestResponse,
86+
next: NextFunction
87+
): Promise<void> => {
88+
const schema = joi.object().keys({
89+
dev: joi.string().optional(),
90+
id: joi.string().max(100).pattern(/^[a-zA-Z0-9-_]+$/).required(),
91+
});
92+
93+
try {
94+
await schema.validateAsync(req.params, { abortEarly: false });
95+
next();
96+
} catch (error) {
97+
const errorMessages = error.details.map((detail: { message: string }) => detail.message);
98+
logger.error(`Error while validating request payload : ${errorMessages}`);
99+
return res.boom.badRequest(errorMessages);
100+
}
101+
};

models/impersonationRequests.ts

Lines changed: 117 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ import {
33
ERROR_WHILE_CREATING_REQUEST,
44
IMPERSONATION_NOT_COMPLETED,
55
REQUEST_ALREADY_PENDING,
6-
REQUEST_STATE
6+
REQUEST_STATE,
7+
ERROR_WHILE_FETCHING_REQUEST
78
} from "../constants/requests";
89
import { Timestamp } from "firebase-admin/firestore";
9-
import { CreateImpersonationRequestModelDto, ImpersonationRequest } from "../types/impersonationRequest";
10+
import { Query, CollectionReference } from '@google-cloud/firestore';
11+
import { CreateImpersonationRequestModelDto, ImpersonationRequest, PaginatedImpersonationRequests,ImpersonationRequestQuery} from "../types/impersonationRequest";
1012
import { Forbidden } from "http-errors";
1113
const logger = require("../utils/logger");
12-
1314
const impersonationRequestModel = firestore.collection("impersonationRequests");
15+
const DEFAULT_PAGE_SIZE = 5;
1416

1517
/**
1618
* Creates a new impersonation request in Firestore.
@@ -55,4 +57,115 @@ export const createImpersonationRequest = async (
5557
logger.error(ERROR_WHILE_CREATING_REQUEST, { error, requestData: body });
5658
throw error;
5759
}
58-
};
60+
};
61+
62+
/**
63+
* Retrieves an impersonation request by its ID.
64+
* @param {string} id - The ID of the impersonation request to retrieve.
65+
* @returns {Promise<ImpersonationRequest|null>} The found impersonation request or null if not found.
66+
* @throws {Error} Logs and rethrows any error encountered during fetch.
67+
*/
68+
export const getImpersonationRequestById = async (
69+
id: string
70+
): Promise<ImpersonationRequest | null> => {
71+
try {
72+
const requestDoc = await impersonationRequestModel.doc(id).get();
73+
if (!requestDoc.exists) {
74+
return null;
75+
}
76+
const data = requestDoc.data() as ImpersonationRequest;
77+
return {
78+
id: requestDoc.id,
79+
...data,
80+
};
81+
} catch (error) {
82+
logger.error(`${ERROR_WHILE_FETCHING_REQUEST} for ID: ${id}`, error);
83+
throw error;
84+
}
85+
};
86+
87+
/**
88+
* Retrieves a paginated list of impersonation requests based on query filters.
89+
* @param {object} query - The query filters.
90+
* @param {string} [query.createdBy] - Filter by the username of the request creator.
91+
* @param {string} [query.createdFor] - Filter by the username of the user the request is created for.
92+
* @param {string} [query.status] - Filter by request status (e.g., "APPROVED", "PENDING", "REJECTED").
93+
* @param {string} [query.prev] - Document ID to use as the ending point for backward pagination.
94+
* @param {string} [query.next] - Document ID to use as the starting point for forward pagination.
95+
* @param {string} [query.size] - Number of results per page.
96+
* @returns {Promise<PaginatedImpersonationRequests|null>} The paginated impersonation requests or null if none found.
97+
* @throws Logs and rethrows any error encountered during fetch.
98+
*/
99+
export const getImpersonationRequests = async (
100+
query
101+
): Promise<PaginatedImpersonationRequests | null> => {
102+
103+
let { createdBy, createdFor, status, prev, next, size = DEFAULT_PAGE_SIZE } = query;
104+
105+
size = size ? Number.parseInt(size) : DEFAULT_PAGE_SIZE;
106+
107+
108+
try {
109+
let requestQuery: Query<ImpersonationRequest> = impersonationRequestModel as CollectionReference<ImpersonationRequest>;
110+
111+
if (createdBy) {
112+
requestQuery = requestQuery.where("createdBy", "==", createdBy);
113+
}
114+
if (status) {
115+
requestQuery = requestQuery.where("status", "==", status);
116+
}
117+
if (createdFor) {
118+
requestQuery = requestQuery.where("createdFor", "==", createdFor);
119+
}
120+
121+
requestQuery = requestQuery.orderBy("createdAt", "desc");
122+
let requestQueryDoc = requestQuery;
123+
124+
if (prev) {
125+
requestQueryDoc = requestQueryDoc.limitToLast(size);
126+
} else {
127+
requestQueryDoc = requestQueryDoc.limit(size);
128+
}
129+
130+
if (next) {
131+
const doc = await impersonationRequestModel.doc(next).get();
132+
requestQueryDoc = requestQueryDoc.startAt(doc);
133+
} else if (prev) {
134+
const doc = await impersonationRequestModel.doc(prev).get();
135+
requestQueryDoc = requestQueryDoc.endAt(doc);
136+
}
137+
138+
const snapshot = await requestQueryDoc.get();
139+
let nextDoc;
140+
let prevDoc;
141+
142+
if (!snapshot.empty) {
143+
const first = snapshot.docs[0];
144+
prevDoc = await requestQuery.endBefore(first).limitToLast(1).get();
145+
const last = snapshot.docs[snapshot.docs.length - 1];
146+
nextDoc = await requestQuery.startAfter(last).limit(1).get();
147+
}
148+
149+
const allRequests = snapshot.empty
150+
? []
151+
: snapshot.docs.map(doc => ({
152+
id: doc.id,
153+
...doc.data()
154+
}));
155+
156+
if (allRequests.length === 0) {
157+
return null;
158+
}
159+
160+
const count = allRequests.length;
161+
return {
162+
allRequests,
163+
prev: prevDoc && !prevDoc.empty ? prevDoc.docs[0].id : null,
164+
next: nextDoc && !nextDoc.empty ? nextDoc.docs[0].id : null,
165+
count,
166+
};
167+
} catch (error) {
168+
logger.error(ERROR_WHILE_FETCHING_REQUEST, error);
169+
throw error;
170+
}
171+
}

routes/impersonation.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import express from "express";
2-
import { createImpersonationRequestValidator } from "../middlewares/validators/impersonationRequests";
2+
import { createImpersonationRequestValidator, getImpersonationRequestByIdValidator, getImpersonationRequestsValidator } from "../middlewares/validators/impersonationRequests";
33
const router = express.Router();
44
const authorizeRoles = require("../middlewares/authorizeRoles");
55
const { SUPERUSER } = require("../constants/roles");
66
import authenticate from "../middlewares/authenticate";
7-
import { createImpersonationRequestController } from "../controllers/impersonationRequests";
7+
import { createImpersonationRequestController, getImpersonationRequestByIdController, getImpersonationRequestsController } from "../controllers/impersonationRequests";
88

99
router.post(
1010
"/requests",
@@ -14,4 +14,18 @@ router.post(
1414
createImpersonationRequestController
1515
);
1616

17+
router.get(
18+
"/requests",
19+
authenticate,
20+
getImpersonationRequestsValidator,
21+
getImpersonationRequestsController
22+
);
23+
24+
router.get(
25+
"/requests/:id",
26+
authenticate,
27+
getImpersonationRequestByIdValidator,
28+
getImpersonationRequestByIdController
29+
);
30+
1731
module.exports = router;

0 commit comments

Comments
 (0)