Skip to content

Commit c014c5d

Browse files
resolve: merge conflict
2 parents 0ad8878 + 0f75af9 commit c014c5d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+4615
-487
lines changed

config/custom-environment-variables.cjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ module.exports = {
9090
__name: "USER_TOKEN_REFRESH_TTL",
9191
__format: "number",
9292
},
93+
impersonationTtl: {
94+
__name: "USER_TOKEN_IMPERSONATION_TTL",
95+
__format: "number",
96+
},
9397
publicKey: "PUBLIC_KEY",
9498
privateKey: "PRIVATE_KEY",
9599
},
@@ -98,6 +102,10 @@ module.exports = {
98102
botPublicKey: "BOT_PUBLIC_KEY",
99103
},
100104

105+
discordService: {
106+
publicKey: "DISCORD_SERVICE_PUBLIC_KEY",
107+
},
108+
101109
cronJobHandler: {
102110
publicKey: "CRON_JOB_PUBLIC_KEY",
103111
},

config/default.cjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ module.exports = {
8888
cookieV2Name: `rds-session-v2-${NODE_ENV}`,
8989
ttl: 30 * 24 * 60 * 60, // in seconds
9090
refreshTtl: 180 * 24 * 60 * 60, // in seconds
91+
impersonationTtl: 15 * 60, // in seconds
9192
publicKey: "<publicKey>",
9293
privateKey: "<privateKey>",
9394
},
@@ -96,6 +97,10 @@ module.exports = {
9697
botPublicKey: "<botpublicKey>",
9798
},
9899

100+
discordService: {
101+
publicKey: "DISCORD_SERVICE_PUBLIC_KEY",
102+
},
103+
99104
// Cloudinary keys
100105
cloudinary: {
101106
cloud_name: "Cloud_name",

constants/bot.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
const CLOUDFLARE_WORKER = "Cloudflare Worker";
2-
const BAD_TOKEN = "BAD.JWT.TOKEN";
3-
const CRON_JOB_HANDLER = "Cron Job Handler";
1+
export const CLOUDFLARE_WORKER = "Cloudflare Worker";
2+
export const BAD_TOKEN = "BAD.JWT.TOKEN";
3+
export const CRON_JOB_HANDLER = "Cron Job Handler";
4+
export const DISCORD_SERVICE = "Discord Service";
45

5-
const Services = {
6+
export const Services = {
67
CLOUDFLARE_WORKER: CLOUDFLARE_WORKER,
78
CRON_JOB_HANDLER: CRON_JOB_HANDLER,
89
};
910

10-
export { CLOUDFLARE_WORKER, BAD_TOKEN, CRON_JOB_HANDLER, Services };
11+
export const DiscordServiceHeader = {
12+
name: "x-service-name"
13+
}
14+
15+
export default { CLOUDFLARE_WORKER, BAD_TOKEN, CRON_JOB_HANDLER, Services, DISCORD_SERVICE, DiscordServiceHeader };

constants/requests.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ export const REQUEST_ALREADY_REJECTED = "Request already rejected";
3939
export const ERROR_WHILE_FETCHING_REQUEST = "Error while fetching request";
4040
export const ERROR_WHILE_CREATING_REQUEST = "Error while creating request";
4141
export const ERROR_WHILE_UPDATING_REQUEST = "Error while updating request";
42+
export const ERROR_WHILE_ACKNOWLEDGING_REQUEST = "Error while acknowledging request";
4243

44+
export const REQUEST_ID_REQUIRED = "Request id is required";
4345
export const REQUEST_DOES_NOT_EXIST = "Request does not exist";
4446
export const REQUEST_ALREADY_PENDING = "Request already exists please wait for approval or rejection";
4547
export const UNAUTHORIZED_TO_CREATE_OOO_REQUEST = "Unauthorized to create OOO request";
@@ -68,3 +70,14 @@ export const INVALID_REQUEST_TYPE = "Invalid request type";
6870
export const INVALID_REQUEST_DEADLINE = "New deadline of the request must be greater than old deadline";
6971
export const REQUEST_UPDATED_SUCCESSFULLY = "Request updated successfully";
7072
export const UNAUTHORIZED_TO_UPDATE_REQUEST = "Unauthorized to update request";
73+
74+
export const FEATURE_NOT_IMPLEMENTED = "Feature not implemented";
75+
76+
export const INVALID_ACTION_PARAM = "Invalid 'action' parameter: must be either 'START' or 'STOP'";
77+
78+
export const OPERATION_NOT_ALLOWED = "You are not allowed for this operation at the moment";
79+
80+
export const IMPERSONATION_LOG_TYPE = {
81+
SESSION_STARTED:"SESSION_STARTED",
82+
SESSION_STOPPED:"SESSION_STOPPED"
83+
}
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import {
2+
ERROR_WHILE_CREATING_REQUEST,
3+
ERROR_WHILE_FETCHING_REQUEST,
4+
REQUEST_FETCHED_SUCCESSFULLY,
5+
REQUEST_DOES_NOT_EXIST,
6+
ERROR_WHILE_UPDATING_REQUEST,
7+
REQUEST_CREATED_SUCCESSFULLY,
8+
OPERATION_NOT_ALLOWED
9+
} from "../constants/requests";
10+
import { createImpersonationRequestService, updateImpersonationRequestService, generateImpersonationTokenService, startImpersonationService, stopImpersonationService } from "../services/impersonationRequests";
11+
import { getImpersonationRequestById, getImpersonationRequests } from "../models/impersonationRequests";
12+
import {
13+
CreateImpersonationRequest,
14+
CreateImpersonationRequestBody,
15+
UpdateImpersonationRequest,
16+
UpdateImpersonationRequestStatusBody,
17+
ImpersonationRequestResponse,
18+
GetImpersonationControllerRequest,
19+
GetImpersonationRequestByIdRequest,
20+
ImpersonationSessionRequest
21+
} from "../types/impersonationRequest";
22+
import { getPaginatedLink } from "../utils/helper";
23+
import { NextFunction } from "express";
24+
import logger from "../utils/logger";
25+
26+
/**
27+
* Controller to handle creation of an impersonation request.
28+
*
29+
* @param {CreateImpersonationRequest} req - Express request object with user and body data.
30+
* @param {ImpersonationRequestResponse} res - Express response object.
31+
* @param {NextFunction} next - Express next middleware function.
32+
* @returns {Promise<ImpersonationRequestResponse | void>} Returns the created request or passes error to next middleware.
33+
*/
34+
export const createImpersonationRequestController = async (
35+
req: CreateImpersonationRequest,
36+
res: ImpersonationRequestResponse,
37+
next: NextFunction
38+
): Promise<ImpersonationRequestResponse | void> => {
39+
try {
40+
const { createdFor, reason } = req.body as CreateImpersonationRequestBody;
41+
const userId = req.userData?.id;
42+
43+
const impersonationRequest = await createImpersonationRequestService({
44+
createdBy: userId,
45+
createdFor,
46+
reason
47+
});
48+
49+
return res.status(201).json({
50+
message: REQUEST_CREATED_SUCCESSFULLY,
51+
data: {
52+
...impersonationRequest
53+
}
54+
});
55+
} catch (error) {
56+
logger.error(ERROR_WHILE_CREATING_REQUEST, error);
57+
next(error);
58+
}
59+
};
60+
61+
/**
62+
* Controller to fetch an impersonation request by its ID.
63+
*
64+
* @param {GetImpersonationRequestByIdRequest} req - Express request object containing `id` parameter.
65+
* @param {ImpersonationRequestResponse} res - Express response object.
66+
* @returns {Promise<ImpersonationRequestResponse>} Returns the request if found, or 404 if it doesn't exist.
67+
*/
68+
export const getImpersonationRequestByIdController = async (
69+
req: GetImpersonationRequestByIdRequest,
70+
res: ImpersonationRequestResponse
71+
): Promise<ImpersonationRequestResponse> => {
72+
const id = req.params.id;
73+
try {
74+
const request = await getImpersonationRequestById(id);
75+
76+
if (!request) {
77+
return res.status(404).json({
78+
message: REQUEST_DOES_NOT_EXIST,
79+
});
80+
}
81+
82+
return res.status(200).json({
83+
message: REQUEST_FETCHED_SUCCESSFULLY,
84+
data: request,
85+
});
86+
87+
} catch (error) {
88+
logger.error(ERROR_WHILE_FETCHING_REQUEST, error);
89+
return res.boom.badImplementation(ERROR_WHILE_FETCHING_REQUEST);
90+
}
91+
};
92+
93+
/**
94+
* Controller to fetch impersonation requests with optional filtering and pagination.
95+
*
96+
* @param {GetImpersonationControllerRequest} req - Express request object containing query parameters.
97+
* @param {ImpersonationRequestResponse} res - Express response object.
98+
* @returns {Promise<ImpersonationRequestResponse>} Returns paginated impersonation request data or 204 if none found.
99+
*/
100+
export const getImpersonationRequestsController = async (
101+
req: GetImpersonationControllerRequest,
102+
res: ImpersonationRequestResponse
103+
): Promise<ImpersonationRequestResponse> => {
104+
try {
105+
const { query } = req;
106+
107+
const requests = await getImpersonationRequests(query);
108+
if (!requests || requests.allRequests.length === 0) {
109+
return res.status(204).send();
110+
}
111+
112+
const { allRequests, next, prev } = requests;
113+
const count = allRequests.length;
114+
115+
let nextUrl = null;
116+
let prevUrl = null;
117+
if (next) {
118+
nextUrl = getPaginatedLink({
119+
endpoint: "/impersonation/requests",
120+
query,
121+
cursorKey: "next",
122+
docId: next,
123+
});
124+
}
125+
if (prev) {
126+
prevUrl = getPaginatedLink({
127+
endpoint: "/impersonation/requests",
128+
query,
129+
cursorKey: "prev",
130+
docId: prev,
131+
});
132+
}
133+
134+
return res.status(200).json({
135+
message: REQUEST_FETCHED_SUCCESSFULLY,
136+
data: allRequests,
137+
next: nextUrl,
138+
prev: prevUrl,
139+
count,
140+
});
141+
} catch (err) {
142+
logger.error(ERROR_WHILE_FETCHING_REQUEST, err);
143+
return res.boom.badImplementation(ERROR_WHILE_FETCHING_REQUEST);
144+
}
145+
};
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+
};
183+
184+
185+
186+
/**
187+
* Controller to handle impersonation session actions (START or STOP).
188+
*
189+
* @param {ImpersonationSessionRequest} req - Express request object containing user data, query params, and impersonation flag.
190+
* @param {ImpersonationRequestResponse} res - Express response object used to send the response.
191+
* @param {NextFunction} next - Express next middleware function for error handling.
192+
* @returns {Promise<ImpersonationRequestResponse>} Sends a JSON response with updated request data and sets authentication cookies based on action.
193+
*
194+
* @throws {Forbidden} If the action is invalid or STOP is requested without an active impersonation session.
195+
*/
196+
export const impersonationController = async (
197+
req: ImpersonationSessionRequest,
198+
res: ImpersonationRequestResponse,
199+
next: NextFunction
200+
): Promise<ImpersonationRequestResponse | void> => {
201+
const { action } = req.query;
202+
const requestId = req.params.id;
203+
const userId = req.userData?.id;
204+
let authCookie;
205+
let response;
206+
try {
207+
208+
if (action === "START") {
209+
authCookie = await generateImpersonationTokenService(requestId, action);
210+
response = await startImpersonationService({ requestId, userId });
211+
}
212+
213+
if (action === "STOP") {
214+
authCookie = await generateImpersonationTokenService(requestId, action);
215+
response = await stopImpersonationService({ requestId, userId });
216+
}
217+
218+
res.clearCookie(authCookie.name);
219+
res.cookie(authCookie.name, authCookie.value, authCookie.options);
220+
221+
return res.status(200).json({
222+
message: response.returnMessage,
223+
data: response.updatedRequest
224+
});
225+
} catch (error) {
226+
logger.error(`Failed to process impersonation ${action} for requestId=${requestId}, userId=${userId}`, error);
227+
return next(error);
228+
}
229+
};

controllers/oooRequests.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@ export const createOooRequestController = async (
6666
}
6767

6868
const latestOooRequest: OooStatusRequest = await getRequestByKeyValues({
69-
userId,
70-
type: REQUEST_TYPE.OOO,
71-
status: REQUEST_STATE.PENDING,
69+
requestedBy: userId,
70+
type: REQUEST_TYPE.OOO,
71+
status: REQUEST_STATE.PENDING,
7272
});
7373

7474
if (latestOooRequest) {
@@ -79,7 +79,7 @@ export const createOooRequestController = async (
7979
return res.boom.conflict(REQUEST_ALREADY_PENDING);
8080
}
8181

82-
await createOooRequest(requestBody, username, userId);
82+
await createOooRequest(requestBody, userId);
8383

8484
return res.status(201).json({
8585
message: REQUEST_CREATED_SUCCESSFULLY,
@@ -149,3 +149,35 @@ export const updateOooRequestController = async (req: UpdateRequest, res: Custom
149149
return res.boom.badImplementation(ERROR_WHILE_UPDATING_REQUEST);
150150
}
151151
};
152+
/**
153+
* Acknowledges an Out-of-Office (OOO) request by updating its status to approved or rejected
154+
*
155+
* @param {AcknowledgeOooRequest} req - The request object containing acknowledgment details (status, comment) and request ID in params
156+
* @param {OooRequestResponse} res - The response object for sending success/error responses
157+
* @param {NextFunction} next - Express next function for error handling
158+
* @returns {Promise<OooRequestResponse>} Resolves with success message or passes error to next middleware
159+
*/
160+
export const acknowledgeOooRequestController = async (
161+
req: AcknowledgeOooRequest,
162+
res: OooRequestResponse,
163+
next: NextFunction
164+
)
165+
: Promise<OooRequestResponse> => {
166+
try {
167+
168+
const requestBody = req.body;
169+
const superUserId = req.userData.id;
170+
const requestId = req.params.id;
171+
172+
const response = await acknowledgeOooRequestService(requestId, requestBody, superUserId);
173+
174+
return res.status(200).json({
175+
message: response.message,
176+
});
177+
}
178+
catch(error){
179+
logger.error(ERROR_WHILE_ACKNOWLEDGING_REQUEST, error);
180+
next(error);
181+
return;
182+
}
183+
};

controllers/requests.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,12 @@ export const getRequestsController = async (req: any, res: any) => {
121121
* @param {CustomResponse} res - The response object.
122122
* @returns {Promise<void>} Resolves or sends an error for invalid types.
123123
*/
124-
export const updateRequestBeforeAcknowledgedController = async (req: Request, res: CustomResponse) => {
124+
export const updateRequestBeforeAcknowledgedController = async (req: Request, res: CustomResponse, next: NextFunction) => {
125125
const type = req.body.type;
126126
switch(type){
127+
case REQUEST_TYPE.OOO:
128+
await acknowledgeOooRequestController(req as AcknowledgeOooRequest, res as OooRequestResponse, next);
129+
break;
127130
case REQUEST_TYPE.ONBOARDING:
128131
await updateOnboardingExtensionRequestController(req as UpdateOnboardingExtensionRequest, res as OnboardingExtensionResponse);
129132
break;

0 commit comments

Comments
 (0)