From 60df7839f12fed4152ee03817bcf80dad150b68c Mon Sep 17 00:00:00 2001 From: Shubham raj Date: Sun, 18 Aug 2024 23:10:39 +0530 Subject: [PATCH 01/12] [Fix] Broken Link in README.md (#2074) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 95eca3921b..e3a0b62583 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ - [About the Project](#about-the-project) - [Running the Project](#running-the-project) - [Prerequisites](#prerequisites) -- [API Documentation](#api-documentation) +- [API Documentation](https://github.com/Real-Dev-Squad/website-api-contracts/) - [CONTRIBUTING](CONTRIBUTING.md) ## About the Project From a460e10dfcf8e03271ee5a4a0201b6f470a3ace6 Mon Sep 17 00:00:00 2001 From: Vinit khandal <111434418+vinit717@users.noreply.github.com> Date: Mon, 19 Aug 2024 21:12:10 +0530 Subject: [PATCH 02/12] Change discord link generate validator condition (#2076) * fix: discord link generate validator * refactor: checkCanGenerateDiscordLink validator * test: add and refactor for dsicord link generate * chore: remove comment --- middlewares/checkCanGenerateDiscordLink.ts | 65 ++++++++++++--- test/integration/discordactions.test.js | 94 +++++++++++++++++++--- 2 files changed, 139 insertions(+), 20 deletions(-) diff --git a/middlewares/checkCanGenerateDiscordLink.ts b/middlewares/checkCanGenerateDiscordLink.ts index eab2b02cce..7af51d852f 100644 --- a/middlewares/checkCanGenerateDiscordLink.ts +++ b/middlewares/checkCanGenerateDiscordLink.ts @@ -1,28 +1,73 @@ import { NextFunction } from "express"; import { CustomRequest, CustomResponse } from "../types/global"; +const ApplicationModel = require("../models/applications"); const checkCanGenerateDiscordLink = async (req: CustomRequest, res: CustomResponse, next: NextFunction) => { - const { discordId, roles, id: userId, profileStatus } = req.userData; + const { id: userId, roles } = req.userData; const isSuperUser = roles.super_user; const userIdInQuery = req.query.userId; + const currentTime = Date.now(); + const cutoffTime = 1724630399000; // Epoch time for 25 August 2024 + + if (isSuperUser) { + return next(); + } if (userIdInQuery && userIdInQuery !== userId && !isSuperUser) { return res.boom.forbidden("User should be super user to generate link for other users"); } - if (!isSuperUser && discordId) { - return res.boom.forbidden("Only users who have never joined discord can generate invite link"); + if (currentTime >= cutoffTime) { + return res.boom.forbidden("Discord invite link generation is not allowed after the cutoff time."); } - if (roles.archived) { - return res.boom.forbidden("Archived users cannot generate invite"); - } + try { + const applications = await ApplicationModel.getUserApplications(userId); + + if (!applications || applications.length === 0) { + return res.boom.forbidden("No applications found."); + } - if (!isSuperUser && !roles.maven && !roles.designer && !roles.product_manager && profileStatus !== "VERIFIED") { - return res.boom.forbidden("Only selected roles can generate discord link directly"); - } + const approvedApplication = applications.find((application: { status: string; }) => application.status === 'accepted'); + + if (!approvedApplication) { + return res.boom.forbidden("Only users with an approved application can generate a Discord invite link."); + } - return next(); + return next(); + } catch (error) { + return res.boom.badImplementation("An error occurred while checking user applications."); + } }; +export default checkCanGenerateDiscordLink; + +// <------ We have to revisit this later -------> +// <--- https://github.com/Real-Dev-Squad/website-backend/issues/2078 ---> + + +// const checkCanGenerateDiscordLink = async (req: CustomRequest, res: CustomResponse, next: NextFunction) => { +// const { discordId, roles, id: userId, profileStatus } = req.userData; +// const isSuperUser = roles.super_user; +// const userIdInQuery = req.query.userId; + +// if (userIdInQuery && userIdInQuery !== userId && !isSuperUser) { +// return res.boom.forbidden("User should be super user to generate link for other users"); +// } + +// if (!isSuperUser && discordId) { +// return res.boom.forbidden("Only users who have never joined discord can generate invite link"); +// } + +// if (roles.archived) { +// return res.boom.forbidden("Archived users cannot generate invite"); +// } + +// if (!isSuperUser && !roles.maven && !roles.designer && !roles.product_manager && profileStatus !== "VERIFIED") { +// return res.boom.forbidden("Only selected roles can generate discord link directly"); +// } + +// return next(); +// }; + module.exports = checkCanGenerateDiscordLink; diff --git a/test/integration/discordactions.test.js b/test/integration/discordactions.test.js index 8d64574fb4..aeadec41be 100644 --- a/test/integration/discordactions.test.js +++ b/test/integration/discordactions.test.js @@ -8,6 +8,8 @@ const addUser = require("../utils/addUser"); const cleanDb = require("../utils/cleanDb"); // Import fixtures const userData = require("../fixtures/user/user")(); +const ApplicationModel = require("../../models/applications"); + const usersInDiscord = require("../fixtures/user/inDiscord"); const superUser = userData[4]; const archievedUser = userData[19]; @@ -777,7 +779,18 @@ describe("Discord actions", function () { }); }); + // <------ Will revisit this later https://github.com/Real-Dev-Squad/website-backend/issues/2078 ---> describe("POST /discord-actions/invite", function () { + let clock; + + beforeEach(function () { + clock = sinon.useFakeTimers(new Date("2024-08-24").getTime()); + }); + + afterEach(function () { + sinon.restore(); + }); + it("should return 403 if the userId in the query param is not equal to the userId of the user and user is not a super user", async function () { const res = await chai .request(app) @@ -788,7 +801,8 @@ describe("Discord actions", function () { expect(res.body.message).to.be.equal("User should be super user to generate link for other users"); }); - it("should return 403 if the user has discord id in their user object, which means user is already in discord", async function () { + // eslint-disable-next-line mocha/no-skipped-tests + it.skip("should return 403 if the user has discord id in their user object, which means user is already in discord", async function () { const res = await chai .request(app) .post(`/discord-actions/invite`) @@ -798,7 +812,8 @@ describe("Discord actions", function () { expect(res.body.message).to.be.equal("Only users who have never joined discord can generate invite link"); }); - it("should return 403 if user has role archieved", async function () { + // eslint-disable-next-line mocha/no-skipped-tests + it.skip("should return 403 if user has role archieved", async function () { archievedUserId = await addUser(archievedUser); archievedUserToken = authService.generateAuthToken({ userId: archievedUserId }); const res = await chai @@ -810,7 +825,8 @@ describe("Discord actions", function () { expect(res.body.message).to.be.equal("Archived users cannot generate invite"); }); - it("should return 403 if the user doesn't have role designer, product_manager, or mavens", async function () { + // eslint-disable-next-line mocha/no-skipped-tests + it.skip("should return 403 if the user doesn't have role designer, product_manager, or mavens", async function () { developerUserWithoutApprovedProfileStatusId = await addUser(developerUserWithoutApprovedProfileStatus); developerUserWithoutApprovedProfileStatusToken = authService.generateAuthToken({ userId: developerUserWithoutApprovedProfileStatusId, @@ -824,7 +840,8 @@ describe("Discord actions", function () { expect(res.body.message).to.be.equal("Only selected roles can generate discord link directly"); }); - it("should generate discord link if user is a product mananger", async function () { + // eslint-disable-next-line mocha/no-skipped-tests + it.skip("should generate discord link if user is a product mananger", async function () { fetchStub.returns( Promise.resolve({ status: 201, @@ -844,7 +861,8 @@ describe("Discord actions", function () { expect(res.body.inviteLink).to.be.equal("discord.gg/xyz"); }); - it("should generate discord link if user is a designer", async function () { + // eslint-disable-next-line mocha/no-skipped-tests + it.skip("should generate discord link if user is a designer", async function () { fetchStub.returns( Promise.resolve({ status: 201, @@ -864,7 +882,8 @@ describe("Discord actions", function () { expect(res.body.inviteLink).to.be.equal("discord.gg/zlmfasd"); }); - it("should generate discord link if user is a maven", async function () { + // eslint-disable-next-line mocha/no-skipped-tests + it.skip("should generate discord link if user is a maven", async function () { fetchStub.returns( Promise.resolve({ status: 201, @@ -884,18 +903,73 @@ describe("Discord actions", function () { expect(res.body.inviteLink).to.be.equal("discord.gg/asdfdsfsd"); }); - it("should generate discord link if user is a superUser", async function () { + it("should return 403 if current date is after 25 August 2024", async function () { + clock.tick(2 * 24 * 60 * 60 * 1000); // Move time forward to after 25 August 2024 + sinon.stub(ApplicationModel, "getUserApplications").resolves([{ status: "accepted" }]); + + const res = await chai + .request(app) + .post("/discord-actions/invite") + .set("cookie", `${cookieName}=${userAuthToken}`); + + expect(res).to.have.status(403); + expect(res.body.message).to.be.equal("Discord invite link generation is not allowed after the cutoff time."); + }); + + it("should return 403 if user has no applications", async function () { + sinon.stub(ApplicationModel, "getUserApplications").resolves([]); + + const res = await chai + .request(app) + .post(`/discord-actions/invite`) + .set("cookie", `${cookieName}=${userAuthToken}`); + + expect(res).to.have.status(403); + expect(res.body.message).to.be.equal("No applications found."); + }); + + it("should return 403 if user has pending applications", async function () { + sinon.stub(ApplicationModel, "getUserApplications").resolves([{ status: "pending" }]); + + const res = await chai + .request(app) + .post(`/discord-actions/invite`) + .set("cookie", `${cookieName}=${userAuthToken}`); + + expect(res).to.have.status(403); + expect(res.body.message).to.be.equal( + "Only users with an approved application can generate a Discord invite link." + ); + }); + + it("should return 403 if user has rejected applications", async function () { + sinon.stub(ApplicationModel, "getUserApplications").resolves([{ status: "rejected" }]); + + const res = await chai + .request(app) + .post(`/discord-actions/invite`) + .set("cookie", `${cookieName}=${userAuthToken}`); + + expect(res).to.have.status(403); + expect(res.body.message).to.be.equal( + "Only users with an approved application can generate a Discord invite link." + ); + }); + + // eslint-disable-next-line mocha/no-skipped-tests + it.skip("should generate discord link if user has an approved application", async function () { + sinon.stub(ApplicationModel, "getUserApplications").resolves([{ status: "accepted" }]); fetchStub.returns( Promise.resolve({ status: 201, - json: () => Promise.resolve({ data: { code: "asdfdsfsd" } }), + json: () => Promise.resolve({ data: { code: "xyz" } }), }) ); const res = await chai .request(app) - .post(`/discord-actions/invite`) - .set("cookie", `${cookieName}=${superUserAuthToken}`); + .post("/discord-actions/invite") + .set("cookie", `${cookieName}=${userAuthToken}`); expect(res).to.have.status(201); expect(res.body.message).to.be.equal("invite generated successfully"); expect(res.body.inviteLink).to.be.equal("discord.gg/asdfdsfsd"); From 81bb78badf24bd69a9792c2b3f5065bd50f402bc Mon Sep 17 00:00:00 2001 From: Vinit khandal <111434418+vinit717@users.noreply.github.com> Date: Mon, 19 Aug 2024 21:21:14 +0530 Subject: [PATCH 03/12] Dev to Main sync (#2079) --- README.md | 2 +- middlewares/checkCanGenerateDiscordLink.ts | 65 ++++++++++++--- test/integration/discordactions.test.js | 94 +++++++++++++++++++--- 3 files changed, 140 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 95eca3921b..e3a0b62583 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ - [About the Project](#about-the-project) - [Running the Project](#running-the-project) - [Prerequisites](#prerequisites) -- [API Documentation](#api-documentation) +- [API Documentation](https://github.com/Real-Dev-Squad/website-api-contracts/) - [CONTRIBUTING](CONTRIBUTING.md) ## About the Project diff --git a/middlewares/checkCanGenerateDiscordLink.ts b/middlewares/checkCanGenerateDiscordLink.ts index eab2b02cce..7af51d852f 100644 --- a/middlewares/checkCanGenerateDiscordLink.ts +++ b/middlewares/checkCanGenerateDiscordLink.ts @@ -1,28 +1,73 @@ import { NextFunction } from "express"; import { CustomRequest, CustomResponse } from "../types/global"; +const ApplicationModel = require("../models/applications"); const checkCanGenerateDiscordLink = async (req: CustomRequest, res: CustomResponse, next: NextFunction) => { - const { discordId, roles, id: userId, profileStatus } = req.userData; + const { id: userId, roles } = req.userData; const isSuperUser = roles.super_user; const userIdInQuery = req.query.userId; + const currentTime = Date.now(); + const cutoffTime = 1724630399000; // Epoch time for 25 August 2024 + + if (isSuperUser) { + return next(); + } if (userIdInQuery && userIdInQuery !== userId && !isSuperUser) { return res.boom.forbidden("User should be super user to generate link for other users"); } - if (!isSuperUser && discordId) { - return res.boom.forbidden("Only users who have never joined discord can generate invite link"); + if (currentTime >= cutoffTime) { + return res.boom.forbidden("Discord invite link generation is not allowed after the cutoff time."); } - if (roles.archived) { - return res.boom.forbidden("Archived users cannot generate invite"); - } + try { + const applications = await ApplicationModel.getUserApplications(userId); + + if (!applications || applications.length === 0) { + return res.boom.forbidden("No applications found."); + } - if (!isSuperUser && !roles.maven && !roles.designer && !roles.product_manager && profileStatus !== "VERIFIED") { - return res.boom.forbidden("Only selected roles can generate discord link directly"); - } + const approvedApplication = applications.find((application: { status: string; }) => application.status === 'accepted'); + + if (!approvedApplication) { + return res.boom.forbidden("Only users with an approved application can generate a Discord invite link."); + } - return next(); + return next(); + } catch (error) { + return res.boom.badImplementation("An error occurred while checking user applications."); + } }; +export default checkCanGenerateDiscordLink; + +// <------ We have to revisit this later -------> +// <--- https://github.com/Real-Dev-Squad/website-backend/issues/2078 ---> + + +// const checkCanGenerateDiscordLink = async (req: CustomRequest, res: CustomResponse, next: NextFunction) => { +// const { discordId, roles, id: userId, profileStatus } = req.userData; +// const isSuperUser = roles.super_user; +// const userIdInQuery = req.query.userId; + +// if (userIdInQuery && userIdInQuery !== userId && !isSuperUser) { +// return res.boom.forbidden("User should be super user to generate link for other users"); +// } + +// if (!isSuperUser && discordId) { +// return res.boom.forbidden("Only users who have never joined discord can generate invite link"); +// } + +// if (roles.archived) { +// return res.boom.forbidden("Archived users cannot generate invite"); +// } + +// if (!isSuperUser && !roles.maven && !roles.designer && !roles.product_manager && profileStatus !== "VERIFIED") { +// return res.boom.forbidden("Only selected roles can generate discord link directly"); +// } + +// return next(); +// }; + module.exports = checkCanGenerateDiscordLink; diff --git a/test/integration/discordactions.test.js b/test/integration/discordactions.test.js index 8d64574fb4..aeadec41be 100644 --- a/test/integration/discordactions.test.js +++ b/test/integration/discordactions.test.js @@ -8,6 +8,8 @@ const addUser = require("../utils/addUser"); const cleanDb = require("../utils/cleanDb"); // Import fixtures const userData = require("../fixtures/user/user")(); +const ApplicationModel = require("../../models/applications"); + const usersInDiscord = require("../fixtures/user/inDiscord"); const superUser = userData[4]; const archievedUser = userData[19]; @@ -777,7 +779,18 @@ describe("Discord actions", function () { }); }); + // <------ Will revisit this later https://github.com/Real-Dev-Squad/website-backend/issues/2078 ---> describe("POST /discord-actions/invite", function () { + let clock; + + beforeEach(function () { + clock = sinon.useFakeTimers(new Date("2024-08-24").getTime()); + }); + + afterEach(function () { + sinon.restore(); + }); + it("should return 403 if the userId in the query param is not equal to the userId of the user and user is not a super user", async function () { const res = await chai .request(app) @@ -788,7 +801,8 @@ describe("Discord actions", function () { expect(res.body.message).to.be.equal("User should be super user to generate link for other users"); }); - it("should return 403 if the user has discord id in their user object, which means user is already in discord", async function () { + // eslint-disable-next-line mocha/no-skipped-tests + it.skip("should return 403 if the user has discord id in their user object, which means user is already in discord", async function () { const res = await chai .request(app) .post(`/discord-actions/invite`) @@ -798,7 +812,8 @@ describe("Discord actions", function () { expect(res.body.message).to.be.equal("Only users who have never joined discord can generate invite link"); }); - it("should return 403 if user has role archieved", async function () { + // eslint-disable-next-line mocha/no-skipped-tests + it.skip("should return 403 if user has role archieved", async function () { archievedUserId = await addUser(archievedUser); archievedUserToken = authService.generateAuthToken({ userId: archievedUserId }); const res = await chai @@ -810,7 +825,8 @@ describe("Discord actions", function () { expect(res.body.message).to.be.equal("Archived users cannot generate invite"); }); - it("should return 403 if the user doesn't have role designer, product_manager, or mavens", async function () { + // eslint-disable-next-line mocha/no-skipped-tests + it.skip("should return 403 if the user doesn't have role designer, product_manager, or mavens", async function () { developerUserWithoutApprovedProfileStatusId = await addUser(developerUserWithoutApprovedProfileStatus); developerUserWithoutApprovedProfileStatusToken = authService.generateAuthToken({ userId: developerUserWithoutApprovedProfileStatusId, @@ -824,7 +840,8 @@ describe("Discord actions", function () { expect(res.body.message).to.be.equal("Only selected roles can generate discord link directly"); }); - it("should generate discord link if user is a product mananger", async function () { + // eslint-disable-next-line mocha/no-skipped-tests + it.skip("should generate discord link if user is a product mananger", async function () { fetchStub.returns( Promise.resolve({ status: 201, @@ -844,7 +861,8 @@ describe("Discord actions", function () { expect(res.body.inviteLink).to.be.equal("discord.gg/xyz"); }); - it("should generate discord link if user is a designer", async function () { + // eslint-disable-next-line mocha/no-skipped-tests + it.skip("should generate discord link if user is a designer", async function () { fetchStub.returns( Promise.resolve({ status: 201, @@ -864,7 +882,8 @@ describe("Discord actions", function () { expect(res.body.inviteLink).to.be.equal("discord.gg/zlmfasd"); }); - it("should generate discord link if user is a maven", async function () { + // eslint-disable-next-line mocha/no-skipped-tests + it.skip("should generate discord link if user is a maven", async function () { fetchStub.returns( Promise.resolve({ status: 201, @@ -884,18 +903,73 @@ describe("Discord actions", function () { expect(res.body.inviteLink).to.be.equal("discord.gg/asdfdsfsd"); }); - it("should generate discord link if user is a superUser", async function () { + it("should return 403 if current date is after 25 August 2024", async function () { + clock.tick(2 * 24 * 60 * 60 * 1000); // Move time forward to after 25 August 2024 + sinon.stub(ApplicationModel, "getUserApplications").resolves([{ status: "accepted" }]); + + const res = await chai + .request(app) + .post("/discord-actions/invite") + .set("cookie", `${cookieName}=${userAuthToken}`); + + expect(res).to.have.status(403); + expect(res.body.message).to.be.equal("Discord invite link generation is not allowed after the cutoff time."); + }); + + it("should return 403 if user has no applications", async function () { + sinon.stub(ApplicationModel, "getUserApplications").resolves([]); + + const res = await chai + .request(app) + .post(`/discord-actions/invite`) + .set("cookie", `${cookieName}=${userAuthToken}`); + + expect(res).to.have.status(403); + expect(res.body.message).to.be.equal("No applications found."); + }); + + it("should return 403 if user has pending applications", async function () { + sinon.stub(ApplicationModel, "getUserApplications").resolves([{ status: "pending" }]); + + const res = await chai + .request(app) + .post(`/discord-actions/invite`) + .set("cookie", `${cookieName}=${userAuthToken}`); + + expect(res).to.have.status(403); + expect(res.body.message).to.be.equal( + "Only users with an approved application can generate a Discord invite link." + ); + }); + + it("should return 403 if user has rejected applications", async function () { + sinon.stub(ApplicationModel, "getUserApplications").resolves([{ status: "rejected" }]); + + const res = await chai + .request(app) + .post(`/discord-actions/invite`) + .set("cookie", `${cookieName}=${userAuthToken}`); + + expect(res).to.have.status(403); + expect(res.body.message).to.be.equal( + "Only users with an approved application can generate a Discord invite link." + ); + }); + + // eslint-disable-next-line mocha/no-skipped-tests + it.skip("should generate discord link if user has an approved application", async function () { + sinon.stub(ApplicationModel, "getUserApplications").resolves([{ status: "accepted" }]); fetchStub.returns( Promise.resolve({ status: 201, - json: () => Promise.resolve({ data: { code: "asdfdsfsd" } }), + json: () => Promise.resolve({ data: { code: "xyz" } }), }) ); const res = await chai .request(app) - .post(`/discord-actions/invite`) - .set("cookie", `${cookieName}=${superUserAuthToken}`); + .post("/discord-actions/invite") + .set("cookie", `${cookieName}=${userAuthToken}`); expect(res).to.have.status(201); expect(res.body.message).to.be.equal("invite generated successfully"); expect(res.body.inviteLink).to.be.equal("discord.gg/asdfdsfsd"); From 3b072eb794221446b9585bab5b373e65ec32e939 Mon Sep 17 00:00:00 2001 From: vinit717 Date: Tue, 20 Aug 2024 19:29:46 +0530 Subject: [PATCH 04/12] fix: sorting with created field to use userId --- models/applications.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/models/applications.ts b/models/applications.ts index 01b592c4ad..40280f93bd 100644 --- a/models/applications.ts +++ b/models/applications.ts @@ -60,8 +60,10 @@ const getAllApplications = async (limit: number, lastDocId?: string) => { if (lastDocId) { lastDoc = await ApplicationsModel.doc(lastDocId).get(); } - - let dbQuery = ApplicationsModel.orderBy("createdAt", "desc"); + // Hot-fix: Sorting by userId due to missing created field in some entries. + // Revert to createdAt once the field is updated. + // https://github.com/Real-Dev-Squad/website-backend/issues/2084 + let dbQuery = ApplicationsModel.orderBy("userId", "desc"); if (lastDoc) { dbQuery = dbQuery.startAfter(lastDoc); @@ -114,7 +116,10 @@ const getApplicationsBasedOnStatus = async (status: string, limit: number, lastD lastDoc = await ApplicationsModel.doc(lastDocId).get(); } - dbQuery = dbQuery.orderBy("createdAt", "desc"); + // Hot-fix: Sorting by userId due to missing created field in some entries. + // Revert to createdAt once the field is updated. + // https://github.com/Real-Dev-Squad/website-backend/issues/2084 + dbQuery = dbQuery.orderBy("userId", "desc"); if (lastDoc) { dbQuery = dbQuery.startAfter(lastDoc); From 3fbf6c28e65bd75be1baa22e162796a7d05c848b Mon Sep 17 00:00:00 2001 From: Vinit khandal <111434418+vinit717@users.noreply.github.com> Date: Wed, 21 Aug 2024 00:06:34 +0530 Subject: [PATCH 05/12] Migration script to add status pending in applicants collection (#2087) * feat: add migration script to add status pending in applicants collection * chore: get createdAt time from firestore metadata * chore: update api route * chore: add deleted route back --------- Co-authored-by: Prakash --- controllers/applications.ts | 12 +++++++ models/applications.ts | 67 +++++++++++++++++++++++++++++++++++++ routes/applications.ts | 1 + 3 files changed, 80 insertions(+) diff --git a/controllers/applications.ts b/controllers/applications.ts index 3fd7932ddc..45882c6aba 100644 --- a/controllers/applications.ts +++ b/controllers/applications.ts @@ -149,10 +149,22 @@ const getApplicationById = async (req: CustomRequest, res: CustomResponse) => { } }; + +const batchUpdateApplicantsStatus = async (req: CustomRequest, res: CustomResponse): Promise => { + try { + const updateStats = await ApplicationModel.updateApplicantsStatus(); + return res.json(updateStats); + } catch (err) { + logger.error(`Error in batch updating applicants: ${err}`); + return res.boom.badImplementation("Internal Server Error"); + } +}; + module.exports = { getAllOrUserApplication, addApplication, updateApplication, getApplicationById, batchUpdateApplications, + batchUpdateApplicantsStatus }; diff --git a/models/applications.ts b/models/applications.ts index 40280f93bd..927fedca70 100644 --- a/models/applications.ts +++ b/models/applications.ts @@ -180,6 +180,72 @@ const updateApplication = async (dataToUpdate: object, applicationId: string) => } }; +const updateApplicantsStatus = async () => { + try { + const operationStats = { + failedApplicantUpdateIds: [], + applicantUpdatesFailed: 0, + applicantUpdated: 0, + totalApplicant: 0, + }; + + const updatedApplicants = []; + const applicantsSnapshot = await ApplicationsModel.get(); + + if (applicantsSnapshot.empty) { + return operationStats; + } + + operationStats.totalApplicant = applicantsSnapshot.size; + + applicantsSnapshot.forEach((applicant) => { + const applicantData = applicant.data(); + + const createdAt = applicant.createTime.seconds * 1000 + applicant.createTime.nanoseconds / 1000000; + + let propertyUpdated = false; + + if ("createdAt" in applicantData === false) { + const createdAtISO = new Date(createdAt).toISOString(); + applicantData.createdAt = createdAtISO; + propertyUpdated = true; + } + if ("status" in applicantData === false) { + applicantData.status = "pending"; + propertyUpdated = true; + } + if (propertyUpdated === true) { + operationStats.applicantUpdated += 1; + updatedApplicants.push({ id: applicant.id, data: applicantData }); + } + }); + + const multipleApplicantUpdateBatch = []; + const chunkedApplicants = chunks(updatedApplicants, FIRESTORE_BATCH_OPERATIONS_LIMIT); + + for (const applicants of chunkedApplicants) { + const batch = firestore.batch(); + applicants.forEach(({ id, data }) => { + batch.update(firestore.collection("applicants").doc(id), data); + }); + + try { + await batch.commit(); + multipleApplicantUpdateBatch.push(batch); + } catch (error) { + operationStats.applicantUpdatesFailed += applicants.length; + applicants.forEach(({ id }) => operationStats.failedApplicantUpdateIds.push(id)); + } + } + + await Promise.allSettled(multipleApplicantUpdateBatch); + return operationStats; + } catch (err) { + logger.error("Error in batch update", err); + throw err; + } +}; + module.exports = { getAllApplications, getUserApplications, @@ -188,4 +254,5 @@ module.exports = { getApplicationsBasedOnStatus, getApplicationById, batchUpdateApplications, + updateApplicantsStatus, }; diff --git a/routes/applications.ts b/routes/applications.ts index 9bd605630a..c829239930 100644 --- a/routes/applications.ts +++ b/routes/applications.ts @@ -25,5 +25,6 @@ router.patch( applications.updateApplication ); router.patch("/batch/update", authenticate, authorizeRoles([SUPERUSER]), applications.batchUpdateApplications); +router.post("/batch", authenticate, authorizeRoles([SUPERUSER]), applications.batchUpdateApplicantsStatus); module.exports = router; From cfaa0c2bd2bd08c07a030d15a812d06acbf733bd Mon Sep 17 00:00:00 2001 From: Vinit khandal <111434418+vinit717@users.noreply.github.com> Date: Wed, 21 Aug 2024 00:57:05 +0530 Subject: [PATCH 06/12] revert: hotfix to query application by createdAt (#2090) Co-authored-by: Amit Prakash <34869115+iamitprakash@users.noreply.github.com> --- models/applications.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/models/applications.ts b/models/applications.ts index 927fedca70..8af5b00027 100644 --- a/models/applications.ts +++ b/models/applications.ts @@ -60,10 +60,8 @@ const getAllApplications = async (limit: number, lastDocId?: string) => { if (lastDocId) { lastDoc = await ApplicationsModel.doc(lastDocId).get(); } - // Hot-fix: Sorting by userId due to missing created field in some entries. - // Revert to createdAt once the field is updated. - // https://github.com/Real-Dev-Squad/website-backend/issues/2084 - let dbQuery = ApplicationsModel.orderBy("userId", "desc"); + + let dbQuery = ApplicationsModel.orderBy("createdAt", "desc"); if (lastDoc) { dbQuery = dbQuery.startAfter(lastDoc); @@ -116,10 +114,7 @@ const getApplicationsBasedOnStatus = async (status: string, limit: number, lastD lastDoc = await ApplicationsModel.doc(lastDocId).get(); } - // Hot-fix: Sorting by userId due to missing created field in some entries. - // Revert to createdAt once the field is updated. - // https://github.com/Real-Dev-Squad/website-backend/issues/2084 - dbQuery = dbQuery.orderBy("userId", "desc"); + dbQuery = dbQuery.orderBy("createdAt", "desc"); if (lastDoc) { dbQuery = dbQuery.startAfter(lastDoc); From 5b7a0387126d39ade9d96376fb06791abcf4941f Mon Sep 17 00:00:00 2001 From: Amit Prakash <34869115+iamitprakash@users.noreply.github.com> Date: Sat, 24 Aug 2024 13:03:00 +0530 Subject: [PATCH 07/12] marked inDiscord: true, archived: false on /verify command (#2098) * inital commit * removed not needed code --- controllers/external-accounts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controllers/external-accounts.js b/controllers/external-accounts.js index 490ee6e99e..7e5763910d 100644 --- a/controllers/external-accounts.js +++ b/controllers/external-accounts.js @@ -62,7 +62,7 @@ const linkExternalAccount = async (req, res) => { await addOrUpdate( { - roles: { ...roles, in_discord: true }, + roles: { ...roles, in_discord: true, archived: false }, discordId: attributes.discordId, discordJoinedAt: attributes.discordJoinedAt, }, From feada3f372722a353a42856e285c162e198775c5 Mon Sep 17 00:00:00 2001 From: Vinit khandal <111434418+vinit717@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:59:05 +0530 Subject: [PATCH 08/12] Hotfix to extend date to generate discord link (#2101) * chore: make hotfix to extend date to generate discord link * chore: fix failing test * chore: remove comment code --- middlewares/checkCanGenerateDiscordLink.ts | 2 +- test/integration/discordactions.test.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/middlewares/checkCanGenerateDiscordLink.ts b/middlewares/checkCanGenerateDiscordLink.ts index 7af51d852f..c255352305 100644 --- a/middlewares/checkCanGenerateDiscordLink.ts +++ b/middlewares/checkCanGenerateDiscordLink.ts @@ -7,7 +7,7 @@ const checkCanGenerateDiscordLink = async (req: CustomRequest, res: CustomRespon const isSuperUser = roles.super_user; const userIdInQuery = req.query.userId; const currentTime = Date.now(); - const cutoffTime = 1724630399000; // Epoch time for 25 August 2024 + const cutoffTime = 1725147849000; // Todo will remove this Hotfix time for 31 August 2024 if (isSuperUser) { return next(); diff --git a/test/integration/discordactions.test.js b/test/integration/discordactions.test.js index aeadec41be..66632a1253 100644 --- a/test/integration/discordactions.test.js +++ b/test/integration/discordactions.test.js @@ -784,7 +784,7 @@ describe("Discord actions", function () { let clock; beforeEach(function () { - clock = sinon.useFakeTimers(new Date("2024-08-24").getTime()); + clock = sinon.useFakeTimers(new Date("2024-08-31").getTime()); }); afterEach(function () { @@ -904,7 +904,7 @@ describe("Discord actions", function () { }); it("should return 403 if current date is after 25 August 2024", async function () { - clock.tick(2 * 24 * 60 * 60 * 1000); // Move time forward to after 25 August 2024 + clock.tick(2 * 24 * 60 * 60 * 1000); // Move time forward to after 31 August 2024 sinon.stub(ApplicationModel, "getUserApplications").resolves([{ status: "accepted" }]); const res = await chai From e000314b84ca7fde7e17e02c24b13bc06e622fba Mon Sep 17 00:00:00 2001 From: Yesha Vyas <103744693+yeshavyas27@users.noreply.github.com> Date: Tue, 27 Aug 2024 01:27:20 +0530 Subject: [PATCH 09/12] Removed migration script that added createdAt and status fields to applicants collection (#2096) Co-authored-by: Vinit khandal <111434418+vinit717@users.noreply.github.com> --- controllers/applications.ts | 12 ------- models/applications.ts | 67 ------------------------------------- routes/applications.ts | 1 - 3 files changed, 80 deletions(-) diff --git a/controllers/applications.ts b/controllers/applications.ts index 45882c6aba..3fd7932ddc 100644 --- a/controllers/applications.ts +++ b/controllers/applications.ts @@ -149,22 +149,10 @@ const getApplicationById = async (req: CustomRequest, res: CustomResponse) => { } }; - -const batchUpdateApplicantsStatus = async (req: CustomRequest, res: CustomResponse): Promise => { - try { - const updateStats = await ApplicationModel.updateApplicantsStatus(); - return res.json(updateStats); - } catch (err) { - logger.error(`Error in batch updating applicants: ${err}`); - return res.boom.badImplementation("Internal Server Error"); - } -}; - module.exports = { getAllOrUserApplication, addApplication, updateApplication, getApplicationById, batchUpdateApplications, - batchUpdateApplicantsStatus }; diff --git a/models/applications.ts b/models/applications.ts index 8af5b00027..d447db028d 100644 --- a/models/applications.ts +++ b/models/applications.ts @@ -175,72 +175,6 @@ const updateApplication = async (dataToUpdate: object, applicationId: string) => } }; -const updateApplicantsStatus = async () => { - try { - const operationStats = { - failedApplicantUpdateIds: [], - applicantUpdatesFailed: 0, - applicantUpdated: 0, - totalApplicant: 0, - }; - - const updatedApplicants = []; - const applicantsSnapshot = await ApplicationsModel.get(); - - if (applicantsSnapshot.empty) { - return operationStats; - } - - operationStats.totalApplicant = applicantsSnapshot.size; - - applicantsSnapshot.forEach((applicant) => { - const applicantData = applicant.data(); - - const createdAt = applicant.createTime.seconds * 1000 + applicant.createTime.nanoseconds / 1000000; - - let propertyUpdated = false; - - if ("createdAt" in applicantData === false) { - const createdAtISO = new Date(createdAt).toISOString(); - applicantData.createdAt = createdAtISO; - propertyUpdated = true; - } - if ("status" in applicantData === false) { - applicantData.status = "pending"; - propertyUpdated = true; - } - if (propertyUpdated === true) { - operationStats.applicantUpdated += 1; - updatedApplicants.push({ id: applicant.id, data: applicantData }); - } - }); - - const multipleApplicantUpdateBatch = []; - const chunkedApplicants = chunks(updatedApplicants, FIRESTORE_BATCH_OPERATIONS_LIMIT); - - for (const applicants of chunkedApplicants) { - const batch = firestore.batch(); - applicants.forEach(({ id, data }) => { - batch.update(firestore.collection("applicants").doc(id), data); - }); - - try { - await batch.commit(); - multipleApplicantUpdateBatch.push(batch); - } catch (error) { - operationStats.applicantUpdatesFailed += applicants.length; - applicants.forEach(({ id }) => operationStats.failedApplicantUpdateIds.push(id)); - } - } - - await Promise.allSettled(multipleApplicantUpdateBatch); - return operationStats; - } catch (err) { - logger.error("Error in batch update", err); - throw err; - } -}; - module.exports = { getAllApplications, getUserApplications, @@ -249,5 +183,4 @@ module.exports = { getApplicationsBasedOnStatus, getApplicationById, batchUpdateApplications, - updateApplicantsStatus, }; diff --git a/routes/applications.ts b/routes/applications.ts index c829239930..9bd605630a 100644 --- a/routes/applications.ts +++ b/routes/applications.ts @@ -25,6 +25,5 @@ router.patch( applications.updateApplication ); router.patch("/batch/update", authenticate, authorizeRoles([SUPERUSER]), applications.batchUpdateApplications); -router.post("/batch", authenticate, authorizeRoles([SUPERUSER]), applications.batchUpdateApplicantsStatus); module.exports = router; From bca2d8c5f94c3a45d0428c52b8048ca232725704 Mon Sep 17 00:00:00 2001 From: Yesha Vyas <103744693+yeshavyas27@users.noreply.github.com> Date: Tue, 27 Aug 2024 02:14:44 +0530 Subject: [PATCH 10/12] Removed migration script at applications/batch/update that added createdAt and status fields to applicants collection (#2104) * Removed /applications/batch/update endpoint and it's related code and tests * Resolved merge conflicts --- controllers/applications.ts | 11 ------ models/applications.ts | 51 ---------------------------- routes/applications.ts | 1 - test/integration/application.test.ts | 39 --------------------- test/unit/models/application.test.ts | 7 ---- 5 files changed, 109 deletions(-) diff --git a/controllers/applications.ts b/controllers/applications.ts index 3fd7932ddc..47853b3479 100644 --- a/controllers/applications.ts +++ b/controllers/applications.ts @@ -7,16 +7,6 @@ const { API_RESPONSE_MESSAGES } = require("../constants/application"); const { getUserApplicationObject } = require("../utils/application"); const admin = require("firebase-admin"); -const batchUpdateApplications = async (req: CustomRequest, res: CustomResponse): Promise => { - try { - const updateStats = await ApplicationModel.batchUpdateApplications(); - return res.json(updateStats); - } catch (err) { - logger.error(`Error in batch updating application: ${err}`); - return res.boom.badImplementation(INTERNAL_SERVER_ERROR); - } -}; - const getAllOrUserApplication = async (req: CustomRequest, res: CustomResponse): Promise => { try { const { userId, status, next, size } = req.query; @@ -154,5 +144,4 @@ module.exports = { addApplication, updateApplication, getApplicationById, - batchUpdateApplications, }; diff --git a/models/applications.ts b/models/applications.ts index d447db028d..e6595634e7 100644 --- a/models/applications.ts +++ b/models/applications.ts @@ -1,56 +1,6 @@ import { application } from "../types/application"; const firestore = require("../utils/firestore"); const ApplicationsModel = firestore.collection("applicants"); -const { DOCUMENT_WRITE_SIZE: FIRESTORE_BATCH_OPERATIONS_LIMIT } = require("../constants/constants"); -import { chunks } from "../utils/array"; - -const batchUpdateApplications = async () => { - try { - const operationStats = { - failedApplicationUpdateIds: [], - totalFailedApplicationUpdates: 0, - totalApplicationUpdates: 0, - }; - - const updatedApplications = []; - const applications = await ApplicationsModel.get(); - - if (applications.empty) { - return operationStats; - } - - operationStats.totalApplicationUpdates = applications.size; - - applications.forEach((application) => { - const taskData = application.data(); - taskData.createdAt = null; - updatedApplications.push({ id: application.id, data: taskData }); - }); - - const multipleApplicationUpdateBatch = []; - const chunkedApplication = chunks(updatedApplications, FIRESTORE_BATCH_OPERATIONS_LIMIT); - - chunkedApplication.forEach(async (applications) => { - const batch = firestore.batch(); - applications.forEach(({ id, data }) => { - batch.update(ApplicationsModel.doc(id), data); - }); - try { - await batch.commit(); - multipleApplicationUpdateBatch.push(batch); - } catch (error) { - operationStats.totalFailedApplicationUpdates += applications.length; - applications.forEach(({ id }) => operationStats.failedApplicationUpdateIds.push(id)); - } - }); - - await Promise.allSettled(multipleApplicationUpdateBatch); - return operationStats; - } catch (err) { - logger.log("Error in batch update", err); - throw err; - } -}; const getAllApplications = async (limit: number, lastDocId?: string) => { try { @@ -182,5 +132,4 @@ module.exports = { updateApplication, getApplicationsBasedOnStatus, getApplicationById, - batchUpdateApplications, }; diff --git a/routes/applications.ts b/routes/applications.ts index 9bd605630a..7c687e78f8 100644 --- a/routes/applications.ts +++ b/routes/applications.ts @@ -24,6 +24,5 @@ router.patch( applicationValidator.validateApplicationUpdateData, applications.updateApplication ); -router.patch("/batch/update", authenticate, authorizeRoles([SUPERUSER]), applications.batchUpdateApplications); module.exports = router; diff --git a/test/integration/application.test.ts b/test/integration/application.test.ts index 43b20d3369..eb86b7eb52 100644 --- a/test/integration/application.test.ts +++ b/test/integration/application.test.ts @@ -400,43 +400,4 @@ describe("Application", function () { }); }); }); - - describe("PATCH /application/batch/update", function () { - it("should return 401 if the user is not super user", function (done) { - chai - .request(app) - .patch(`/applications/batch/update`) - .set("cookie", `${cookieName}=${jwt}`) - .end((err, res) => { - if (err) { - return done(err); - } - - expect(res).to.have.status(401); - expect(res.body.message).to.be.equal("You are not authorized for this action."); - return done(); - }); - }); - - it("should return updated stats after updating all the application", function (done) { - chai - .request(app) - .patch(`/applications/batch/update`) - .set("cookie", `${cookieName}=${superUserJwt}`) - .end((err, res) => { - if (err) { - return done(err); - } - - expect(res).to.have.status(200); - expect(res.body).to.be.a("object"); - expect(res.body).to.be.deep.equal({ - failedApplicationUpdateIds: [], - totalFailedApplicationUpdates: 0, - totalApplicationUpdates: 6, - }); - return done(); - }); - }); - }); }); diff --git a/test/unit/models/application.test.ts b/test/unit/models/application.test.ts index da6ad077ed..0c9395299f 100644 --- a/test/unit/models/application.test.ts +++ b/test/unit/models/application.test.ts @@ -114,11 +114,4 @@ describe("applications", function () { expect(application.status).to.be.equal("accepted"); }); }); - - describe("batchUpdateApplications", function () { - it("should add createdAt null to all existing application docs", async function () { - const operationStats = await ApplicationModel.batchUpdateApplications(); - expect(operationStats.totalApplicationUpdates).to.be.equal(6); - }); - }); }); From 301a6632746a1ad05cb3fbe993d87e3e15023b00 Mon Sep 17 00:00:00 2001 From: Amit Prakash <34869115+iamitprakash@users.noreply.github.com> Date: Tue, 27 Aug 2024 03:48:18 +0530 Subject: [PATCH 11/12] test checkin (#2105) --- models/users.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/models/users.js b/models/users.js index 9c7b0c3425..82a0b2c45d 100644 --- a/models/users.js +++ b/models/users.js @@ -184,9 +184,9 @@ const fetchPaginatedUsers = async (query) => { let compositeQuery = [dbQuery]; if (isDevMode) { - const usernameQuery = userModel.where("roles.archived", "==", false).orderBy("username"); - const firstNameQuery = userModel.where("roles.archived", "==", false).orderBy("first_name"); - const lastNameQuery = userModel.where("roles.archived", "==", false).orderBy("last_name"); + const usernameQuery = userModel.where("roles.archived", "==", false).orderBy("username_lowercase"); + const firstNameQuery = userModel.where("roles.archived", "==", false).orderBy("first_name_lowercase"); + const lastNameQuery = userModel.where("roles.archived", "==", false).orderBy("last_name_lowercase"); compositeQuery = [usernameQuery, firstNameQuery, lastNameQuery]; } From 6c33d2abdfdd1eaf84906276b7b4736443868b86 Mon Sep 17 00:00:00 2001 From: Lakshay Manchanda <45519620+lakshayman@users.noreply.github.com> Date: Fri, 30 Aug 2024 00:49:23 +0530 Subject: [PATCH 12/12] Fixed users self patch API for new users (#2069) * Fixed users self patch API for new users * Tests fixed * Tests fixed --- controllers/users.js | 2 +- test/integration/users.test.js | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/controllers/users.js b/controllers/users.js index 858212fa4c..82c197ced4 100644 --- a/controllers/users.js +++ b/controllers/users.js @@ -410,7 +410,7 @@ const updateSelf = async (req, res) => { } } - if (userRoles.in_discord) { + if (userRoles.in_discord && !user.incompleteUserDetails) { const membersInDiscord = await getDiscordMembers(); const discordMember = membersInDiscord.find((member) => member.user.id === discordId); if (discordMember) { diff --git a/test/integration/users.test.js b/test/integration/users.test.js index 0e063fab06..5d8fd720ce 100644 --- a/test/integration/users.test.js +++ b/test/integration/users.test.js @@ -2344,7 +2344,11 @@ describe("Users", function () { }); describe("PATCH /users/self for developers", function () { - beforeEach(function () { + let id, jwtoken; + + beforeEach(async function () { + id = await addUser(); + jwtoken = authService.generateAuthToken({ userId: id }); fetchStub = Sinon.stub(global, "fetch"); const discordMembers = [...getDiscordMembers]; discordMembers[0].user.id = "12345"; @@ -2365,7 +2369,7 @@ describe("Users", function () { chai .request(app) .patch("/users/self") - .set("cookie", `${cookieName}=${jwt}`) + .set("cookie", `${cookieName}=${jwtoken}`) .send({ first_name: "Test first_name", })