diff --git a/controllers/discordactions.js b/controllers/discordactions.js index c5b74634d..9f2d0ef89 100644 --- a/controllers/discordactions.js +++ b/controllers/discordactions.js @@ -488,9 +488,15 @@ const setRoleToUsersWith31DaysPlusOnboarding = async (req, res) => { const generateInviteForUser = async (req, res) => { try { const { userId } = req.query; + const isSuperUser = req.userData.roles.super_user; + + const applicationId = req.approvedApplicationId || (isSuperUser ? req.query.applicationId : undefined); + const role = req.approvedApplicationRole || (isSuperUser ? req.query.role : undefined); + + if (!applicationId || !role) return res.boom.forbidden("Application data is required to generate an invite."); const userIdForInvite = userId || req.userData.id; - const modelResponse = await discordRolesModel.getUserDiscordInvite(userIdForInvite); + const modelResponse = await discordRolesModel.getUserDiscordInviteByApplication(userIdForInvite, applicationId); if (!modelResponse.notFound) { return res.status(409).json({ @@ -506,7 +512,7 @@ const generateInviteForUser = async (req, res) => { const inviteOptions = { channelId: channelId, - role: req.approvedApplicationRole, + role, }; const response = await fetch(`${DISCORD_BASE_URL}/invite`, { method: "POST", @@ -518,7 +524,11 @@ const generateInviteForUser = async (req, res) => { const inviteCode = discordInviteResponse.data.code; const inviteLink = `discord.gg/${inviteCode}`; - await discordRolesModel.addInviteToInviteModel({ userId: userIdForInvite, inviteLink }); + await discordRolesModel.addInviteToInviteModel({ + userId: userIdForInvite, + inviteLink, + applicationId, + }); return res.status(201).json({ message: "invite generated successfully", diff --git a/middlewares/checkCanGenerateDiscordLink.ts b/middlewares/checkCanGenerateDiscordLink.ts index 9fd7278a6..2245b1b09 100644 --- a/middlewares/checkCanGenerateDiscordLink.ts +++ b/middlewares/checkCanGenerateDiscordLink.ts @@ -33,6 +33,7 @@ const checkCanGenerateDiscordLink = async (req: CustomRequest, res: CustomRespon } req.approvedApplicationRole = approvedApplication.role; + req.approvedApplicationId = approvedApplication.id; return next(); } catch (error) { return res.boom.badImplementation("An error occurred while checking user applications."); diff --git a/models/discordactions.js b/models/discordactions.js index 0937b8869..4a6085c70 100644 --- a/models/discordactions.js +++ b/models/discordactions.js @@ -1255,7 +1255,7 @@ const getUserDiscordInvite = async (userId) => { return { notFound: true }; } } catch (err) { - logger.log("error in getting user invite", err); + logger.error("error in getting user invite", err); throw err; } }; @@ -1264,6 +1264,24 @@ const groupUpdateLastJoinDate = async ({ id }) => { return { updated: true }; }; +const getUserDiscordInviteByApplication = async (userId, applicationId) => { + try { + const invite = await discordInvitesModel + .where("userId", "==", userId) + .where("applicationId", "==", applicationId) + .limit(1) + .get(); + const [inviteDoc] = invite.docs; + if (inviteDoc) { + return { id: inviteDoc.id, ...inviteDoc.data(), notFound: false }; + } + return { notFound: true }; + } catch (err) { + logger.error("error in getting user invite by application", err); + throw err; + } +}; + module.exports = { createNewRole, removeMemberGroup, @@ -1288,4 +1306,5 @@ module.exports = { groupUpdateLastJoinDate, deleteGroupRole, skipOnboardingUsersHavingApprovedExtensionRequest, + getUserDiscordInviteByApplication, }; diff --git a/routes/discordactions.js b/routes/discordactions.js index 0f8a8cdb6..ac1d2f3b7 100644 --- a/routes/discordactions.js +++ b/routes/discordactions.js @@ -31,22 +31,13 @@ const ROLES = require("../constants/roles"); const { Services } = require("../constants/bot"); const { verifyCronJob } = require("../middlewares/authorizeBot"); const { authorizeAndAuthenticate } = require("../middlewares/authorizeUsersAndService"); -const { disableRoute } = require("../middlewares/shortCircuit"); const router = express.Router(); router.post("/groups", authenticate, checkIsVerifiedDiscord, validateGroupRoleBody, createGroupRole); router.get("/groups", authenticate, checkIsVerifiedDiscord, validateLazyLoadingParams, getPaginatedAllGroupRoles); router.delete("/groups/:groupId", authenticate, checkIsVerifiedDiscord, authorizeRoles([SUPERUSER]), deleteGroupRole); router.post("/roles", authenticate, checkIsVerifiedDiscord, validateMemberRoleBody, addGroupRoleToMember); -/** - * Short-circuit the GET method for this endpoint - * Refer https://github.com/Real-Dev-Squad/todo-action-items/issues/269 for more details. - */ -router.get("/invite", disableRoute, authenticate, getUserDiscordInvite); -/** - * Short-circuit this POST method for this endpoint - * Refer https://github.com/Real-Dev-Squad/todo-action-items/issues/269 for more details. - */ +router.get("/invite", authenticate, getUserDiscordInvite); router.post("/invite", authenticate, checkCanGenerateDiscordLink, generateInviteForUser); router.delete("/roles", authenticate, checkIsVerifiedDiscord, deleteRole); diff --git a/test/integration/discordactions.test.js b/test/integration/discordactions.test.js index 69a1d7750..693fd4a20 100644 --- a/test/integration/discordactions.test.js +++ b/test/integration/discordactions.test.js @@ -1173,6 +1173,82 @@ describe("Discord actions", function () { }); }); + describe("POST /discord-actions/invite (application-scoped)", function () { + it("should create invite and return 201 for an eligible non-super user", async function () { + sinon + .stub(ApplicationModel, "getUserApplications") + .resolves([{ id: "app-1", status: "accepted", role: "developer", isNew: true }]); + sinon.stub(discordRolesModel, "getUserDiscordInviteByApplication").resolves({ notFound: true }); + sinon.stub(discordRolesModel, "addInviteToInviteModel").resolves("invite-doc-id"); + fetchStub.returns( + Promise.resolve({ + status: 201, + json: () => Promise.resolve({ data: { code: "new-code" } }), + }) + ); + + const res = await chai + .request(app) + .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/new-code"); + }); + + it("should return 409 when invite already exists for the same application", async function () { + sinon + .stub(ApplicationModel, "getUserApplications") + .resolves([{ id: "app-1", status: "accepted", role: "developer", isNew: true }]); + sinon.stub(discordRolesModel, "getUserDiscordInviteByApplication").resolves({ + notFound: false, + id: "invite-doc-id", + userId, + applicationId: "app-1", + inviteLink: "discord.gg/existing", + }); + + const res = await chai + .request(app) + .post("/discord-actions/invite") + .set("cookie", `${cookieName}=${userAuthToken}`); + + expect(res).to.have.status(409); + expect(res.body.message).to.be.equal("User invite is already present!"); + }); + + it("should return 403 for super user when application data is not provided", async function () { + const res = await chai + .request(app) + .post("/discord-actions/invite") + .set("cookie", `${cookieName}=${superUserAuthToken}`); + + expect(res).to.have.status(403); + expect(res.body.message).to.be.equal("Application data is required to generate an invite."); + }); + + it("should allow super user to create invite when application data is provided", async function () { + sinon.stub(discordRolesModel, "getUserDiscordInviteByApplication").resolves({ notFound: true }); + sinon.stub(discordRolesModel, "addInviteToInviteModel").resolves("invite-doc-id"); + fetchStub.returns( + Promise.resolve({ + status: 201, + json: () => Promise.resolve({ data: { code: "super-code" } }), + }) + ); + + const res = await chai + .request(app) + .post("/discord-actions/invite?applicationId=app-super-1&role=developer") + .set("cookie", `${cookieName}=${superUserAuthToken}`); + + 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/super-code"); + }); + }); + describe("PUT /discord-actions/group-idle", function () { let allIds; diff --git a/test/unit/models/discordactions.test.js b/test/unit/models/discordactions.test.js index 6f4d20836..1dc5bb809 100644 --- a/test/unit/models/discordactions.test.js +++ b/test/unit/models/discordactions.test.js @@ -33,6 +33,7 @@ const { getMissedProgressUpdatesUsers, addInviteToInviteModel, getUserDiscordInvite, + getUserDiscordInviteByApplication, groupUpdateLastJoinDate, updateIdleUsersOnDiscord, updateIdle7dUsersOnDiscord, @@ -877,6 +878,10 @@ describe("discordactions", function () { await addInviteToInviteModel(inviteObject); }); + afterEach(async function () { + await cleanDb(); + }); + it("should return invite for the user when the userId of a user is passed at it exists in the db", async function () { const invite = await getUserDiscordInvite("kfjkasdfl"); expect(invite).to.have.property("id"); @@ -891,6 +896,40 @@ describe("discordactions", function () { }); }); + describe("getUserDiscordInviteByApplication", function () { + afterEach(async function () { + await cleanDb(); + }); + + it("should return invite when userId and applicationId match an existing doc", async function () { + const userId = "user-app-invite"; + const applicationId = "app-123"; + await addInviteToInviteModel({ + userId, + inviteLink: "discord.gg/abc", + applicationId, + createdAt: new Date().toISOString(), + }); + const invite = await getUserDiscordInviteByApplication(userId, applicationId); + expect(invite.notFound).to.be.equal(false); + expect(invite.userId).to.be.equal(userId); + expect(invite.applicationId).to.be.equal(applicationId); + expect(invite.inviteLink).to.be.equal("discord.gg/abc"); + }); + + it("should return notFound when no invite exists for that applicationId", async function () { + const invite = await getUserDiscordInviteByApplication("nonexistent-user", "app-456"); + expect(invite.notFound).to.be.equal(true); + }); + + it("should return notFound for applicationId when only legacy doc without applicationId exists", async function () { + const userId = "user-legacy-only"; + await addInviteToInviteModel({ userId, inviteLink: "discord.gg/legacy" }); + const invite = await getUserDiscordInviteByApplication(userId, "new-app-id"); + expect(invite.notFound).to.be.equal(true); + }); + }); + describe("groupUpdateLastJoinDate", function () { beforeEach(function () { sinon.stub(discordRoleModel, "doc").returns({ diff --git a/types/global.d.ts b/types/global.d.ts index db3c4a19b..e16afd656 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -42,4 +42,8 @@ export type userData = { }; export type CustomResponse = Response & { boom: Boom }; -export type CustomRequest = Request & { userData; approvedApplicationRole?: string }; +export type CustomRequest = Request & { + userData; + approvedApplicationRole?: string; + approvedApplicationId?: string; +};