diff --git a/controllers/usersStatus.ts b/controllers/newUserStatus.ts similarity index 54% rename from controllers/usersStatus.ts rename to controllers/newUserStatus.ts index d70d9ed37..7ea138dd0 100644 --- a/controllers/usersStatus.ts +++ b/controllers/newUserStatus.ts @@ -1,9 +1,15 @@ +import { CANCEL_OOO, userState } from "../constants/userStatus"; +import { Forbidden, NotFound } from "http-errors"; import { getUserStatus as getUserStatusFromModel, updateUserStatus as updateUserStatusFromModel, updateAllUserStatus as updateAllUserStatusModel, batchUpdateUsersStatus as batchUpdateUsersStatusModel, -} from "../models/usersStatus"; + deleteUserStatus as deleteUserStatusModel, + getAllUserStatus as getAllUserStatusModel, + cancelOooStatus +} from "../models/newUserStatus"; +import { getUserIdBasedOnRoute } from "../utils/newUserStatus"; const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); /** @@ -14,7 +20,7 @@ const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); */ const getUserStatus = async (req: any, res: any) => { try { - let userId: string = req.params.userId; + let userId: string = getUserIdBasedOnRoute(req); if (userId) { const userData: any = await getUserStatusFromModel(userId); const { userStatusExists, id, data } = userData; @@ -46,7 +52,7 @@ const getUserStatus = async (req: any, res: any) => { */ const updateUserStatus = async (req: any, res: any) => { try { - let userId: string = req.params.userId; + let userId: string = getUserIdBasedOnRoute(req); if (userId) { const dataToUpdate = { state: "CURRENT", @@ -84,6 +90,7 @@ const updateUserStatus = async (req: any, res: any) => { * @param res {Object} - Express response object */ const updateAllUserStatus = async (req, res) => { + console.log("Abc") try { const data = await updateAllUserStatusModel(); return res.status(200).json({ @@ -112,9 +119,87 @@ const batchUpdateUsersStatus = async (req, res) => { } }; +const deleteUserStatus = async (req, res) => { + try { + const { userId } = req.params; + const deletedUserStatus = await deleteUserStatusModel(userId); + const responseObj = { id: deletedUserStatus.id, userId, message: null }; + let statusCode: number; + if (deletedUserStatus.userStatusExisted) { + responseObj.message = "User Status deleted successfully."; + statusCode = 200; + } else { + responseObj.message = "User Status to delete not found."; + statusCode = 404; + } + return res.status(statusCode).json(responseObj); + } catch (error) { + logger.error(`Error while deleting User Status: ${error}`); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } +}; + +const getAllUserStatus = async (req, res) => { + const limit = parseInt(req.query.size) || 10; + const lastDocId = req.query.next; + + try { + const { allUserStatus, lastDocId: nextLastDocId } = await getAllUserStatusModel(req.query, limit, lastDocId); + + // Construct the next page URL + const nextPageUrl = allUserStatus.length === limit ? `${req.baseUrl}${req.path}?next=${nextLastDocId}&size=${limit}${req.query.state ? `&state=${req.query.state}` : ''}` : null; + + return res.json({ + message: "All User Status found successfully.", + totalUserStatus: allUserStatus.length, + pageSize: limit, + nextPageLink: nextPageUrl, + allUserStatus: allUserStatus, + }); + } catch (err) { + logger.error(`Error while fetching all the User Status: ${err}`); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } +}; + +const cancelOOOStatus = async (req, res) => { + let userId: string = req.params.userId; + try { + const responseObject = await cancelOooStatus(userId); + return res.status(200).json(responseObject); + } catch (error) { + logger.error(`Error while cancelling the ${userState.OOO} Status : ${error}`); + if (error instanceof Forbidden) { + return res.status(403).json({ + statusCode: 403, + error: "Forbidden", + message: error.message, + }); + } else if (error instanceof NotFound) { + return res.status(404).json({ + statusCode: 404, + error: "NotFound", + message: error.message, + }); + } + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } +}; + +const updateUserStatusController = async (req, res) => { + if (Object.keys(req.body).includes(CANCEL_OOO)) { + await cancelOOOStatus(req, res); + } else { + await updateUserStatus(req, res); + } +}; + + export default { getUserStatus, - updateUserStatus, updateAllUserStatus, batchUpdateUsersStatus, + deleteUserStatus, + getAllUserStatus, + updateUserStatusController }; diff --git a/middlewares/validators/usersStatus.ts b/middlewares/validators/newUserStatus.ts similarity index 81% rename from middlewares/validators/usersStatus.ts rename to middlewares/validators/newUserStatus.ts index 066fd5024..4216328a8 100644 --- a/middlewares/validators/usersStatus.ts +++ b/middlewares/validators/newUserStatus.ts @@ -113,4 +113,28 @@ export const validateMassUpdate = async (req: any, res: CustomResponse, next: Ne logger.error(`Error validating Query Params for GET ${error.message}`); res.boom.badRequest(error); } +}; + +export const validateGetQueryParams = async (req: any, res: CustomResponse, next: NextFunction) => { + const schema = Joi.object() + .keys({ + aggregate: Joi.boolean().valid(true).error(new Error(`Invalid boolean value passed for aggregate.`)), + status: Joi.string() + .trim() + .valid(userState.IDLE, userState.ACTIVE, userState.OOO, userState.ONBOARDING) + .error(new Error(`Invalid State. State must be either IDLE, ACTIVE, OOO, or ONBOARDING`)), + size: Joi.number().optional(), + next: Joi.optional() + }) + .messages({ + "object.unknown": "Invalid query param provided.", + }); + + try { + await schema.validateAsync(req.query); + next(); + } catch (error) { + logger.error(`Error validating Query Params for GET ${error.message}`); + res.boom.badRequest(error); + } }; \ No newline at end of file diff --git a/models/usersStatus.ts b/models/newUserStatus.ts similarity index 73% rename from models/usersStatus.ts rename to models/newUserStatus.ts index 380d1b74c..8783e39e6 100644 --- a/models/usersStatus.ts +++ b/models/newUserStatus.ts @@ -1,15 +1,18 @@ import { userState } from "../constants/userStatus"; +import { Forbidden, NotFound } from "http-errors"; import firestore from "../utils/firestore"; -import { convertTimestampsInUserStatusToUTC, getTomorrowTimeStamp } from "../utils/userStatus"; +import { checkIfUserHasLiveTasks, convertTimestampsInUserStatusToUTC, getTomorrowTimeStamp } from "../utils/userStatus"; import admin from "firebase-admin"; const userStatusModel = firestore.collection("userStatus"); const futureStatusModel = firestore.collection("futureStatus"); const memberRoleModel = firestore.collection("member-group-roles"); const usersCollection = firestore.collection("users"); const discordRoleModel = firestore.collection("discord-roles"); +const tasksModel = firestore.collection("tasks"); // @ts-ignore const DISCORD_BASE_URL = config.get("services.discordBot.baseUrl"); import { generateAuthTokenForCloudflare } from "../utils/discord-actions"; +import { generateNewStatus } from "../utils/newUserStatus"; const getGroupRole = async (rolename: string) => { try { @@ -108,10 +111,6 @@ const addGroupIdleRoleToDiscordUser = async (userId: string) => { } }; -/** - * @params userId {string} : id of the user - * @returns {Promise} : returns the userStatus of a single user - */ export const getUserStatus = async ( userId: string ): Promise<{ id: string; data: any; userStatusExists: boolean } | object> => { @@ -199,12 +198,73 @@ export const updateUserStatus = async (userId: string, statusData: any) => { } }; -/** - * @param userId { String }: Id of the User - * @param newStatusData { Object }: Data to be Updated - * @returns Promise - */ -// TODO: Fix this implementation +export const cancelOooStatus = async (userId) => { + try { + let userStatusDoc: admin.firestore.QuerySnapshot; + let isActive: boolean; + try { + userStatusDoc = await userStatusModel + .where("userId", "==", userId) + .where("state", "==", "CURRENT") + .limit(1) + .get(); + } catch (error) { + logger.error(`Unable to fetch user status document from the firestore : ${error.message}`); + throw error; + } + if (!userStatusDoc.size) { + throw new NotFound("No User status document found"); + } + const [userStatusDocument] = userStatusDoc.docs; + const docId = userStatusDocument.id; + const docData = userStatusDocument.data(); + if (docData.status !== userState.OOO) { + throw new Forbidden( + `The ${userState.OOO} Status cannot be canceled because the current status is ${docData.status}.` + ); + } + try { + isActive = await checkIfUserHasLiveTasks(userId, tasksModel); + } catch (error) { + logger.error(`Unable to fetch user status based on the task : ${error.message}`); + throw error; + } + const newStatusData = generateNewStatus(isActive); + + const futureStatus = await futureStatusModel + .where("userId", "==", userId) + .where("state", "==", "UPCOMING") + .limit(1) + .get(); + if (futureStatus.size) { + const [futureStatusDoc] = futureStatus.docs; + await futureStatusModel.doc(futureStatusDoc.id).update({ state: "NOT_APPLIED" }); + } + + const today = new Date(); + const todaysTime = Date.UTC( + today.getUTCFullYear(), + today.getUTCMonth(), + today.getUTCDate(), + today.getUTCHours(), + today.getUTCMinutes(), + today.getUTCSeconds() + ); + + const newDocRef = await userStatusModel.add({ userId, ...newStatusData }); + await userStatusModel.doc(docId).update({ state: "PAST", endedOn: todaysTime }); + if (!isActive) { + await addGroupIdleRoleToDiscordUser(userId); + } else { + await removeGroupIdleRoleFromDiscordUser(userId); + } + return { id: newDocRef.id, userStatusExists: true, data: { userId, ...newStatusData } }; + } catch (error) { + logger.error(`Error while canceling ${userState.OOO} status: ${error.message}`); + throw error; + } +}; + export const updateAllUserStatus = async () => { const summary = { noOfUserStatusUpdated: 0, @@ -217,7 +277,7 @@ export const updateAllUserStatus = async () => { const batch = firestore.batch(); const today = new Date().setUTCHours(0, 0, 0, 0); - const updateUserStatusFromFutureStatus = async (document: any, resolve: (value: unknown)=>void) => { + const updateUserStatusFromFutureStatus = async (document: any, resolve: (value: unknown) => void) => { const futureStatusData = document.data(); const futureStatusRef = document.ref; const userId = futureStatusData.userId; @@ -273,7 +333,7 @@ export const updateAllUserStatus = async () => { const promises = userFutureStatusDocs.docs.map((document) => { return new Promise((resolve, reject) => { - updateUserStatusFromFutureStatus(document, resolve) + updateUserStatusFromFutureStatus(document, resolve); }); }); await Promise.all(promises); @@ -294,7 +354,7 @@ const getNextDayTimeStamp = (timeStamp: number) => { return nextDateDateTime.getTime(); }; -export const batchUpdateUsersStatus = async (users: {userId: string, state: string}[]) => { +export const batchUpdateUsersStatus = async (users: { userId: string; state: string }[]) => { const batch = firestore.batch(); const summary = { usersCount: users.length, @@ -335,11 +395,8 @@ export const batchUpdateUsersStatus = async (users: {userId: string, state: stri if (state === userState.IDLE) await addGroupIdleRoleToDiscordUser(userId); batch.set(newUserStatusRef, newUserStatusData); } else { - const { - status: currentStatus, - endsOn - } = data; - + const { status: currentStatus, endsOn } = data; + if (currentStatus === state) { currentStatus === userState.ACTIVE ? summary.activeUsersUnaltered++ : summary.idleUsersUnaltered++; continue; @@ -408,4 +465,73 @@ export const batchUpdateUsersStatus = async (users: {userId: string, state: stri } catch (error) { throw new Error("Batch operation failed"); } -}; \ No newline at end of file +}; + +export const deleteUserStatus = async (userId: string) => { + try { + const userStatusDocs = await userStatusModel + .where("userId", "==", userId) + .where("state", "==", "CURRENT") + .limit(1) + .get(); + const [userStatusDoc] = userStatusDocs.docs; + if (userStatusDoc) { + const today = new Date(); + const todaysTime = Date.UTC( + today.getUTCFullYear(), + today.getUTCMonth(), + today.getUTCDate(), + today.getUTCHours(), + today.getUTCMinutes(), + today.getUTCSeconds() + ); + const docId = userStatusDoc.id; + await userStatusModel.doc(docId).set({ state: "PAST", endedOn: todaysTime }, { merge: true }); + return { id: userStatusDoc.id, userStatusExisted: true, userStatusDeleted: true }; + } else { + return { id: null, userStatusExisted: false, userStatusDeleted: false }; + } + } catch (error) { + logger.error(`error in deleting User Status Document . Reason - ${error}`); + throw error; + } +}; + +export const getAllUserStatus = async (query: { status: string }, limit = 10, lastDocId: any) => { + try { + const allUserStatus = []; + let lastDoc = null; + + if (lastDocId) { + lastDoc = await userStatusModel.doc(lastDocId).get(); + } + + let dbQuery = userStatusModel.where("state", "==", "CURRENT"); + + if (query.status) { + dbQuery = dbQuery.where("status", "==", query.status); + } + + if (lastDoc) { + dbQuery = dbQuery.startAfter(lastDoc); + } + + const data = await dbQuery.limit(limit).get(); + const lastUserStatusDoc = data.docs[data.docs.length - 1]; + + data.forEach((doc) => { + const currentUserStatus = { + id: doc.id, + userId: doc.data().userId, + status: doc.data().status, + monthlyHours: doc.data().monthlyHours, + }; + allUserStatus.push(currentUserStatus); + }); + + return { allUserStatus, lastDocId: lastUserStatusDoc?.id }; + } catch (error) { + logger.error(`error in fetching the User Status of all Users. ${error}`); + throw error; + } +}; diff --git a/routes/index.ts b/routes/index.ts index cba0a2a5e..136c7d92d 100644 --- a/routes/index.ts +++ b/routes/index.ts @@ -18,7 +18,7 @@ app.use("/tasks", require("./tasks.js")); app.use("/taskRequests", require("./taskRequests")); app.use("/trade", require("./trading")); app.use("/users/status", require("./userStatus.js")); -app.use("/v1/users/status", require("./usersStatus.ts")); +app.use("/v1/users/status", require("./newUserStatus.ts")); app.use("/users", require("./users.js")); app.use("/profileDiffs", require("./profileDiffs.js")); app.use("/wallet", require("./wallets.js")); diff --git a/routes/usersStatus.ts b/routes/newUserStatus.ts similarity index 52% rename from routes/usersStatus.ts rename to routes/newUserStatus.ts index 388c9d95d..f8fbb35ed 100644 --- a/routes/usersStatus.ts +++ b/routes/newUserStatus.ts @@ -1,14 +1,17 @@ import express from "express"; const router = express.Router(); import authenticate from "../middlewares/authenticate"; -import usersStatusController from "../controllers/usersStatus"; -import { validateUsersStatus, validateMassUpdate } from "../middlewares/validators/usersStatus"; -import { authorizeOwnUserIdParamOrSuperUser } from "../middlewares/authorizeOwnOrSuperUser"; +import usersStatusController from "../controllers/newUserStatus"; +import { validateUsersStatus, validateMassUpdate, validateGetQueryParams } from "../middlewares/validators/newUserStatus"; import { authorizeAndAuthenticate } from "../middlewares/authorizeUsersAndService"; +const authorizeRoles = require("../middlewares/authorizeRoles"); const ROLES = require("../constants/roles"); const { Services } = require("../constants/bot"); +router.get("/", validateGetQueryParams, usersStatusController.getAllUserStatus); +router.get('/self', authenticate, usersStatusController.getUserStatus); router.get("/:userId", usersStatusController.getUserStatus); +router.patch('/self', authenticate, usersStatusController.updateUserStatusController); router.patch( "/update", authorizeAndAuthenticate([ROLES.SUPERUSER], [Services.CRON_JOB_HANDLER]), @@ -17,9 +20,9 @@ router.patch( router.patch( "/:userId", authenticate, - authorizeOwnUserIdParamOrSuperUser, + authorizeRoles([ROLES.SUPERUSER]), validateUsersStatus, - usersStatusController.updateUserStatus + usersStatusController.updateUserStatusController ); router.patch( "/batch", @@ -27,4 +30,5 @@ router.patch( validateMassUpdate, usersStatusController.batchUpdateUsersStatus ); +router.delete("/:userId", authenticate, authorizeRoles([ROLES.SUPERUSER]), usersStatusController.deleteUserStatus); module.exports = router; diff --git a/test/fixtures/usersStatus/usersStatus.js b/test/fixtures/newUserStatus/newUserStatus.js similarity index 100% rename from test/fixtures/usersStatus/usersStatus.js rename to test/fixtures/newUserStatus/newUserStatus.js diff --git a/test/integration/usersStatus.test.js b/test/integration/newUserStatus.test.js similarity index 92% rename from test/integration/usersStatus.test.js rename to test/integration/newUserStatus.test.js index 6410c65b8..b595276b6 100644 --- a/test/integration/usersStatus.test.js +++ b/test/integration/newUserStatus.test.js @@ -14,20 +14,20 @@ const { generateUserStatusData, userStatusDataForOooState, oooStatusDataForShortDuration, -} = require("../fixtures/usersStatus/usersStatus"); +} = require("../fixtures/newUserStatus/newUserStatus"); const userData = require("../fixtures/user/user")(); const superUser = userData[4]; const { convertTimestampToUTCStartOrEndOfDay } = require("../../utils/time"); const config = require("config"); -const { updateUserStatus } = require("../../models/usersStatus"); +const { updateUserStatus } = require("../../models/newUserStatus"); const cookieName = config.get("userToken.cookieName"); chai.use(chaiHttp); -describe("UserStatus", function () { +describe("NewUserStatus", function () { let jwt; let superUserId; let superUserAuthToken; @@ -63,7 +63,7 @@ describe("UserStatus", function () { }); }); - it("Should not return the User Status Document of the user requesting it", function (done) { + it("Should return the User Status Document of the user requesting it", function (done) { chai .request(app) .get(`/v1/users/status/self`) @@ -72,11 +72,11 @@ describe("UserStatus", function () { if (err) { return done(err); } - expect(res).to.have.status(404); + expect(res).to.have.status(200); expect(res.body).to.be.a("object"); - expect(res.body.message).to.equal("User Status couldn't be found."); - expect(res.body.userId).to.equal("self"); - expect(res.body.data).to.equal(null); + expect(res.body.message).to.equal("User Status found successfully."); + expect(res.body.userId).to.equal(userId); + expect(res.body.data.state).to.equal("CURRENT"); return done(); }); }); @@ -119,7 +119,7 @@ describe("UserStatus", function () { const response2 = await chai .request(app) .patch(`/v1/users/status/${testUserId}`) - .set("Cookie", `${cookieName}=${testUserJwt}`) + .set("Cookie", `${cookieName}=${superUserAuthToken}`) .send(generateUserStatusData("OOO", appliedOnDate, endsOnDate, "Vacation Trip")); expect(response2).to.have.status(200); expect(response2.body.message).to.equal("Future Status of user updated successfully."); @@ -202,7 +202,7 @@ describe("UserStatus", function () { const response2 = await chai .request(app) .patch(`/v1/users/status/${testUserId}`) - .set("Cookie", `${cookieName}=${testUserJwt}`) + .set("Cookie", `${cookieName}=${superUserAuthToken}`) .send(generateUserStatusData("OOO", appliedOnDate, endsOnDate, "Vacation Trip")); expect(response2).to.have.status(200); expect(response2.body.message).to.equal("Future Status of user updated successfully."); @@ -268,9 +268,10 @@ describe("UserStatus", function () { if (err) { return done(err); } - expect(res).to.have.status(403); + expect(res).to.have.status(201); expect(res.body).to.be.a("object"); - expect(res.body.message).to.equal("Unauthorized User"); + expect(res.body.message).to.equal("User Status created successfully."); + expect(res.body.data.status).to.equal("OOO"); return done(); }); }); @@ -279,7 +280,7 @@ describe("UserStatus", function () { chai .request(app) .patch(`/v1/users/status/${testUserId}`) - .set("Cookie", `${cookieName}=${testUserJwt}`) + .set("Cookie", `${cookieName}=${superUserAuthToken}`) .send(userStatusDataForOooState) .end((err, res) => { if (err) { @@ -315,7 +316,7 @@ describe("UserStatus", function () { chai .request(app) .patch(`/v1/users/status/${testUserId}`) - .set("cookie", `${cookieName}=${testUserJwt}`) + .set("cookie", `${cookieName}=${superUserAuthToken}`) .send(oooStatusDataForShortDuration) .end((err, res) => { if (err) { @@ -348,7 +349,7 @@ describe("UserStatus", function () { chai .request(app) .patch(`/v1/users/status/${testUserId}`) - .set("cookie", `${cookieName}=${testUserJwt}`) + .set("cookie", `${cookieName}=${superUserAuthToken}`) .send(generateUserStatusData("IN_OFFICE", Date.now())) .end((err, res) => { if (err) { @@ -368,7 +369,7 @@ describe("UserStatus", function () { chai .request(app) .patch(`/v1/users/status/${testUserId}`) - .set("cookie", `${cookieName}=${testUserJwt}`) + .set("cookie", `${cookieName}=${superUserAuthToken}`) .send(generateUserStatusData("OOO", Date.now(), endsOnDate, "")) .end((err, res) => { if (err) { @@ -389,7 +390,7 @@ describe("UserStatus", function () { chai .request(app) .patch(`/v1/users/status/${testUserId}`) - .set("cookie", `${cookieName}=${testUserJwt}`) + .set("cookie", `${cookieName}=${superUserAuthToken}`) .send(generateUserStatusData("OOO", appliedOnDate, "", "")) .end((err, res) => { if (err) { @@ -411,7 +412,7 @@ describe("UserStatus", function () { chai .request(app) .patch(`/v1/users/status/${testUserId}`) - .set("cookie", `${cookieName}=${testUserJwt}`) + .set("cookie", `${cookieName}=${superUserAuthToken}`) .send(generateUserStatusData("OOO", appliedOnDate, endsOnDate, "Semester Exams")) .end((err, res) => { if (err) { diff --git a/utils/newUserStatus.ts b/utils/newUserStatus.ts new file mode 100644 index 000000000..2bda4d4c7 --- /dev/null +++ b/utils/newUserStatus.ts @@ -0,0 +1,25 @@ +export const generateNewStatus = (isActive: boolean) => { + const currentTimeStamp = new Date().getTime(); + + const newStatusData = { + message: "", + appliedOn: currentTimeStamp, + status: "IDLE", + state: "CURRENT" + }; + + if (isActive) { + newStatusData.status = "ACTIVE"; + } + return newStatusData; +}; + +export const getUserIdBasedOnRoute = (req) => { + let userId; + if (req.route.path === "/self") { + userId = req.userData.id; + } else { + userId = req.params.userId; + } + return userId; +}; \ No newline at end of file