Skip to content

Commit cbb07cd

Browse files
committed
Merge branch 'develop' into fix/assignee-handle
2 parents 4b97842 + 21fd72b commit cbb07cd

File tree

18 files changed

+2915
-1437
lines changed

18 files changed

+2915
-1437
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ on:
1111
jobs:
1212
build:
1313
runs-on: ubuntu-latest
14+
timeout-minutes: 5
1415

1516
strategy:
1617
matrix:

constants/monitor.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const RESOURCE_CREATED_SUCCESSFULLY = "Resource created successfully.";
2+
const RESOURCE_UPDATED_SUCCESSFULLY = "Resource updated successfully.";
3+
const RESOURCE_RETRIEVED_SUCCESSFULLY = "Resource retrieved successfully.";
4+
const RESOURCE_NOT_FOUND = "Resource not found.";
5+
const RESOURCE_ALREADY_TRACKED = "Resource is already being tracked.";
6+
7+
const RESPONSE_MESSAGES = {
8+
RESOURCE_CREATED_SUCCESSFULLY,
9+
RESOURCE_UPDATED_SUCCESSFULLY,
10+
RESOURCE_RETRIEVED_SUCCESSFULLY,
11+
RESOURCE_NOT_FOUND,
12+
RESOURCE_ALREADY_TRACKED,
13+
};
14+
15+
module.exports = { RESPONSE_MESSAGES };

constants/rateLimiting.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const TOO_MANY_REQUESTS = {
2+
ERROR_TYPE: "Too Many Requests",
3+
STATUS_CODE: 429,
4+
};
5+
6+
module.exports = {
7+
TOO_MANY_REQUESTS,
8+
};

controllers/monitor.js

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
const { Conflict, NotFound } = require("http-errors");
2+
const { INTERNAL_SERVER_ERROR_MESSAGE } = require("../constants/progresses");
3+
const {
4+
createTrackedProgressDocument,
5+
updateTrackedProgressDocument,
6+
getTrackedProgressDocuments,
7+
} = require("../models/monitor");
8+
const { RESPONSE_MESSAGES } = require("../constants/monitor");
9+
const { RESOURCE_CREATED_SUCCESSFULLY, RESOURCE_UPDATED_SUCCESSFULLY, RESOURCE_RETRIEVED_SUCCESSFULLY } =
10+
RESPONSE_MESSAGES;
11+
/**
12+
* @typedef {Object} TrackedProgressRequestBody
13+
* @property {string} type - The type of tracked progress ("user" or "task").
14+
* @property {string} [userId] - The user ID (required if type is "user").
15+
* @property {string} [taskId] - The task ID (required if type is "task").
16+
* @property {boolean} monitored - Indicates if the progress is currently being tracked.
17+
* @property {number} [frequency=1] - The frequency of tracking.By default 1 if not specified
18+
*/
19+
20+
/**
21+
* @typedef {Object} TrackedProgressResponseData
22+
* @property {string} id - The ID of the tracked progress document.
23+
* @property {string} type - The type of tracked progress ("user" or "task").
24+
* @property {string} userId - The user ID.
25+
* @property {boolean} monitored - Indicates if the progress is currently being tracked.
26+
* @property {number} frequency - The frequency of tracking.
27+
* @property {string} createdAt - The timestamp when the document was created.
28+
* @property {string} updatedAt - The timestamp when the document was last updated.
29+
*/
30+
31+
/**
32+
* @typedef {Object} TrackedProgressResponse
33+
* @property {TrackedProgressResponseData} data - The data of the tracked progress document.
34+
* @property {string} message - The success message.
35+
*/
36+
37+
/**
38+
* Controller function for creating a tracked progress document.
39+
*
40+
* @param {Express.Request} req - The Express request object.
41+
* @param {TrackedProgressRequestBody} req.body - The Request body object.
42+
* @param {Express.Response} res - The Express response object.
43+
* @returns {Promise<void>} - A Promise that resolves when the response has been sent.
44+
*/
45+
46+
const createTrackedProgressController = async (req, res) => {
47+
try {
48+
const data = await createTrackedProgressDocument({ ...req.body });
49+
return res.status(201).json({
50+
message: RESOURCE_CREATED_SUCCESSFULLY,
51+
data,
52+
});
53+
} catch (error) {
54+
if (error instanceof Conflict) {
55+
return res.status(409).json({
56+
message: error.message,
57+
});
58+
} else if (error instanceof NotFound) {
59+
return res.status(404).json({
60+
message: error.message,
61+
});
62+
}
63+
return res.status(500).json({
64+
message: INTERNAL_SERVER_ERROR_MESSAGE,
65+
});
66+
}
67+
};
68+
69+
/**
70+
* @typedef {Object} UpdateTrackedProgressRequestParams
71+
* @property {string} type - The type of tracked progress ("user" or "task").
72+
* @property {string} id - The ID of the tracked progress document.
73+
*/
74+
75+
/**
76+
* @typedef {Object} UpdateTrackedProgressRequestBody
77+
* @property {number} frequency - The frequency of tracking.
78+
* @property {boolean} monitored - Indicates if the progress is currently being tracked.
79+
*/
80+
81+
/**
82+
* @typedef {Object} UpdateTrackedProgressResponseData
83+
* @property {string} id - The ID of the tracked progress document.
84+
* @property {string} createdAt - The timestamp when the document was created.
85+
* @property {string} type - The type of tracked progress ("user" or "task").
86+
* @property {string} userId - The user ID.
87+
* @property {number} frequency - The frequency of tracking.
88+
* @property {boolean} monitored - Indicates if the progress is currently being tracked.
89+
* @property {string} updatedAt - The timestamp when the document was last updated.
90+
*/
91+
92+
/**
93+
* @typedef {Object} UpdateTrackedProgressResponse
94+
* @property {UpdateTrackedProgressResponseData} data - The data of the tracked progress document.
95+
* @property {string} message - The success message.
96+
*/
97+
98+
/**
99+
* Controller function for updating a tracked progress document.
100+
*
101+
* @param {Express.Request} req - The Express request object.
102+
* @param {UpdateTrackedProgressRequestParams} req.params - The request path parameters.
103+
* @param {UpdateTrackedProgressRequestBody} req.body - The request body object.
104+
* @param {Express.Response} res - The Express response object.
105+
* @returns {Promise<void>} - A Promise that resolves when the response has been sent.
106+
*/
107+
108+
const updateTrackedProgressController = async (req, res) => {
109+
try {
110+
const data = await updateTrackedProgressDocument({ ...req });
111+
return res.status(200).json({
112+
data,
113+
message: RESOURCE_UPDATED_SUCCESSFULLY,
114+
});
115+
} catch (error) {
116+
if (error instanceof NotFound) {
117+
return res.status(404).json({
118+
message: error.message,
119+
});
120+
}
121+
return res.status(500).json({
122+
message: INTERNAL_SERVER_ERROR_MESSAGE,
123+
});
124+
}
125+
};
126+
127+
/**
128+
* @typedef {Object} GetTrackedProgressRequestParams
129+
* @property {string} [type] - The type of tracked progress ("user" or "task").
130+
* @property {string} [monitored] - Indicates if the progress is currently being tracked.
131+
* @property {string} [userId] - The ID of the User who is currently being tracked.
132+
* @property {string} [taskId] - The ID of the task which is currently being tracked.
133+
*/
134+
135+
/**
136+
* @typedef {Object} TrackedProgressData
137+
* @property {string} id - The ID of the tracked progress document.
138+
* @property {boolean} monitored - Indicates if the progress is currently being tracked.
139+
* @property {string} createdAt - The timestamp when the document was created.
140+
* @property {string} type - The type of tracked progress ("user" or "task").
141+
* @property {string} [userId] - The user ID.
142+
* @property {string} [taskId] - The task ID.
143+
* @property {number} frequency - The frequency of tracking.
144+
* @property {string} updatedAt - The timestamp when the document was last updated.
145+
*/
146+
147+
/**
148+
* @typedef {Object} GetTrackedProgressResponse
149+
* @property {string} message - The success message.
150+
* @property {TrackedProgressData | TrackedProgressData[]} data - The data of the tracked progress document(s).
151+
*/
152+
153+
/**
154+
* Controller function for retrieving tracked progress documents.
155+
*
156+
* @param {Express.Request} req - The Express request object.
157+
* @param {Express.Response} res - The Express response object.
158+
* @returns {Promise<void>} A Promise that resolves when the response has been sent.
159+
*/
160+
161+
const getTrackedProgressController = async (req, res) => {
162+
try {
163+
const data = await getTrackedProgressDocuments({ ...req.query });
164+
return res.status(200).json({
165+
message: RESOURCE_RETRIEVED_SUCCESSFULLY,
166+
data,
167+
});
168+
} catch (error) {
169+
if (error instanceof NotFound) {
170+
const response = {
171+
message: error.message,
172+
};
173+
if (req.query.type) {
174+
response.data = [];
175+
}
176+
return res.status(404).json(response);
177+
}
178+
return res.status(500).json({
179+
message: INTERNAL_SERVER_ERROR_MESSAGE,
180+
});
181+
}
182+
};
183+
184+
module.exports = {
185+
createTrackedProgressController,
186+
updateTrackedProgressController,
187+
getTrackedProgressController,
188+
};

middlewares/rateLimiting.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
const { RateLimiterMemory } = require("rate-limiter-flexible");
2+
const { TOO_MANY_REQUESTS } = require("../constants/rateLimiting");
3+
const { getRetrySeconds } = require("../utils/rateLimiting");
4+
5+
// INFO: temporarily added here, will be take from env-var/config
6+
const opts = {
7+
keyPrefix: "commonRateLimiter--login_fail_by_ip_per_minute",
8+
points: 5,
9+
duration: 30,
10+
blockDuration: 60 * 10,
11+
};
12+
const globalRateLimiter = new RateLimiterMemory(opts);
13+
14+
/**
15+
* @param req object represents the HTTP request and has property for the request ip address
16+
* @param res object represents the HTTP response that app sends when it get an HTTP request
17+
* @param next indicates the next middelware function
18+
* @returns Promise, which:
19+
* - `resolved` with next middelware function call `next()`
20+
* - `resolved` with response status set to 429 and message `Too Many Requests` */
21+
async function commonRateLimiter(req, res, next) {
22+
// INFO: get the clientIP when running behind a proxy
23+
const ipAddress = req.headers["x-forwarded-for"] || req.socket.remoteAddress;
24+
let retrySeconds = 0;
25+
try {
26+
const responseGlobalRateLimiter = await globalRateLimiter.get(ipAddress);
27+
if (responseGlobalRateLimiter && responseGlobalRateLimiter.consumedPoints > opts.points) {
28+
retrySeconds = getRetrySeconds(responseGlobalRateLimiter.msBeforeNext);
29+
}
30+
if (retrySeconds > 0) {
31+
throw Error();
32+
}
33+
await globalRateLimiter.consume(ipAddress);
34+
return next();
35+
} catch (error) {
36+
// INFO: sending raw seconds in response,``
37+
// for letting API user decide how to represent this number.
38+
retrySeconds = getRetrySeconds(error?.msBeforeNext, retrySeconds);
39+
res.set({
40+
"Retry-After": retrySeconds,
41+
"X-RateLimit-Limit": opts.points,
42+
"X-RateLimit-Remaining": error?.remainingPoints ?? 0,
43+
"X-RateLimit-Reset": new Date(Date.now() + error?.msBeforeNext),
44+
});
45+
const message = `${TOO_MANY_REQUESTS.ERROR_TYPE}: Retry After ${retrySeconds} seconds, requests limit reached`;
46+
return res.status(TOO_MANY_REQUESTS.STATUS_CODE).json({ message });
47+
}
48+
}
49+
50+
module.exports = {
51+
commonRateLimiter,
52+
};

middlewares/validators/monitor.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
const joi = require("joi");
2+
const { VALID_PROGRESS_TYPES, TYPE_MAP } = require("../../constants/progresses");
3+
4+
const baseSchema = joi
5+
.object()
6+
.strict()
7+
.keys({
8+
type: joi
9+
.string()
10+
.valid(...VALID_PROGRESS_TYPES)
11+
.required()
12+
.messages({
13+
"any.required": "Required field 'type' is missing.",
14+
"any.only": "Type field is restricted to either 'user' or 'task'.",
15+
}),
16+
monitored: joi.boolean().required().messages({
17+
"any.required": "Required field 'monitored' is missing.",
18+
"boolean.base": "monitored field must be a boolean value.",
19+
}),
20+
frequency: joi
21+
.number()
22+
.integer()
23+
.positive()
24+
.when("type", {
25+
is: "user",
26+
then: joi.number().equal(1).messages({
27+
"number.equal": "'frequency' field must be equal to 1",
28+
}),
29+
otherwise: joi.optional(),
30+
})
31+
.messages({
32+
"number.base": "'frequency' field must be a number",
33+
"number.integer": "'frequency' field must be an integer",
34+
"number.positive": "'frequency' field must be a positive integer",
35+
"any.only": "'frequency' field must be equal to 1 for type 'user'",
36+
}),
37+
taskId: joi.string().when("type", {
38+
is: "task",
39+
then: joi.required().messages({
40+
"any.required": "Required field 'taskId' is missing.",
41+
}),
42+
otherwise: joi.optional(),
43+
}),
44+
userId: joi.string().when("type", {
45+
is: "user",
46+
then: joi.required().messages({
47+
"any.required": "Required field 'userId' is missing.",
48+
}),
49+
otherwise: joi.optional(),
50+
}),
51+
})
52+
.messages({ "object.unknown": "Invalid field provided." });
53+
54+
const validateCreateTrackedProgressRecord = async (req, res, next) => {
55+
const monitoredSchema = joi.object().keys({
56+
monitored: joi.boolean().required().messages({
57+
"boolean.base": "monitored field must be a boolean value.",
58+
}),
59+
});
60+
const createSchema = baseSchema.concat(monitoredSchema);
61+
try {
62+
await createSchema.validateAsync(req.body, { abortEarly: false });
63+
next();
64+
} catch (error) {
65+
logger.error(`Error validating payload: ${error}`);
66+
res.boom.badRequest(error.details[0].message);
67+
}
68+
};
69+
70+
const validateUpdateTrackedProgress = async (req, res, next) => {
71+
const { type, typeId } = req.params;
72+
const { monitored, frequency } = req.body;
73+
const updatedData = { type, [TYPE_MAP[type]]: typeId, monitored, frequency };
74+
const monitoredSchema = joi.object().keys({
75+
monitored: joi.boolean().optional().messages({
76+
"boolean.base": "monitored field must be a boolean value.",
77+
}),
78+
});
79+
const updateSchema = baseSchema.concat(monitoredSchema).or("monitored", "frequency");
80+
try {
81+
await updateSchema.validateAsync(updatedData, { abortEarly: false });
82+
next();
83+
} catch (error) {
84+
logger.error(`Error validating payload: ${error}`);
85+
res.boom.badRequest(error.details[0].message);
86+
}
87+
};
88+
89+
const validateGetTrackedProgressQueryParams = async (req, res, next) => {
90+
const schema = joi
91+
.object({
92+
type: joi.string().valid(...VALID_PROGRESS_TYPES),
93+
userId: joi.string(),
94+
taskId: joi.string(),
95+
monitored: joi.bool().optional(),
96+
})
97+
.xor("type", "userId", "taskId")
98+
.with("monitored", "type")
99+
.messages({
100+
"any.only": "Type field is restricted to either 'user' or 'task'.",
101+
"object.xor": "Invalid combination of request params.",
102+
"object.missing": "One of the following fields is required: type, userId, or taskId.",
103+
"object.unknown": "Invalid field provided.",
104+
"object.with": "The monitored param is missing a required field type.",
105+
});
106+
107+
try {
108+
await schema.validateAsync(req.query, { abortEarly: false });
109+
next();
110+
} catch (error) {
111+
logger.error(`Error validating payload: ${error}`);
112+
res.boom.badRequest(error.details[0].message);
113+
}
114+
};
115+
116+
module.exports = {
117+
validateCreateTrackedProgressRecord,
118+
validateUpdateTrackedProgress,
119+
validateGetTrackedProgressQueryParams,
120+
};

0 commit comments

Comments
 (0)