Skip to content

Commit

Permalink
Merge branch 'develop' into test/pagination-get-progresses-api
Browse files Browse the repository at this point in the history
  • Loading branch information
AnujChhikara authored Jan 5, 2025
2 parents d25f644 + a256841 commit 2e82d48
Show file tree
Hide file tree
Showing 10 changed files with 292 additions and 6 deletions.
4 changes: 4 additions & 0 deletions constants/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const REQUEST_TYPE = {
EXTENSION: "EXTENSION",
TASK: "TASK",
ALL: "ALL",
ONBOARDING: "ONBOARDING",
};

export const REQUEST_LOG_TYPE = {
Expand Down Expand Up @@ -53,3 +54,6 @@ export const TASK_REQUEST_MESSAGES = {
ERROR_CREATING_TASK_REQUEST: "Error while creating task request",
TASK_REQUEST_UPDATED_SUCCESS: "Task request updated successfully",
};

export const ONBOARDING_REQUEST_CREATED_SUCCESSFULLY = "Onboarding extension request created successfully"
export const UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST = "Only super user and onboarding user are authorized to create an onboarding extension request"
124 changes: 124 additions & 0 deletions controllers/onboardingExtension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import {
ERROR_WHILE_CREATING_REQUEST,
LOG_ACTION,
ONBOARDING_REQUEST_CREATED_SUCCESSFULLY,
REQUEST_ALREADY_PENDING,
REQUEST_LOG_TYPE,
REQUEST_STATE,
REQUEST_TYPE,
UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST,
} from "../constants/requests";
import { userState } from "../constants/userStatus";
import { addLog } from "../services/logService";
import { createRequest, getRequestByKeyValues } from "../models/requests";
import { fetchUser } from "../models/users";
import { getUserStatus } from "../models/userStatus";
import { User } from "../typeDefinitions/users";
import {
CreateOnboardingExtensionBody,
OnboardingExtension,
OnboardingExtensionCreateRequest,
OnboardingExtensionResponse
} from "../types/onboardingExtension";
import { convertDateStringToMilliseconds, getNewDeadline } from "../utils/requests";
import { convertDaysToMilliseconds } from "../utils/time";

/**
* Controller to handle the creation of onboarding extension requests.
*
* This function processes the request to create an extension for the onboarding period,
* validates the user status, checks existing requests, calculates new deadlines,
* and stores the new request in the database with logging.
*
* @param {OnboardingExtensionCreateRequest} req - The Express request object containing the body with extension details.
* @param {OnboardingExtensionResponse} res - The Express response object used to send back the response.
* @returns {Promise<OnboardingExtensionResponse>} Resolves to a response with the status and data or an error message.
*/
export const createOnboardingExtensionRequestController = async (req: OnboardingExtensionCreateRequest, res: OnboardingExtensionResponse): Promise<OnboardingExtensionResponse> => {
try {

const data = req.body as CreateOnboardingExtensionBody;
const {user, userExists} = await fetchUser({discordId: data.userId});

if(!userExists) {
return res.boom.notFound("User not found");
}

const { id: userId, discordJoinedAt, username} = user as User;
const { data: userStatus } = await getUserStatus(userId);

if(!userStatus || userStatus.currentStatus.state != userState.ONBOARDING){
return res.boom.forbidden(UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST);
}

const latestExtensionRequest: OnboardingExtension = await getRequestByKeyValues({
userId: userId,
type: REQUEST_TYPE.ONBOARDING
});

if(latestExtensionRequest && latestExtensionRequest.state === REQUEST_STATE.PENDING){
return res.boom.badRequest(REQUEST_ALREADY_PENDING);
}

const millisecondsInThirtyOneDays = convertDaysToMilliseconds(31);
const numberOfDaysInMillisecond = convertDaysToMilliseconds(data.numberOfDays);
const { isDate, milliseconds: discordJoinedDateInMillisecond } = convertDateStringToMilliseconds(discordJoinedAt);

if(!isDate){
logger.error(ERROR_WHILE_CREATING_REQUEST, "Invalid date");
return res.boom.badImplementation(ERROR_WHILE_CREATING_REQUEST);
}

let requestNumber: number;
let oldEndsOn: number;
const currentDate = Date.now();

if(!latestExtensionRequest){
requestNumber = 1;
oldEndsOn = discordJoinedDateInMillisecond + millisecondsInThirtyOneDays;
}else if(latestExtensionRequest.state === REQUEST_STATE.REJECTED) {
requestNumber = latestExtensionRequest.requestNumber + 1;
oldEndsOn = latestExtensionRequest.oldEndsOn;
}else{
requestNumber = latestExtensionRequest.requestNumber + 1;
oldEndsOn = latestExtensionRequest.newEndsOn;
}

const newEndsOn = getNewDeadline(currentDate, oldEndsOn, numberOfDaysInMillisecond);

const onboardingExtension = await createRequest({
type: REQUEST_TYPE.ONBOARDING,
state: REQUEST_STATE.PENDING,
userId: userId,
requestedBy: username,
oldEndsOn: oldEndsOn,
newEndsOn: newEndsOn,
reason: data.reason,
requestNumber: requestNumber,
});

const onboardingExtensionLog = {
type: REQUEST_LOG_TYPE.REQUEST_CREATED,
meta: {
requestId: onboardingExtension.id,
action: LOG_ACTION.CREATE,
userId: userId,
createdAt: Date.now(),
},
body: onboardingExtension,
};

await addLog(onboardingExtensionLog.type, onboardingExtensionLog.meta, onboardingExtensionLog.body);

return res.status(201).json({
message: ONBOARDING_REQUEST_CREATED_SUCCESSFULLY,
data: {
id: onboardingExtension.id,
...onboardingExtension,
}
});
}catch (err) {
logger.error(ERROR_WHILE_CREATING_REQUEST, err);
return res.boom.badImplementation(ERROR_WHILE_CREATING_REQUEST);
}
};
6 changes: 5 additions & 1 deletion controllers/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import { createTaskExtensionRequest, updateTaskExtensionRequest } from "./extens
import { UpdateRequest } from "../types/requests";
import { TaskRequestRequest } from "../types/taskRequests";
import { createTaskRequestController } from "./taskRequestsv2";
import { OnboardingExtensionCreateRequest, OnboardingExtensionResponse } from "../types/onboardingExtension";
import { createOnboardingExtensionRequestController } from "./onboardingExtension";

export const createRequestController = async (
req: OooRequestCreateRequest | ExtensionRequestRequest | TaskRequestRequest,
req: OooRequestCreateRequest | ExtensionRequestRequest | TaskRequestRequest | OnboardingExtensionCreateRequest,
res: CustomResponse
) => {
const type = req.body.type;
Expand All @@ -26,6 +28,8 @@ export const createRequestController = async (
return await createTaskExtensionRequest(req as ExtensionRequestRequest, res as ExtensionRequestResponse);
case REQUEST_TYPE.TASK:
return await createTaskRequestController(req as TaskRequestRequest, res as CustomResponse);
case REQUEST_TYPE.ONBOARDING:
return await createOnboardingExtensionRequestController(req as OnboardingExtensionCreateRequest, res as OnboardingExtensionResponse);
default:
return res.boom.badRequest("Invalid request type");
}
Expand Down
30 changes: 30 additions & 0 deletions middlewares/skipAuthenticateForOnboardingExtension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NextFunction, Request, Response } from "express"
import { REQUEST_TYPE } from "../constants/requests";
/**
* Middleware to selectively authenticate or verify Discord bot based on the request type.
* Specifically handles requests for onboarding extensions by skipping authentication.
*
* @param {Function} authenticate - The authentication middleware to apply for general requests.
* @param {Function} verifyDiscordBot - The middleware to verify requests from a Discord bot.
* @returns {Function} A middleware function that processes the request based on its type.
*
* @example
* app.use(skipAuthenticateForOnboardingExtensionRequest(authenticate, verifyDiscordBot));
*/
export const skipAuthenticateForOnboardingExtensionRequest = (authenticate, verifyDiscordBot) => {
return async (req: Request, res: Response, next: NextFunction) => {
const type = req.body.type;
const dev = req.query.dev;

if(type === REQUEST_TYPE.ONBOARDING){
if (dev != "true"){
return res.status(501).json({
message: "Feature not implemented"
})
}
return await verifyDiscordBot(req, res, next);
}

return await authenticate(req, res, next)
}
}
42 changes: 42 additions & 0 deletions middlewares/validators/onboardingExtensionRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import joi from "joi";
import { NextFunction } from "express";
import { REQUEST_TYPE } from "../../constants/requests";
import { OnboardingExtensionCreateRequest, OnboardingExtensionResponse } from "../../types/onboardingExtension";

export const createOnboardingExtensionRequestValidator = async (
req: OnboardingExtensionCreateRequest,
_res: OnboardingExtensionResponse,
_next: NextFunction
) => {

const schema = joi
.object()
.strict()
.keys({
numberOfDays: joi.number().required().positive().integer().min(1).messages({
"number.base": "numberOfDays must be a number",
"any.required": "numberOfDays is required",
"number.positive": "numberOfDays must be positive",
"number.min": "numberOfDays must be greater than zero",
"number.integer": "numberOfDays must be a integer"
}),
reason: joi.string().required().messages({
"string.empty": "reason cannot be empty",
"any.required": "reason is required",
}),
type: joi.string().valid(REQUEST_TYPE.ONBOARDING).required().messages({
"string.empty": "type cannot be empty",
"any.required": "type is required",
}),
userId: joi.string().required().messages({
"string.empty": "userId cannot be empty",
"any.required": "userId is required"
})
});
try{
await schema.validateAsync(req.body, { abortEarly: false });
}catch(error){
logger.error(`Error while validating request payload`, error);
throw error;
}
};
9 changes: 7 additions & 2 deletions middlewares/validators/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import { ExtensionRequestRequest, ExtensionRequestResponse } from "../../types/e
import { CustomResponse } from "../../typeDefinitions/global";
import { UpdateRequest } from "../../types/requests";
import { TaskRequestRequest, TaskRequestResponse } from "../../types/taskRequests";
import { createOnboardingExtensionRequestValidator } from "./onboardingExtensionRequest";
import { OnboardingExtensionCreateRequest, OnboardingExtensionResponse } from "../../types/onboardingExtension";

export const createRequestsMiddleware = async (
req: OooRequestCreateRequest|ExtensionRequestRequest | TaskRequestRequest,
req: OooRequestCreateRequest|ExtensionRequestRequest | TaskRequestRequest | OnboardingExtensionCreateRequest,
res: CustomResponse,
next: NextFunction
) => {
Expand All @@ -28,6 +30,9 @@ export const createRequestsMiddleware = async (
case REQUEST_TYPE.TASK:
await createTaskRequestValidator(req as TaskRequestRequest, res as TaskRequestResponse, next);
break;
case REQUEST_TYPE.ONBOARDING:
await createOnboardingExtensionRequestValidator(req as OnboardingExtensionCreateRequest, res as OnboardingExtensionResponse, next);
break;
default:
res.boom.badRequest(`Invalid request type: ${type}`);
}
Expand All @@ -36,7 +41,7 @@ export const createRequestsMiddleware = async (
} catch (error) {
const errorMessages = error.details.map((detail:any) => detail.message);
logger.error(`Error while validating request payload : ${errorMessages}`);
res.boom.badRequest(errorMessages);
return res.boom.badRequest(errorMessages);
}
};

Expand Down
4 changes: 3 additions & 1 deletion routes/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ const { SUPERUSER } = require("../constants/roles");
import authenticate from "../middlewares/authenticate";
import { createRequestsMiddleware,updateRequestsMiddleware,getRequestsMiddleware } from "../middlewares/validators/requests";
import { createRequestController , updateRequestController, getRequestsController} from "../controllers/requests";
import { skipAuthenticateForOnboardingExtensionRequest } from "../middlewares/skipAuthenticateForOnboardingExtension";
import { verifyDiscordBot } from "../middlewares/authorizeBot";

router.get("/", getRequestsMiddleware, getRequestsController);
router.post("/",authenticate, createRequestsMiddleware, createRequestController);
router.post("/", skipAuthenticateForOnboardingExtensionRequest(authenticate, verifyDiscordBot), createRequestsMiddleware, createRequestController);
router.put("/:id",authenticate, authorizeRoles([SUPERUSER]), updateRequestsMiddleware, updateRequestController);
module.exports = router;
6 changes: 4 additions & 2 deletions typeDefinitions/users.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type User = {
id?: string
username?: string;
first_name?: string;
last_name?: string;
Expand All @@ -17,7 +18,8 @@ export type User = {
roles?: {
member?: boolean;
in_discord?: boolean;
};
super_user?: boolean;
}
tokens?: {
githubAccessToken?: string;
};
Expand All @@ -29,4 +31,4 @@ export type User = {
};
incompleteUserDetails?: boolean;
nickname_synced?: boolean;
};
};
39 changes: 39 additions & 0 deletions types/onboardingExtension.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Request, Response } from "express";
import { Boom } from "express-boom";
import { REQUEST_STATE, REQUEST_TYPE } from "../constants/requests";
import { RequestQuery } from "./requests";

export type OnboardingExtension = {
id: string;
type: REQUEST_TYPE.ONBOARDING;
oldEndsOn: number;
newEndsOn: number;
message?: string;
reason: string;
requestedBy: string;
state: REQUEST_STATE;
lastModifiedBy?: string;
createdAt: Timestamp;
updatedAt: Timestamp;
requestNumber: number;
userId: string;
}

export type CreateOnboardingExtensionBody = {
type: string;
numberOfDays: number;
userId: string;
reason: string;
}

export type OnboardingExtensionRequestQuery = RequestQuery & {
dev?: string
}

export type OnboardingExtensionResponse = Response & {
boom: Boom
}
export type OnboardingExtensionCreateRequest = Request & {
body: CreateOnboardingExtensionBody;
query: OnboardingExtensionRequestQuery;
}
34 changes: 34 additions & 0 deletions utils/requests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Calculates the new deadline based on the current date, the old end date, and the additional duration in milliseconds.
*
* @param {number} currentDate - The current date as a timestamp in milliseconds.
* @param {number} oldEndsOn - The previous end date as a timestamp in milliseconds.
* @param {number} numberOfDaysInMillisecond - The duration to extend the deadline, in milliseconds.
* @returns {number} The new deadline as a timestamp in milliseconds.
*/
export const getNewDeadline = (currentDate: number, oldEndsOn: number, numberOfDaysInMillisecond: number): number => {
if (currentDate > oldEndsOn) {
return currentDate + numberOfDaysInMillisecond;
}
return oldEndsOn + numberOfDaysInMillisecond;
};

/**
* Converts a date string into a timestamp in milliseconds.
* Validates whether the provided string is a valid date format.
*
* @param {string} date - The date string to convert (e.g., "2024-10-17T16:10:52.668Z").
* @returns {{ isDate: boolean, milliseconds?: number }} An object indicating validity and the timestamp if valid.
*/
export const convertDateStringToMilliseconds = (date: string): { isDate: boolean; milliseconds?: number; } => {
const milliseconds = Date.parse(date);
if (!milliseconds) {
return {
isDate: false,
};
}
return {
isDate: true,
milliseconds,
};
};

0 comments on commit 2e82d48

Please sign in to comment.