generated from Real-Dev-Squad/website-template
-
Notifications
You must be signed in to change notification settings - Fork 264
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'develop' into test/pagination-get-progresses-api
- Loading branch information
Showing
10 changed files
with
292 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; | ||
}; |