From e4363101519d1938b068d2f36e93b295805cdf24 Mon Sep 17 00:00:00 2001 From: anuj chhikara Date: Sun, 22 Feb 2026 23:48:49 +0530 Subject: [PATCH 1/2] feat: enhance Discord invite generation with application-specific handling - Added applicationId and role validation in the invite generation process. - Updated the invite retrieval method to support application-scoped invites. - Enhanced middleware to pass applicationId to the request object. - Implemented tests for application-specific invite generation scenarios. --- controllers/discordactions.js | 14 ++++-- middlewares/checkCanGenerateDiscordLink.ts | 1 + models/discordactions.js | 19 ++++++++ test/integration/discordactions.test.js | 23 ++++++++++ test/unit/models/discordactions.test.js | 50 ++++++++++++++++++++++ types/global.d.ts | 6 ++- 6 files changed, 109 insertions(+), 4 deletions(-) diff --git a/controllers/discordactions.js b/controllers/discordactions.js index c5b74634d..7d2f8ed1e 100644 --- a/controllers/discordactions.js +++ b/controllers/discordactions.js @@ -488,9 +488,13 @@ const setRoleToUsersWith31DaysPlusOnboarding = async (req, res) => { const generateInviteForUser = async (req, res) => { try { const { userId } = req.query; + const applicationId = req.approvedApplicationId; + const role = req.approvedApplicationRole; + + 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 +510,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 +522,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..fadd5ea1f 100644 --- a/models/discordactions.js +++ b/models/discordactions.js @@ -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.log("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/test/integration/discordactions.test.js b/test/integration/discordactions.test.js index 69a1d7750..3efb9b4ea 100644 --- a/test/integration/discordactions.test.js +++ b/test/integration/discordactions.test.js @@ -1173,6 +1173,29 @@ describe("Discord actions", function () { }); }); + describe("POST /discord-actions/invite (application-scoped)", function () { + 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!"); + }); + }); + 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..3b2147ce7 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, @@ -889,6 +890,55 @@ describe("discordactions", function () { const invite = await getUserDiscordInvite("kfjkasdafdfdsfl"); expect(invite.notFound).to.be.equal(true); }); + + it("should return latest invite by createdAt when user has multiple invite docs", async function () { + const userId = "user-multi-invite"; + await addInviteToInviteModel({ + userId, + inviteLink: "discord.gg/old", + applicationId: "app1", + createdAt: "2025-01-01T00:00:00.000Z", + }); + await addInviteToInviteModel({ + userId, + inviteLink: "discord.gg/new", + applicationId: "app2", + createdAt: "2025-02-01T00:00:00.000Z", + }); + const invite = await getUserDiscordInvite(userId); + expect(invite.notFound).to.be.equal(false); + expect(invite.inviteLink).to.be.equal("discord.gg/new"); + }); + }); + + describe("getUserDiscordInviteByApplication", function () { + 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 () { 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; +}; From 2fe7a9a30d6a6feb81685a331d6cf4df56de347a Mon Sep 17 00:00:00 2001 From: anuj chhikara Date: Mon, 23 Feb 2026 10:08:40 +0530 Subject: [PATCH 2/2] feat: enhance Discord invite generation for super users and improve error logging --- controllers/discordactions.js | 6 ++- models/discordactions.js | 4 +- test/integration/discordactions.test.js | 53 +++++++++++++++++++++++++ test/unit/models/discordactions.test.js | 27 ++++--------- 4 files changed, 67 insertions(+), 23 deletions(-) diff --git a/controllers/discordactions.js b/controllers/discordactions.js index 7d2f8ed1e..9f2d0ef89 100644 --- a/controllers/discordactions.js +++ b/controllers/discordactions.js @@ -488,8 +488,10 @@ const setRoleToUsersWith31DaysPlusOnboarding = async (req, res) => { const generateInviteForUser = async (req, res) => { try { const { userId } = req.query; - const applicationId = req.approvedApplicationId; - const role = req.approvedApplicationRole; + 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; diff --git a/models/discordactions.js b/models/discordactions.js index fadd5ea1f..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; } }; @@ -1277,7 +1277,7 @@ const getUserDiscordInviteByApplication = async (userId, applicationId) => { } return { notFound: true }; } catch (err) { - logger.log("error in getting user invite by application", err); + logger.error("error in getting user invite by application", err); throw err; } }; diff --git a/test/integration/discordactions.test.js b/test/integration/discordactions.test.js index 3efb9b4ea..693fd4a20 100644 --- a/test/integration/discordactions.test.js +++ b/test/integration/discordactions.test.js @@ -1174,6 +1174,29 @@ 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") @@ -1194,6 +1217,36 @@ describe("Discord actions", function () { 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 () { diff --git a/test/unit/models/discordactions.test.js b/test/unit/models/discordactions.test.js index 3b2147ce7..1dc5bb809 100644 --- a/test/unit/models/discordactions.test.js +++ b/test/unit/models/discordactions.test.js @@ -878,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"); @@ -890,28 +894,13 @@ describe("discordactions", function () { const invite = await getUserDiscordInvite("kfjkasdafdfdsfl"); expect(invite.notFound).to.be.equal(true); }); - - it("should return latest invite by createdAt when user has multiple invite docs", async function () { - const userId = "user-multi-invite"; - await addInviteToInviteModel({ - userId, - inviteLink: "discord.gg/old", - applicationId: "app1", - createdAt: "2025-01-01T00:00:00.000Z", - }); - await addInviteToInviteModel({ - userId, - inviteLink: "discord.gg/new", - applicationId: "app2", - createdAt: "2025-02-01T00:00:00.000Z", - }); - const invite = await getUserDiscordInvite(userId); - expect(invite.notFound).to.be.equal(false); - expect(invite.inviteLink).to.be.equal("discord.gg/new"); - }); }); 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";