Skip to content

Commit 24d91f5

Browse files
Feat: Super users can delete discord group roles (#2241)
* added route, controller and model for deleting the group role * added route, controller and model for deleting the group role * unit tests for deleteGroupRole * updated unit tests for deleteGroupRole * integrated feature flag * chore: roleid fetch from firestore and removed console logs * integration tests for deleteGroupRole * fix: reduced db calls from 2 to 1 for roleId * feat: integrated discord service to delete role from discord * test: unit and integration tests for discord service integration * chore: refactored controller and tests to use res.boom and routes to use devFlag middleware * fix: error code
1 parent 20c9c1a commit 24d91f5

File tree

7 files changed

+423
-7
lines changed

7 files changed

+423
-7
lines changed

controllers/discordactions.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const discordRolesModel = require("../models/discordactions");
66
const discordServices = require("../services/discordService");
77
const { fetchAllUsers, fetchUser } = require("../models/users");
88
const { generateCloudFlareHeaders } = require("../utils/discord-actions");
9+
const { addLog } = require("../models/logs");
910
const discordDeveloperRoleId = config.get("discordDeveloperRoleId");
1011
const discordMavenRoleId = config.get("discordMavenRoleId");
1112

@@ -63,6 +64,60 @@ const createGroupRole = async (req, res) => {
6364
}
6465
};
6566

67+
/**
68+
* Controller function to handle the soft deletion of a group role.
69+
*
70+
* @param {Object} req - The request object
71+
* @param {Object} res - The response object
72+
* @returns {Promise<void>}
73+
*/
74+
const deleteGroupRole = async (req, res) => {
75+
const { groupId } = req.params;
76+
77+
try {
78+
const { roleExists, existingRoles } = await discordRolesModel.isGroupRoleExists({ groupId });
79+
80+
if (!roleExists) {
81+
return res.boom.notFound("Group role not found");
82+
}
83+
84+
const roleData = existingRoles.data();
85+
86+
const discordDeletion = await discordServices.deleteGroupRoleFromDiscord(roleData.roleid);
87+
88+
if (!discordDeletion.success) {
89+
return res.boom.badImplementation(discordDeletion.message);
90+
}
91+
92+
const { isSuccess } = await discordRolesModel.deleteGroupRole(groupId, req.userData.id);
93+
94+
if (!isSuccess) {
95+
logger.error(`Role deleted from Discord but failed to delete from database for groupId: ${groupId}`);
96+
return res.boom.badImplementation("Group role deletion failed");
97+
}
98+
99+
const groupDeletionLog = {
100+
type: "group-role-deletion",
101+
meta: {
102+
userId: req.userData.id,
103+
},
104+
body: {
105+
groupId: groupId,
106+
roleName: roleData.rolename,
107+
discordRoleId: roleData.roleid,
108+
action: "delete",
109+
},
110+
};
111+
await addLog(groupDeletionLog.type, groupDeletionLog.meta, groupDeletionLog.body);
112+
return res.status(200).json({
113+
message: "Group role deleted successfully",
114+
});
115+
} catch (error) {
116+
logger.error(`Error while deleting group role: ${error}`);
117+
return res.boom.badImplementation("Internal server error");
118+
}
119+
};
120+
66121
/**
67122
* Gets all group-roles
68123
*
@@ -491,4 +546,5 @@ module.exports = {
491546
setRoleToUsersWith31DaysPlusOnboarding,
492547
getUserDiscordInvite,
493548
generateInviteForUser,
549+
deleteGroupRole,
494550
};

models/discordactions.js

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,31 @@ const createNewRole = async (roleData) => {
4646
}
4747
};
4848

49+
/**
50+
* Soft deletes a group role by marking it as deleted in the database.
51+
* This function updates the role document in Firestore, setting isDeleted to true
52+
* and recording who deleted it and when.
53+
*
54+
* @param {string} groupId - The ID of the group role to be deleted
55+
* @param {string} deletedBy - The ID of the user performing the deletion for logging purpose
56+
* @returns {Promise<Object>} An object indicating whether the operation was successful
57+
*/
58+
const deleteGroupRole = async (groupId, deletedBy) => {
59+
try {
60+
const roleRef = admin.firestore().collection("discord-roles").doc(groupId);
61+
await roleRef.update({
62+
isDeleted: true,
63+
deletedAt: admin.firestore.Timestamp.fromDate(new Date()),
64+
deletedBy: deletedBy,
65+
});
66+
67+
return { isSuccess: true };
68+
} catch (error) {
69+
logger.error(`Error in deleteGroupRole: ${error}`);
70+
return { isSuccess: false };
71+
}
72+
};
73+
4974
const removeMemberGroup = async (roleId, discordId) => {
5075
try {
5176
const backendResponse = await deleteRoleFromDatabase(roleId, discordId);
@@ -139,10 +164,13 @@ const updateGroupRole = async (roleData, docId) => {
139164

140165
const isGroupRoleExists = async (options = {}) => {
141166
try {
142-
const { rolename = null, roleid = null } = options;
167+
const { groupId = null, rolename = null, roleid = null } = options;
143168

144169
let existingRoles;
145-
if (rolename && roleid) {
170+
if (groupId) {
171+
existingRoles = await discordRoleModel.doc(groupId).get();
172+
return { roleExists: existingRoles.exists, existingRoles };
173+
} else if (rolename && roleid) {
146174
existingRoles = await discordRoleModel
147175
.where("rolename", "==", rolename)
148176
.where("roleid", "==", roleid)
@@ -153,9 +181,8 @@ const isGroupRoleExists = async (options = {}) => {
153181
} else if (roleid) {
154182
existingRoles = await discordRoleModel.where("roleid", "==", roleid).limit(1).get();
155183
} else {
156-
throw Error("Either rolename or roleId is required");
184+
throw Error("Either rolename, roleId, or groupId is required");
157185
}
158-
159186
return { roleExists: !existingRoles.empty, existingRoles };
160187
} catch (err) {
161188
logger.error("Error in getting all group-roles", err);
@@ -1075,4 +1102,5 @@ module.exports = {
10751102
getUserDiscordInvite,
10761103
addInviteToInviteModel,
10771104
groupUpdateLastJoinDate,
1105+
deleteGroupRole,
10781106
};

routes/discordactions.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const {
1515
updateUsersNicknameStatus,
1616
syncDiscordGroupRolesInFirestore,
1717
setRoleToUsersWith31DaysPlusOnboarding,
18+
deleteGroupRole,
1819
} = require("../controllers/discordactions");
1920
const {
2021
validateGroupRoleBody,
@@ -29,11 +30,19 @@ const ROLES = require("../constants/roles");
2930
const { Services } = require("../constants/bot");
3031
const { verifyCronJob } = require("../middlewares/authorizeBot");
3132
const { authorizeAndAuthenticate } = require("../middlewares/authorizeUsersAndService");
32-
33+
const { devFlagMiddleware } = require("../middlewares/devFlag");
3334
const router = express.Router();
3435

3536
router.post("/groups", authenticate, checkIsVerifiedDiscord, validateGroupRoleBody, createGroupRole);
3637
router.get("/groups", authenticate, checkIsVerifiedDiscord, getAllGroupRoles);
38+
router.delete(
39+
"/groups/:groupId",
40+
authenticate,
41+
checkIsVerifiedDiscord,
42+
authorizeRoles([SUPERUSER]),
43+
devFlagMiddleware,
44+
deleteGroupRole
45+
);
3746
router.post("/roles", authenticate, checkIsVerifiedDiscord, validateMemberRoleBody, addGroupRoleToMember);
3847
router.get("/invite", authenticate, getUserDiscordInvite);
3948
router.post("/invite", authenticate, checkCanGenerateDiscordLink, generateInviteForUser);

services/discordService.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,40 @@ const setUserDiscordNickname = async (userName, discordId) => {
103103
};
104104
} catch (err) {
105105
logger.error("Error in updating discord Nickname", err);
106-
throw err;
106+
throw new Error(err);
107+
}
108+
};
109+
110+
/**
111+
* Deletes a group role from the Discord server.
112+
* This function sends a DELETE request to the Discord API to remove the role.
113+
* It's part of the soft delete process, where we remove the role from Discord
114+
* but keep a record of it in our database.
115+
*
116+
* @param {string} roleId - The Discord ID of the role to be deleted
117+
* @returns {Promise<Object>} The response from the Discord API
118+
* @throws {Error} If the deletion fails or there's a network error
119+
*/
120+
121+
const deleteGroupRoleFromDiscord = async (roleId) => {
122+
try {
123+
const authToken = generateAuthTokenForCloudflare();
124+
const response = await fetch(`${DISCORD_BASE_URL}/roles/${roleId}?dev=true`, {
125+
method: "DELETE",
126+
headers: {
127+
"Content-Type": "application/json",
128+
Authorization: `Bearer ${authToken}`,
129+
},
130+
});
131+
132+
if (response.status === 204) {
133+
return { success: true, message: "Role deleted successfully" };
134+
}
135+
136+
return { success: false, message: "Failed to delete role from discord" };
137+
} catch (err) {
138+
logger.error("Error deleting role from Discord", err);
139+
return { success: false, message: "Internal server error" };
107140
}
108141
};
109142

@@ -114,4 +147,5 @@ module.exports = {
114147
addRoleToUser,
115148
removeRoleFromUser,
116149
setUserDiscordNickname,
150+
deleteGroupRoleFromDiscord,
117151
};

test/integration/discordactions.test.js

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,182 @@ describe("Discord actions", function () {
213213
});
214214
});
215215

216+
describe("DELETE /discord-actions/groups/:groupId", function () {
217+
let groupId;
218+
// eslint-disable-next-line mocha/no-setup-in-describe
219+
const roleData = groupData[0];
220+
221+
beforeEach(async function () {
222+
const docRef = await discordRoleModel.add(roleData);
223+
groupId = docRef.id;
224+
225+
superUserId = await addUser(superUser);
226+
superUserAuthToken = authService.generateAuthToken({ userId: superUserId });
227+
228+
sinon.stub(discordRolesModel, "deleteGroupRole").resolves({ isSuccess: true });
229+
});
230+
231+
afterEach(async function () {
232+
sinon.restore();
233+
await cleanDb();
234+
});
235+
236+
it("should return 404 when not in dev mode", function (done) {
237+
chai
238+
.request(app)
239+
.delete(`/discord-actions/groups/${groupId}`)
240+
.set("cookie", `${cookieName}=${superUserAuthToken}`)
241+
.end((err, res) => {
242+
expect(res).to.have.status(404);
243+
expect(res.body.error).to.equal("Not Found");
244+
done(err);
245+
});
246+
});
247+
248+
it("should return 404 if group role not found", function (done) {
249+
sinon.stub(discordRolesModel, "isGroupRoleExists").resolves({
250+
roleExists: false,
251+
existingRoles: { data: () => ({ ...roleData, roleid: roleData.roleid }) },
252+
});
253+
254+
chai
255+
.request(app)
256+
.delete(`/discord-actions/groups/${groupId}?dev=true`)
257+
.set("cookie", `${cookieName}=${superUserAuthToken}`)
258+
.end((err, res) => {
259+
expect(res).to.have.status(404);
260+
expect(res.body.error).to.equal("Not Found");
261+
done(err);
262+
});
263+
});
264+
265+
it("should successfully delete the group role from discord server", function (done) {
266+
sinon.stub(discordRolesModel, "isGroupRoleExists").resolves({
267+
roleExists: true,
268+
existingRoles: { data: () => ({ ...roleData, roleid: roleData.roleid }) },
269+
});
270+
271+
sinon.stub(discordServices, "deleteGroupRoleFromDiscord").resolves({
272+
success: true,
273+
message: "Role deleted successfully",
274+
});
275+
276+
chai
277+
.request(app)
278+
.delete(`/discord-actions/groups/${groupId}?dev=true`)
279+
.set("cookie", `${cookieName}=${superUserAuthToken}`)
280+
.end((err, res) => {
281+
expect(res).to.have.status(200);
282+
expect(res.body.message).to.equal("Group role deleted successfully");
283+
done(err);
284+
});
285+
});
286+
287+
it("should return 500 when discord role deletion fails", function (done) {
288+
sinon.stub(discordRolesModel, "isGroupRoleExists").resolves({
289+
roleExists: true,
290+
existingRoles: { data: () => ({ ...roleData, roleid: roleData.roleid }) },
291+
});
292+
293+
sinon.stub(discordServices, "deleteGroupRoleFromDiscord").resolves({
294+
success: false,
295+
message: "Failed to delete role from Discord",
296+
});
297+
298+
chai
299+
.request(app)
300+
.delete(`/discord-actions/groups/${groupId}?dev=true`)
301+
.set("cookie", `${cookieName}=${superUserAuthToken}`)
302+
.end((err, res) => {
303+
expect(res).to.have.status(500);
304+
expect(res.body.error).to.equal("Internal Server Error");
305+
done(err);
306+
});
307+
});
308+
309+
it("should return 500 when discord service throws an error", function (done) {
310+
sinon.stub(discordRolesModel, "isGroupRoleExists").resolves({
311+
roleExists: true,
312+
existingRoles: { data: () => ({ ...roleData, roleid: roleData.roleid }) },
313+
});
314+
315+
sinon.stub(discordServices, "deleteGroupRoleFromDiscord").resolves({
316+
success: false,
317+
message: "Internal server error",
318+
});
319+
320+
chai
321+
.request(app)
322+
.delete(`/discord-actions/groups/${groupId}?dev=true`)
323+
.set("cookie", `${cookieName}=${superUserAuthToken}`)
324+
.end((err, res) => {
325+
expect(res).to.have.status(500);
326+
expect(res.body.error).to.equal("Internal Server Error");
327+
done(err);
328+
});
329+
});
330+
331+
it("should successfully delete a group role from database", function (done) {
332+
sinon.stub(discordRolesModel, "isGroupRoleExists").resolves({
333+
roleExists: true,
334+
existingRoles: { data: () => ({ ...roleData, roleid: roleData.roleid }) },
335+
});
336+
337+
sinon.stub(discordServices, "deleteGroupRoleFromDiscord").resolves({
338+
success: true,
339+
message: "Role deleted successfully",
340+
});
341+
342+
chai
343+
.request(app)
344+
.delete(`/discord-actions/groups/${groupId}?dev=true`)
345+
.set("cookie", `${cookieName}=${superUserAuthToken}`)
346+
.end((err, res) => {
347+
expect(res).to.have.status(200);
348+
expect(res.body.message).to.equal("Group role deleted successfully");
349+
done(err);
350+
});
351+
});
352+
353+
it("should return 500 when deletion fails", function (done) {
354+
sinon.restore();
355+
sinon.stub(discordRolesModel, "isGroupRoleExists").resolves({
356+
roleExists: true,
357+
existingRoles: { data: () => ({ ...roleData, roleid: roleData.roleid }) },
358+
});
359+
360+
sinon.stub(discordServices, "deleteGroupRoleFromDiscord").resolves({
361+
success: true,
362+
message: "Role deleted successfully",
363+
});
364+
365+
sinon.stub(discordRolesModel, "deleteGroupRole").resolves({ isSuccess: false });
366+
chai
367+
.request(app)
368+
.delete(`/discord-actions/groups/${groupId}?dev=true`)
369+
.set("cookie", `${cookieName}=${superUserAuthToken}`)
370+
.end((err, res) => {
371+
expect(res).to.have.status(500);
372+
expect(res.body.error).to.equal("Internal Server Error");
373+
done(err);
374+
});
375+
});
376+
377+
it("should return 500 when an internal error occurs", function (done) {
378+
sinon.restore();
379+
sinon.stub(discordRolesModel, "isGroupRoleExists").throws(new Error("Database error"));
380+
chai
381+
.request(app)
382+
.delete(`/discord-actions/groups/${groupId}?dev=true`)
383+
.set("cookie", `${cookieName}=${superUserAuthToken}`)
384+
.end((err, res) => {
385+
expect(res).to.have.status(500);
386+
expect(res.body.error).to.equal("Internal Server Error");
387+
done(err);
388+
});
389+
});
390+
});
391+
216392
describe("POST /discord-actions/roles", function () {
217393
let roleid;
218394

0 commit comments

Comments
 (0)