Skip to content

Commit c00d82f

Browse files
Merge branch 'develop' of https://github.com/Suvidh-kaushik/website-backend into feat/impersonation_session
2 parents 782ecb0 + 798794d commit c00d82f

File tree

8 files changed

+871
-33
lines changed

8 files changed

+871
-33
lines changed

controllers/impersonationRequests.ts

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
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, generateImpersonationTokenService, startImpersonationService, stopImpersonationService } from "../services/impersonationRequests";
9+
import { getImpersonationRequestById, getImpersonationRequests } from "../models/impersonationRequests";
710
import {
811
CreateImpersonationRequest,
912
CreateImpersonationRequestBody,
1013
ImpersonationRequestResponse,
14+
GetImpersonationControllerRequest,
15+
GetImpersonationRequestByIdRequest,
1116
ImpersonationSessionRequest
1217
} from "../types/impersonationRequest";
18+
import { getPaginatedLink } from "../utils/helper";
1319
import { Forbidden } from "http-errors";
1420
import { NextFunction } from "express";
1521
const logger = require("../utils/logger");
@@ -20,7 +26,7 @@ const logger = require("../utils/logger");
2026
* @param {CreateImpersonationRequest} req - Express request object with user and body data.
2127
* @param {ImpersonationRequestResponse} res - Express response object.
2228
* @param {NextFunction} next - Express next middleware function.
23-
* @returns {Promise<ImpersonationRequestResponse | void>}
29+
* @returns {Promise<ImpersonationRequestResponse | void>} Returns the created request or passes error to next middleware.
2430
*/
2531
export const createImpersonationRequestController = async (
2632
req: CreateImpersonationRequest,
@@ -51,6 +57,93 @@ export const createImpersonationRequestController = async (
5157
}
5258
};
5359

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

55148
/**
56149
* Controller to handle impersonation session actions (START or STOP).

middlewares/validators/impersonationRequests.ts

Lines changed: 66 additions & 1 deletion
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, ImpersonationSessionRequest } from "../../types/impersonationRequest";
3+
import { CreateImpersonationRequest,GetImpersonationControllerRequest,GetImpersonationRequestByIdRequest,ImpersonationRequestResponse, ImpersonationSessionRequest } from "../../types/impersonationRequest";
4+
import { REQUEST_STATE } from "../../constants/requests";
45
const logger = require("../../utils/logger");
56

67
/**
@@ -36,6 +37,70 @@ export const createImpersonationRequestValidator = async (
3637
}
3738
};
3839

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+
};
102+
103+
39104

40105
export const impersonationSessionValidator = async (req:ImpersonationSessionRequest,res:ImpersonationRequestResponse,next:NextFunction) => {
41106
const querySchema = joi

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, impersonationSessionValidator } from "../middlewares/validators/impersonationRequests";
2+
import { createImpersonationRequestValidator, getImpersonationRequestByIdValidator, getImpersonationRequestsValidator, impersonationSessionValidator } 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, impersonationController } from "../controllers/impersonationRequests";
7+
import { createImpersonationRequestController, getImpersonationRequestByIdController, getImpersonationRequestsController, impersonationController } from "../controllers/impersonationRequests";
88

99
router.post(
1010
"/requests",
@@ -14,6 +14,20 @@ 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

1832
router.patch("/:id",authenticate,impersonationSessionValidator,impersonationController)
1933

0 commit comments

Comments
 (0)