Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Super users can delete discord group roles #2241

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions controllers/discordactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
const discordServices = require("../services/discordService");
const { fetchAllUsers, fetchUser } = require("../models/users");
const { generateCloudFlareHeaders } = require("../utils/discord-actions");
const { addLog } = require("../models/logs");
const discordDeveloperRoleId = config.get("discordDeveloperRoleId");
const discordMavenRoleId = config.get("discordMavenRoleId");

Expand Down Expand Up @@ -63,6 +64,79 @@
}
};

/**
* Controller function to handle the soft deletion of a group role.
*
* @param {Object} req - The request object
* @param {Object} res - The response object
* @returns {Promise<void>}
*/
const deleteGroupRole = async (req, res) => {
const isDevMode = req.query.dev === "true";

if (isDevMode) {
try {
const { groupId } = req.params;
const { roleid } = req.body;

const { roleExists, existingRoles } = await discordRolesModel.isGroupRoleExists({ roleid });

if (!roleExists) {
return res.status(404).json({
error: "Group role not found",
});
}

const roleData = existingRoles.docs[0].data();

// To be implemented later after availability of Discord API

// const discordDeletionSuccess = await discordServices.deleteGroupRoleFromDiscord(roleid);

// if (!discordDeletionSuccess) {
// console.log(`Failed to delete role from Discord with ID: ${roleid}`);
// return res.status(500).json({
// error: "Failed to delete role from Discord server",
// });
// }

const { isSuccess } = await discordRolesModel.deleteGroupRole(groupId, req.userData.id);

if (isSuccess) {
const groupDeletionLog = {
type: "group-role-deletion",
meta: {
userId: req.userData.id,
},
body: {
groupId: groupId,
roleName: roleData.rolename,
discordRoleId: roleid,
action: "delete",
},
};
await addLog(groupDeletionLog.type, groupDeletionLog.meta, groupDeletionLog.body);
return res.status(200).json({
message: "Group role deleted succesfully",
});
} else {
return res.status(400).json({
error: "Group role deletion failed",
});
}
} catch (error) {
logger.error(`Error while deleting group role: ${error}`);
return res.status(500).json({
error: "Internal server error",
});
}
} else {
return res.status(403).json({
error: "Feature not available in production mode.",
});
}
};

/**
* Gets all group-roles
*
Expand Down Expand Up @@ -286,7 +360,7 @@
const nickNameUpdatedUsers = [];
let counter = 0;
for (let i = 0; i < usersToBeEffected.length; i++) {
const { discordId, username, first_name: firstName } = usersToBeEffected[i];

Check warning on line 363 in controllers/discordactions.js

View workflow job for this annotation

GitHub Actions / build (20.11.x)

Variable Assigned to Object Injection Sink
try {
if (counter % 10 === 0 && counter !== 0) {
await new Promise((resolve) => setTimeout(resolve, 5500));
Expand All @@ -302,7 +376,7 @@
if (message) {
counter++;
totalNicknamesUpdated.count++;
nickNameUpdatedUsers.push(usersToBeEffected[i].id);

Check warning on line 379 in controllers/discordactions.js

View workflow job for this annotation

GitHub Actions / build (20.11.x)

Generic Object Injection Sink
}
}
} catch (error) {
Expand Down Expand Up @@ -491,4 +565,5 @@
setRoleToUsersWith31DaysPlusOnboarding,
getUserDiscordInvite,
generateInviteForUser,
deleteGroupRole,
};
26 changes: 26 additions & 0 deletions models/discordactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,31 @@
}
};

/**
* Soft deletes a group role by marking it as deleted in the database.
* This function updates the role document in Firestore, setting isDeleted to true
* and recording who deleted it and when.
*
* @param {string} groupId - The ID of the group role to be deleted
* @param {string} deletedBy - The ID of the user performing the deletion for logging purpose
* @returns {Promise<Object>} An object indicating whether the operation was successful
*/
const deleteGroupRole = async (groupId, deletedBy) => {
try {
const roleRef = admin.firestore().collection("discord-roles").doc(groupId);
await roleRef.update({
isDeleted: true,
deletedAt: admin.firestore.Timestamp.fromDate(new Date()),
deletedBy: deletedBy,
});

return { isSuccess: true };
} catch (error) {
logger.error(`Error in deleteGroupRole: ${error}`);
return { isSuccess: false };
}
};

const removeMemberGroup = async (roleId, discordId) => {
try {
const backendResponse = await deleteRoleFromDatabase(roleId, discordId);
Expand Down Expand Up @@ -496,7 +521,7 @@

for (let i = 0; i < nicknameUpdateBatches.length; i++) {
const promises = [];
const usersStatusDocsBatch = nicknameUpdateBatches[i];

Check warning on line 524 in models/discordactions.js

View workflow job for this annotation

GitHub Actions / build (20.11.x)

Variable Assigned to Object Injection Sink
usersStatusDocsBatch.forEach((document) => {
const doc = document.data();
const userId = doc.userId;
Expand Down Expand Up @@ -1075,4 +1100,5 @@
getUserDiscordInvite,
addInviteToInviteModel,
groupUpdateLastJoinDate,
deleteGroupRole,
};
3 changes: 2 additions & 1 deletion routes/discordactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
updateUsersNicknameStatus,
syncDiscordGroupRolesInFirestore,
setRoleToUsersWith31DaysPlusOnboarding,
deleteGroupRole,
} = require("../controllers/discordactions");
const {
validateGroupRoleBody,
Expand All @@ -29,11 +30,11 @@
const { Services } = require("../constants/bot");
const { verifyCronJob } = require("../middlewares/authorizeBot");
const { authorizeAndAuthenticate } = require("../middlewares/authorizeUsersAndService");

const router = express.Router();

router.post("/groups", authenticate, checkIsVerifiedDiscord, validateGroupRoleBody, createGroupRole);
router.get("/groups", authenticate, checkIsVerifiedDiscord, getAllGroupRoles);
router.delete("/groups/:groupId", authenticate, checkIsVerifiedDiscord, authorizeRoles([SUPERUSER]), deleteGroupRole);

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
This route handler performs
authorization
, but is not rate-limited.
This route handler performs
authorization
, but is not rate-limited.
router.post("/roles", authenticate, checkIsVerifiedDiscord, validateMemberRoleBody, addGroupRoleToMember);
router.get("/invite", authenticate, getUserDiscordInvite);
router.post("/invite", authenticate, checkCanGenerateDiscordLink, generateInviteForUser);
Expand Down
38 changes: 37 additions & 1 deletion services/discordService.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,51 @@ const setUserDiscordNickname = async (userName, discordId) => {
};
} catch (err) {
logger.error("Error in updating discord Nickname", err);
throw err;
throw new Error(err);
}
};

/**
* Deletes a group role from the Discord server.
* This function sends a DELETE request to the Discord API to remove the role.
* It's part of the soft delete process, where we remove the role from Discord
* but keep a record of it in our database.
*
* @param {string} roleId - The Discord ID of the role to be deleted
* @returns {Promise<Object>} The response from the Discord API
* @throws {Error} If the deletion fails or there's a network error
*/

// To be implemented later after availability of Discord API

// const deleteGroupRoleFromDiscord = async (roleId) => {
// try {
// const authToken = generateAuthTokenForCloudflare();
// const response = await fetch(`URL`, {
// method: "DELETE",
// headers: {
// "Content-Type": "application/json",
// Authorization: `Bearer ${authToken}`,
// },
// });

// if (!response.ok) {
// throw new Error(`Failed to delete role from discord`);
// }
// const result = await response.json();
// return result;
// } catch (err) {
// logger.error(`Error deleting role from Discord: ${err.message}`);
// throw new Error(err);
// }
// };

module.exports = {
getDiscordMembers,
getDiscordRoles,
setInDiscordFalseScript,
addRoleToUser,
removeRoleFromUser,
setUserDiscordNickname,
// deleteGroupRoleFromDiscord,
};
61 changes: 61 additions & 0 deletions test/unit/models/discordactions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const {
isGroupRoleExists,
addGroupRoleToMember,
deleteRoleFromDatabase,
deleteGroupRole,
updateDiscordImageForVerification,
enrichGroupDataWithMembershipInfo,
fetchGroupToUserMapping,
Expand Down Expand Up @@ -242,6 +243,66 @@ describe("discordactions", function () {
});
});

describe("deleteGroupRole", function () {
const groupId = "1234";
const deletedBy = "4321";
let firestoreOriginal;

beforeEach(async function () {
firestoreOriginal = admin.firestore;

const roleRef = admin.firestore().collection("discord-roles").doc(groupId);
await roleRef.set({
isDeleted: false,
});
});

it("should mark the group role as deleted", async function () {
const result = await deleteGroupRole(groupId, deletedBy);

const updatedDoc = await admin.firestore().collection("discord-roles").doc(groupId).get();

const data = updatedDoc.data();
expect(data.isDeleted).to.equal(true);
expect(data.deletedBy).to.equal(deletedBy);
expect(data.deletedAt).to.be.an.instanceof(admin.firestore.Timestamp);
expect(result.isSuccess).to.equal(true);
});

it("should return isSuccess as false if Firestore update fails", async function () {
delete require.cache[require.resolve("firebase-admin")];

const mockFirestore = {
collection: () => ({
doc: () => ({
update: async () => {
throw new Error("Database error");
},
}),
}),
};
// Replacing firestore implementation with our mock
Object.defineProperty(admin, "firestore", {
configurable: true,
get: () => () => mockFirestore,
});

const result = await deleteGroupRole(groupId, deletedBy);
expect(result.isSuccess).to.equal(false);
});

afterEach(async function () {
// Restoring original firestore implementation
Object.defineProperty(admin, "firestore", {
configurable: true,
value: firestoreOriginal,
});

const roleRef = admin.firestore().collection("discord-roles").doc(groupId);
await roleRef.delete();
});
});

describe("deleteRoleFromMember", function () {
let deleteStub;

Expand Down
Loading