diff --git a/constants/requests.ts b/constants/requests.ts index f0fdb9907..1173a883e 100644 --- a/constants/requests.ts +++ b/constants/requests.ts @@ -15,6 +15,7 @@ export const REQUEST_TYPE = { EXTENSION: "EXTENSION", TASK: "TASK", ALL: "ALL", + ONBOARDING: "ONBOARDING", }; export const REQUEST_LOG_TYPE = { @@ -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" \ No newline at end of file diff --git a/controllers/onboardingExtension.ts b/controllers/onboardingExtension.ts new file mode 100644 index 000000000..57fc524db --- /dev/null +++ b/controllers/onboardingExtension.ts @@ -0,0 +1,107 @@ +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 "../models/logs"; +import { createRequest, getRequestByKeyValues } from "../models/requests"; +import { fetchUser } from "../models/users"; +import { getUserStatus } from "../models/userStatus"; +import { User } from "../typeDefinitions/users"; +import { OnboardingExtension, OnboardingExtensionCreateRequest, OnboardingExtensionResponse } from "../types/onboardingExtension"; + +export const createOnboardingExtensionRequestController = async (req: OnboardingExtensionCreateRequest, res: OnboardingExtensionResponse) => { + try { + + const data = req.body; + 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.unauthorized(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 thirtyOneDaysInMillisecond = 31*24*60*60*1000; + const discordJoinedDateInMillisecond = new Date(discordJoinedAt).getTime(); + const numberOfDaysInMillisecond = Math.floor(data.numberOfDays)*24*60*60*1000; + + let requestNumber: number; + let oldEndsOn: number; + let newEndsOn: number; + const currentDate = Date.now(); + + if(!latestExtensionRequest){ + requestNumber = 1; + oldEndsOn = discordJoinedDateInMillisecond + thirtyOneDaysInMillisecond; + }else if(latestExtensionRequest.state === REQUEST_STATE.REJECTED) { + requestNumber = latestExtensionRequest.requestNumber + 1; + oldEndsOn = latestExtensionRequest.oldEndsOn; + }else{ + requestNumber = latestExtensionRequest.requestNumber + 1; + oldEndsOn = latestExtensionRequest.newEndsOn; + } + + if(currentDate > oldEndsOn){ + newEndsOn = currentDate + numberOfDaysInMillisecond; + } + else { + newEndsOn = 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); + } +}; \ No newline at end of file diff --git a/controllers/requests.ts b/controllers/requests.ts index d4bf87179..ab0333fd0 100644 --- a/controllers/requests.ts +++ b/controllers/requests.ts @@ -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; @@ -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"); } diff --git a/middlewares/skipAuthenticateForOnboardingExtension.ts b/middlewares/skipAuthenticateForOnboardingExtension.ts new file mode 100644 index 000000000..c67686fd8 --- /dev/null +++ b/middlewares/skipAuthenticateForOnboardingExtension.ts @@ -0,0 +1,20 @@ +import { NextFunction, Request, Response } from "express" +import { REQUEST_TYPE } from "../constants/requests"; + +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) + } +} \ No newline at end of file diff --git a/middlewares/validators/onboardingExtensionRequest.ts b/middlewares/validators/onboardingExtensionRequest.ts new file mode 100644 index 000000000..6f1a6f2b3 --- /dev/null +++ b/middlewares/validators/onboardingExtensionRequest.ts @@ -0,0 +1,38 @@ +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" + }) + }); + + await schema.validateAsync(req.body, { abortEarly: false }); +}; diff --git a/middlewares/validators/requests.ts b/middlewares/validators/requests.ts index 0f5c1909e..2cf3b6983 100644 --- a/middlewares/validators/requests.ts +++ b/middlewares/validators/requests.ts @@ -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 ) => { @@ -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}`); } @@ -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); } }; diff --git a/routes/requests.ts b/routes/requests.ts index 5cda581b6..f04cba0c6 100644 --- a/routes/requests.ts +++ b/routes/requests.ts @@ -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; diff --git a/typeDefinitions/users.ts b/typeDefinitions/users.ts index d74fa4a6c..489cd48c0 100644 --- a/typeDefinitions/users.ts +++ b/typeDefinitions/users.ts @@ -1,4 +1,5 @@ export type User = { + id?: string username?: string; first_name?: string; last_name?: string; @@ -17,7 +18,8 @@ export type User = { roles?: { member?: boolean; in_discord?: boolean; - }; + super_user?: boolean; + } tokens?: { githubAccessToken?: string; }; @@ -29,4 +31,4 @@ export type User = { }; incompleteUserDetails?: boolean; nickname_synced?: boolean; -}; +}; \ No newline at end of file diff --git a/types/onboardingExtension.d.ts b/types/onboardingExtension.d.ts new file mode 100644 index 000000000..ebb2717fe --- /dev/null +++ b/types/onboardingExtension.d.ts @@ -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; +} \ No newline at end of file