Skip to content

Commit 37afd2f

Browse files
feat/ add API to update impersonation requests (#2446)
* added services, controllers and model for update impersonation requests * added route and fixed jsdoc * fixed bot comments and typos * removed validation service and fixed forbidden errors * removed validation service
1 parent 798794d commit 37afd2f

File tree

6 files changed

+199
-20
lines changed

6 files changed

+199
-20
lines changed

controllers/impersonationRequests.ts

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@ import {
22
ERROR_WHILE_CREATING_REQUEST,
33
ERROR_WHILE_FETCHING_REQUEST,
44
REQUEST_FETCHED_SUCCESSFULLY,
5-
REQUEST_CREATED_SUCCESSFULLY,
6-
REQUEST_DOES_NOT_EXIST
5+
REQUEST_DOES_NOT_EXIST,
6+
ERROR_WHILE_UPDATING_REQUEST,
7+
REQUEST_CREATED_SUCCESSFULLY
78
} from "../constants/requests";
8-
import { createImpersonationRequestService } from "../services/impersonationRequests";
9+
import { createImpersonationRequestService, updateImpersonationRequestService } from "../services/impersonationRequests";
910
import { getImpersonationRequestById, getImpersonationRequests } from "../models/impersonationRequests";
1011
import {
1112
CreateImpersonationRequest,
1213
CreateImpersonationRequestBody,
14+
UpdateImpersonationRequest,
15+
UpdateImpersonationRequestStatusBody,
1316
ImpersonationRequestResponse,
1417
GetImpersonationControllerRequest,
15-
GetImpersonationRequestByIdRequest
18+
GetImpersonationRequestByIdRequest,
1619
} from "../types/impersonationRequest";
1720
import { getPaginatedLink } from "../utils/helper";
1821
import { NextFunction } from "express";
@@ -140,3 +143,40 @@ export const getImpersonationRequestsController = async (
140143
return res.boom.badImplementation(ERROR_WHILE_FETCHING_REQUEST);
141144
}
142145
};
146+
147+
148+
/**
149+
* Controller to Update the status of an impersonation request.
150+
*
151+
* @param {UpdateImpersonationRequest} req - Express request with params, body, and user data.
152+
* @param {ImpersonationRequestResponse} res - Express response object.
153+
* @param {NextFunction} next - Express middleware `next` function.
154+
* @returns {Promise<ImpersonationRequestResponse>} Returns updated request data or passes error to `next`.
155+
*/
156+
export const updateImpersonationRequestStatusController = async (
157+
req: UpdateImpersonationRequest,
158+
res: ImpersonationRequestResponse,
159+
next: NextFunction
160+
): Promise<ImpersonationRequestResponse> => {
161+
try {
162+
const requestId = req.params.id;
163+
const lastModifiedBy = req.userData.id;
164+
const requestBody: UpdateImpersonationRequestStatusBody = req.body;
165+
166+
const { returnMessage, updatedRequest: response } = await updateImpersonationRequestService({
167+
id: requestId,
168+
updatePayload: requestBody,
169+
lastModifiedBy,
170+
});
171+
172+
return res.status(200).json({
173+
message: returnMessage,
174+
data: {
175+
...response,
176+
},
177+
});
178+
} catch (error) {
179+
logger.error(ERROR_WHILE_UPDATING_REQUEST, error);
180+
next(error);
181+
}
182+
};

middlewares/validators/impersonationRequests.ts

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

@@ -99,3 +99,34 @@ export const getImpersonationRequestByIdValidator = async (
9999
return res.boom.badRequest(errorMessages);
100100
}
101101
};
102+
103+
104+
105+
export const updateImpersonationRequestValidator=async (
106+
req:UpdateImpersonationRequest,
107+
res:ImpersonationRequestResponse,
108+
next:NextFunction
109+
)=>{
110+
const schema = joi
111+
.object()
112+
.strict()
113+
.keys({
114+
status: joi
115+
.string()
116+
.valid(REQUEST_STATE.APPROVED, REQUEST_STATE.REJECTED)
117+
.required()
118+
.messages({
119+
"any.only": "status must be APPROVED or REJECTED",
120+
}),
121+
message: joi.string().optional()
122+
});
123+
124+
try {
125+
await schema.validateAsync(req.body, { abortEarly: false });
126+
next();
127+
} catch (error) {
128+
const errorMessages = error.details.map((detail:{message: string}) => detail.message);
129+
logger.error(`Error while validating request payload : ${errorMessages}`);
130+
return res.boom.badRequest(errorMessages);
131+
}
132+
}

models/impersonationRequests.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import firestore from "../utils/firestore";
22
import {
33
ERROR_WHILE_CREATING_REQUEST,
4-
IMPERSONATION_NOT_COMPLETED,
5-
REQUEST_ALREADY_PENDING,
6-
REQUEST_STATE,
4+
ERROR_WHILE_UPDATING_REQUEST,
75
ERROR_WHILE_FETCHING_REQUEST
86
} from "../constants/requests";
97
import { Timestamp } from "firebase-admin/firestore";
108
import { Query, CollectionReference } from '@google-cloud/firestore';
11-
import { CreateImpersonationRequestModelDto, ImpersonationRequest, PaginatedImpersonationRequests,ImpersonationRequestQuery} from "../types/impersonationRequest";
9+
import { CreateImpersonationRequestModelDto, ImpersonationRequest, UpdateImpersonationRequestModelDto, PaginatedImpersonationRequests,ImpersonationRequestQuery} from "../types/impersonationRequest";
1210
import { Forbidden } from "http-errors";
1311
const logger = require("../utils/logger");
1412
const impersonationRequestModel = firestore.collection("impersonationRequests");
@@ -168,4 +166,29 @@ export const getImpersonationRequests = async (
168166
logger.error(ERROR_WHILE_FETCHING_REQUEST, error);
169167
throw error;
170168
}
171-
}
169+
}
170+
171+
/**
172+
* Updates an existing impersonation request in Firestore.
173+
* @param {UpdateImpersonationRequestModelDto} body - The update data for the impersonation request. Must include `id`, `lastModifiedBy`, and `updatingBody` (fields to update).
174+
* @returns {Promise<object>} An object containing the updated fields and the request id.
175+
* @throws {Error} Logs and rethrows any error encountered during update. Throws error if the request does not exist.
176+
*/
177+
export const updateImpersonationRequest = async ( body: UpdateImpersonationRequestModelDto ) => {
178+
try {
179+
await impersonationRequestModel.doc(body.id).update({
180+
updatedAt: Timestamp.now(),
181+
lastModifiedBy: body.lastModifiedBy,
182+
...body.updatePayload,
183+
});
184+
185+
return {
186+
id:body.id,
187+
lastModifiedBy: body.lastModifiedBy,
188+
...body.updatePayload
189+
};
190+
} catch (error) {
191+
logger.error(`${ERROR_WHILE_UPDATING_REQUEST} for document ID: ${body.id}`, error);
192+
throw error;
193+
}
194+
};

routes/impersonation.ts

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

99
router.post(
1010
"/requests",
@@ -28,4 +28,11 @@ router.get(
2828
getImpersonationRequestByIdController
2929
);
3030

31-
module.exports = router;
31+
router.patch(
32+
"/requests/:id",
33+
authenticate,
34+
updateImpersonationRequestValidator,
35+
updateImpersonationRequestStatusController
36+
);
37+
38+
module.exports = router;

services/impersonationRequests.ts

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,22 @@ import {
33
LOG_ACTION,
44
REQUEST_LOG_TYPE,
55
REQUEST_STATE,
6-
TASK_REQUEST_MESSAGES
6+
TASK_REQUEST_MESSAGES,
7+
REQUEST_ALREADY_APPROVED,
8+
REQUEST_ALREADY_REJECTED,
9+
REQUEST_APPROVED_SUCCESSFULLY,
10+
REQUEST_DOES_NOT_EXIST,
11+
REQUEST_REJECTED_SUCCESSFULLY,
12+
UNAUTHORIZED_TO_UPDATE_REQUEST,
13+
ERROR_WHILE_UPDATING_REQUEST,
714
} from "../constants/requests";
8-
import { createImpersonationRequest } from "../models/impersonationRequests";
15+
import { createImpersonationRequest, updateImpersonationRequest, getImpersonationRequestById } from "../models/impersonationRequests";
916
import { fetchUser } from "../models/users";
1017
import { addLog } from "./logService";
1118
import { User } from "../typeDefinitions/users";
12-
import { NotFound } from "http-errors";
13-
import { CreateImpersonationRequestServiceBody, ImpersonationRequest } from "../types/impersonationRequest";
19+
import { NotFound, Forbidden } from "http-errors";
20+
import { CreateImpersonationRequestServiceBody, ImpersonationRequest, UpdateImpersonationRequestModelDto,
21+
UpdateImpersonationStatusModelResponse, } from "../types/impersonationRequest";
1422
const logger = require("../utils/logger");
1523

1624
/**
@@ -69,4 +77,56 @@ export const createImpersonationRequestService = async (
6977
logger.error(ERROR_WHILE_CREATING_REQUEST, error);
7078
throw error;
7179
}
72-
};
80+
};
81+
82+
/**
83+
* Validates and Updates an impersonation request and logs the update action.
84+
* @async
85+
* @function updateImpersonationRequestService
86+
* @param {UpdateImpersonationRequestModelDto} body - The update data for the impersonation request.
87+
* @returns {Promise<{ returnMessage: string, updatedRequest: UpdateImpersonationStatusModelResponse }>} The update result and message.
88+
* @throws {Error} If the update or logging fails.
89+
*/
90+
export const updateImpersonationRequestService = async (
91+
body: UpdateImpersonationRequestModelDto
92+
) => {
93+
try {
94+
const request = await getImpersonationRequestById(body.id);
95+
96+
if (!request) {
97+
throw new NotFound(REQUEST_DOES_NOT_EXIST);
98+
}
99+
100+
if (request.impersonatedUserId !== body.lastModifiedBy || request.status !== REQUEST_STATE.PENDING) {
101+
throw new Forbidden("You are not allowed for this Operation at the moment");
102+
}
103+
104+
const updatedRequest = await updateImpersonationRequest(body) as UpdateImpersonationStatusModelResponse;
105+
106+
const [logType, returnMessage] = updatedRequest.status === REQUEST_STATE.APPROVED
107+
? [REQUEST_LOG_TYPE.REQUEST_APPROVED, REQUEST_APPROVED_SUCCESSFULLY]
108+
: [REQUEST_LOG_TYPE.REQUEST_REJECTED, REQUEST_REJECTED_SUCCESSFULLY];
109+
110+
const requestLog = {
111+
type: logType,
112+
meta: {
113+
requestId: body.id,
114+
action: LOG_ACTION.UPDATE,
115+
createdBy: body.lastModifiedBy,
116+
},
117+
body: updatedRequest,
118+
};
119+
120+
await addLog(requestLog.type, requestLog.meta, requestLog.body);
121+
122+
const response = {
123+
returnMessage,
124+
updatedRequest,
125+
};
126+
127+
return response;
128+
} catch (error) {
129+
logger.error(ERROR_WHILE_UPDATING_REQUEST, error);
130+
throw error;
131+
}
132+
}

types/impersonationRequest.d.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,20 @@ export type UpdateImpersonationRequestStatusBody = {
4747
message?: string;
4848
}
4949

50+
51+
export type UpdateImpersonationRequestModelDto = {
52+
id: string;
53+
updatePayload: UpdateImpersonationRequestDataBody | UpdateImpersonationRequestStatusBody;
54+
lastModifiedBy: string;
55+
}
56+
57+
export type UpdateImpersonationStatusModelResponse = {
58+
status: REQUEST_STATE.APPROVED | REQUEST_STATE.REJECTED;
59+
message?: string;
60+
id: string;
61+
lastModifiedBy: string;
62+
}
63+
5064
export type ImpersonationRequestQuery = RequestQuery & {
5165
dev?: string;
5266
createdBy?: string;
@@ -71,7 +85,7 @@ export type CreateImpersonationRequest = Request & {
7185
query: ImpersonationRequestQuery;
7286
};
7387

74-
export type UpdateImpersonationRequestStatus = Request & {
88+
export type UpdateImpersonationRequest = Request & {
7589
userData: userData;
7690
body: UpdateImpersonationRequestStatusBody;
7791
query: ImpersonationRequestQuery;
@@ -90,6 +104,10 @@ export type GetImpersonationRequestByIdRequest = Request & {
90104
params: RequestParams;
91105
}
92106

107+
export type GetImpersonationControllerRequest = Request & {
108+
query: ImpersonationRequestQuery
109+
}
110+
93111
export type CreateImpersonationRequestServiceBody={
94112
userId: string;
95113
createdBy: string;

0 commit comments

Comments
 (0)