Skip to content

Commit 30cdfd6

Browse files
feat: add API to create impersonation request (#2439)
* added model,route, controller and service for creation * optimized create query handling * removed rate-limiter imports * fixed types in controllers and services * fixed query used for creating the request * fixed: small query updates and updated dev flag * fixed status type and 403 forbidden error --------- Co-authored-by: Achintya Chatterjee <[email protected]>
1 parent 6d16d08 commit 30cdfd6

File tree

7 files changed

+251
-1
lines changed

7 files changed

+251
-1
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {
2+
ERROR_WHILE_CREATING_REQUEST,
3+
FEATURE_NOT_IMPLEMENTED,
4+
REQUEST_CREATED_SUCCESSFULLY
5+
} from "../constants/requests";
6+
import { createImpersonationRequestService } from "../services/impersonationRequests";
7+
import {
8+
CreateImpersonationRequest,
9+
CreateImpersonationRequestBody,
10+
ImpersonationRequestResponse
11+
} from "../types/impersonationRequest";
12+
import { NextFunction } from "express";
13+
const logger = require("../utils/logger");
14+
15+
/**
16+
* Controller to handle creation of an impersonation request.
17+
*
18+
* @param {CreateImpersonationRequest} req - Express request object with user and body data.
19+
* @param {ImpersonationRequestResponse} res - Express response object.
20+
* @param {NextFunction} next - Express next middleware function.
21+
* @returns {Promise<ImpersonationRequestResponse | void>}
22+
*/
23+
export const createImpersonationRequestController = async (
24+
req: CreateImpersonationRequest,
25+
res: ImpersonationRequestResponse,
26+
next: NextFunction
27+
): Promise<ImpersonationRequestResponse | void> => {
28+
try {
29+
const { impersonatedUserId, reason } = req.body as CreateImpersonationRequestBody;
30+
const userId = req.userData?.id;
31+
const createdBy = req.userData?.username;
32+
33+
const impersonationRequest = await createImpersonationRequestService({
34+
userId,
35+
createdBy,
36+
impersonatedUserId,
37+
reason
38+
});
39+
40+
return res.status(201).json({
41+
message: REQUEST_CREATED_SUCCESSFULLY,
42+
data: {
43+
...impersonationRequest
44+
}
45+
});
46+
} catch (error) {
47+
logger.error(ERROR_WHILE_CREATING_REQUEST, error);
48+
next(error);
49+
}
50+
};
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import joi from "joi";
2+
import { NextFunction } from "express";
3+
import { CreateImpersonationRequest, ImpersonationRequestResponse } from "../../types/impersonationRequest";
4+
const logger = require("../../utils/logger");
5+
6+
/**
7+
* Validates the create Impersonation Request payload
8+
* @param {CreateImpersonationRequest} req - request object.
9+
* @param {ImpersonationRequestResponse} res - response object.
10+
* @param {NextFunction} next - next middleware function.
11+
* @returns {Promise<void>} Resolves or sends errors.
12+
*/
13+
export const createImpersonationRequestValidator = async (
14+
req: CreateImpersonationRequest,
15+
res: ImpersonationRequestResponse,
16+
next: NextFunction
17+
): Promise<void> => {
18+
const schema = joi.object().strict().keys({
19+
impersonatedUserId: joi.string().required().messages({
20+
"string.empty": "impersonatedUserId cannot be empty",
21+
"any.required": "impersonatedUserId is required"
22+
}),
23+
reason: joi.string().required().messages({
24+
"string.empty": "reason cannot be empty",
25+
"any.required": "reason is required"
26+
})
27+
});
28+
29+
try {
30+
await schema.validateAsync(req.body, { abortEarly: false });
31+
next();
32+
} catch ( error ) {
33+
const errorMessages = error.details.map((detail:{message: string}) => detail.message);
34+
logger.error(`Error while validating request payload : ${errorMessages}`);
35+
return res.boom.badRequest(errorMessages);
36+
}
37+
};

models/impersonationRequests.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import firestore from "../utils/firestore";
2+
import {
3+
ERROR_WHILE_CREATING_REQUEST,
4+
IMPERSONATION_NOT_COMPLETED,
5+
REQUEST_ALREADY_PENDING,
6+
REQUEST_STATE
7+
} from "../constants/requests";
8+
import { Timestamp } from "firebase-admin/firestore";
9+
import { CreateImpersonationRequestModelDto, ImpersonationRequest } from "../types/impersonationRequest";
10+
import { Forbidden } from "http-errors";
11+
const logger = require("../utils/logger");
12+
13+
const impersonationRequestModel = firestore.collection("impersonationRequests");
14+
15+
/**
16+
* Creates a new impersonation request in Firestore.
17+
*
18+
* Checks for existing requests with the same impersonatedUserId and userId that are either
19+
* APPROVED or PENDING and not finished. Throws a Forbidden error if such a request exists.
20+
*
21+
* @param {CreateImpersonationRequestModelDto} body - The data for the new impersonation request.
22+
* @returns {Promise<ImpersonationRequest>} The created impersonation request object.
23+
* @throws {Forbidden} If a similar request is already pending or not completed.
24+
* @throws {Error} Logs and rethrows any error encountered during creation.
25+
*/
26+
export const createImpersonationRequest = async (
27+
body: CreateImpersonationRequestModelDto
28+
): Promise<ImpersonationRequest> => {
29+
try {
30+
const reqQuery = impersonationRequestModel
31+
.where("impersonatedUserId", "==", body.impersonatedUserId)
32+
.where("userId", "==", body.userId)
33+
.where("status", "in", ["APPROVED", "PENDING"])
34+
.where("isImpersonationFinished", "==", false).orderBy("createdAt", "desc").limit(1);
35+
36+
const snapshot = await reqQuery.get();
37+
38+
if (!snapshot.empty) {
39+
throw new Forbidden("You are not allowed for this Operation at the moment");
40+
}
41+
42+
const requestBody = {
43+
createdAt: Timestamp.now(),
44+
updatedAt: Timestamp.now(),
45+
...body,
46+
} as ImpersonationRequest;
47+
48+
const result = await impersonationRequestModel.add(requestBody);
49+
50+
return {
51+
id: result.id,
52+
...requestBody,
53+
};
54+
} catch (error) {
55+
logger.error(ERROR_WHILE_CREATING_REQUEST, { error, requestData: body });
56+
throw error;
57+
}
58+
};

routes/impersonation.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import express from "express";
2+
import { createImpersonationRequestValidator } from "../middlewares/validators/impersonationRequests";
3+
const router = express.Router();
4+
const authorizeRoles = require("../middlewares/authorizeRoles");
5+
const { SUPERUSER } = require("../constants/roles");
6+
import authenticate from "../middlewares/authenticate";
7+
import { createImpersonationRequestController } from "../controllers/impersonationRequests";
8+
9+
router.post(
10+
"/requests",
11+
authenticate,
12+
authorizeRoles([SUPERUSER]),
13+
createImpersonationRequestValidator,
14+
createImpersonationRequestController
15+
);
16+
17+
module.exports = router;

routes/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,6 @@ app.use("/v1/notifications", require("./notify"));
4141
app.use("/goals", require("./goals"));
4242
app.use("/invites", require("./invites"));
4343
app.use("/requests", require("./requests"));
44+
app.use("/impersonation", devFlagMiddleware, require("./impersonation"));
4445
app.use("/subscription", devFlagMiddleware, require("./subscription"));
4546
module.exports = app;

services/impersonationRequests.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import {
2+
ERROR_WHILE_CREATING_REQUEST,
3+
LOG_ACTION,
4+
REQUEST_LOG_TYPE,
5+
REQUEST_STATE,
6+
TASK_REQUEST_MESSAGES
7+
} from "../constants/requests";
8+
import { createImpersonationRequest } from "../models/impersonationRequests";
9+
import { fetchUser } from "../models/users";
10+
import { addLog } from "./logService";
11+
import { User } from "../typeDefinitions/users";
12+
import { NotFound } from "http-errors";
13+
import { CreateImpersonationRequestServiceBody, ImpersonationRequest } from "../types/impersonationRequest";
14+
const logger = require("../utils/logger");
15+
16+
/**
17+
* Service to create a new impersonation request.
18+
*
19+
* Checks if the impersonated user exists, creates the request, and logs the action.
20+
*
21+
* @param {CreateImpersonationRequestServiceBody} body - The request body containing impersonation details.
22+
* @returns {Promise<ImpersonationRequest>} The created impersonation request object.
23+
* @throws {NotFound} If the impersonated user does not exist.
24+
* @throws {Error} If there is an error during request creation.
25+
*/
26+
export const createImpersonationRequestService = async (
27+
body: CreateImpersonationRequestServiceBody
28+
) : Promise<ImpersonationRequest> => {
29+
try {
30+
const { userExists, user: impersonatedUser } = await fetchUser({ userId: body.impersonatedUserId });
31+
if (!userExists) {
32+
throw new NotFound(TASK_REQUEST_MESSAGES.USER_NOT_FOUND);
33+
}
34+
35+
const { username: createdFor } = impersonatedUser as User;
36+
37+
const impersonationRequest = await createImpersonationRequest({
38+
status: REQUEST_STATE.PENDING,
39+
userId: body.userId,
40+
impersonatedUserId: body.impersonatedUserId,
41+
isImpersonationFinished: false,
42+
createdBy: body.createdBy,
43+
createdFor: createdFor,
44+
reason: body.reason,
45+
});
46+
47+
const impersonationRequestLog = {
48+
type: REQUEST_LOG_TYPE.REQUEST_CREATED,
49+
meta: {
50+
requestId: impersonationRequest.id,
51+
action: LOG_ACTION.CREATE,
52+
userId: body.userId,
53+
createdAt: Date.now(),
54+
},
55+
body: {
56+
...impersonationRequest,
57+
isImpersonationFinished: String(impersonationRequest.isImpersonationFinished),
58+
},
59+
};
60+
61+
await addLog(
62+
impersonationRequestLog.type,
63+
impersonationRequestLog.meta,
64+
impersonationRequestLog.body
65+
);
66+
67+
return impersonationRequest;
68+
} catch (error) {
69+
logger.error(ERROR_WHILE_CREATING_REQUEST, error);
70+
throw error;
71+
}
72+
};

types/impersonationRequest.d.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export type CreateImpersonationRequestBody = {
2626
reason: string;
2727
};
2828

29-
export type CreateImpersonationRequestModelBody = {
29+
export type CreateImpersonationRequestModelDto = {
3030
status: REQUEST_STATE;
3131
isImpersonationFinished: boolean;
3232
createdBy: string;
@@ -49,6 +49,14 @@ export type UpdateImpersonationRequestStatusBody = {
4949

5050
export type ImpersonationRequestQuery = RequestQuery & {
5151
dev?: string;
52+
createdBy?: string;
53+
createdFor?: string;
54+
status?: keyof typeof REQUEST_STATE;
55+
id?: string;
56+
prev?: string;
57+
next?: string;
58+
page?: number;
59+
size?: number;
5260
};
5361

5462
export type ImpersonationRequestResponse = Response & {
@@ -78,4 +86,11 @@ export type PaginatedImpersonationRequests = {
7886
prev: string;
7987
page: number;
8088
count: number;
89+
}
90+
91+
export type CreateImpersonationRequestServiceBody={
92+
userId: string;
93+
createdBy: string;
94+
impersonatedUserId: string;
95+
reason: string;
8196
}

0 commit comments

Comments
 (0)