Skip to content

Commit

Permalink
Feat: Super users can delete discord group roles (#2241)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
VaibhavSingh8 authored Nov 21, 2024
1 parent 20c9c1a commit 24d91f5
Show file tree
Hide file tree
Showing 7 changed files with 423 additions and 7 deletions.
56 changes: 56 additions & 0 deletions controllers/discordactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const discordRolesModel = require("../models/discordactions");
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,60 @@ const createGroupRole = async (req, res) => {
}
};

/**
* 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 { groupId } = req.params;

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

if (!roleExists) {
return res.boom.notFound("Group role not found");
}

const roleData = existingRoles.data();

const discordDeletion = await discordServices.deleteGroupRoleFromDiscord(roleData.roleid);

if (!discordDeletion.success) {
return res.boom.badImplementation(discordDeletion.message);
}

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

if (!isSuccess) {
logger.error(`Role deleted from Discord but failed to delete from database for groupId: ${groupId}`);
return res.boom.badImplementation("Group role deletion failed");
}

const groupDeletionLog = {
type: "group-role-deletion",
meta: {
userId: req.userData.id,
},
body: {
groupId: groupId,
roleName: roleData.rolename,
discordRoleId: roleData.roleid,
action: "delete",
},
};
await addLog(groupDeletionLog.type, groupDeletionLog.meta, groupDeletionLog.body);
return res.status(200).json({
message: "Group role deleted successfully",
});
} catch (error) {
logger.error(`Error while deleting group role: ${error}`);
return res.boom.badImplementation("Internal server error");
}
};

/**
* Gets all group-roles
*
Expand Down Expand Up @@ -491,4 +546,5 @@ module.exports = {
setRoleToUsersWith31DaysPlusOnboarding,
getUserDiscordInvite,
generateInviteForUser,
deleteGroupRole,
};
36 changes: 32 additions & 4 deletions models/discordactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,31 @@ const createNewRole = async (roleData) => {
}
};

/**
* 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 @@ -139,10 +164,13 @@ const updateGroupRole = async (roleData, docId) => {

const isGroupRoleExists = async (options = {}) => {
try {
const { rolename = null, roleid = null } = options;
const { groupId = null, rolename = null, roleid = null } = options;

let existingRoles;
if (rolename && roleid) {
if (groupId) {
existingRoles = await discordRoleModel.doc(groupId).get();
return { roleExists: existingRoles.exists, existingRoles };
} else if (rolename && roleid) {
existingRoles = await discordRoleModel
.where("rolename", "==", rolename)
.where("roleid", "==", roleid)
Expand All @@ -153,9 +181,8 @@ const isGroupRoleExists = async (options = {}) => {
} else if (roleid) {
existingRoles = await discordRoleModel.where("roleid", "==", roleid).limit(1).get();
} else {
throw Error("Either rolename or roleId is required");
throw Error("Either rolename, roleId, or groupId is required");
}

return { roleExists: !existingRoles.empty, existingRoles };
} catch (err) {
logger.error("Error in getting all group-roles", err);
Expand Down Expand Up @@ -1075,4 +1102,5 @@ module.exports = {
getUserDiscordInvite,
addInviteToInviteModel,
groupUpdateLastJoinDate,
deleteGroupRole,
};
11 changes: 10 additions & 1 deletion routes/discordactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const {
updateUsersNicknameStatus,
syncDiscordGroupRolesInFirestore,
setRoleToUsersWith31DaysPlusOnboarding,
deleteGroupRole,
} = require("../controllers/discordactions");
const {
validateGroupRoleBody,
Expand All @@ -29,11 +30,19 @@ const ROLES = require("../constants/roles");
const { Services } = require("../constants/bot");
const { verifyCronJob } = require("../middlewares/authorizeBot");
const { authorizeAndAuthenticate } = require("../middlewares/authorizeUsersAndService");

const { devFlagMiddleware } = require("../middlewares/devFlag");
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]),
devFlagMiddleware,
deleteGroupRole
);
router.post("/roles", authenticate, checkIsVerifiedDiscord, validateMemberRoleBody, addGroupRoleToMember);
router.get("/invite", authenticate, getUserDiscordInvite);
router.post("/invite", authenticate, checkCanGenerateDiscordLink, generateInviteForUser);
Expand Down
36 changes: 35 additions & 1 deletion services/discordService.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,40 @@ 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
*/

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

if (response.status === 204) {
return { success: true, message: "Role deleted successfully" };
}

return { success: false, message: "Failed to delete role from discord" };
} catch (err) {
logger.error("Error deleting role from Discord", err);
return { success: false, message: "Internal server error" };
}
};

Expand All @@ -114,4 +147,5 @@ module.exports = {
addRoleToUser,
removeRoleFromUser,
setUserDiscordNickname,
deleteGroupRoleFromDiscord,
};
176 changes: 176 additions & 0 deletions test/integration/discordactions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,182 @@ describe("Discord actions", function () {
});
});

describe("DELETE /discord-actions/groups/:groupId", function () {
let groupId;
// eslint-disable-next-line mocha/no-setup-in-describe
const roleData = groupData[0];

beforeEach(async function () {
const docRef = await discordRoleModel.add(roleData);
groupId = docRef.id;

superUserId = await addUser(superUser);
superUserAuthToken = authService.generateAuthToken({ userId: superUserId });

sinon.stub(discordRolesModel, "deleteGroupRole").resolves({ isSuccess: true });
});

afterEach(async function () {
sinon.restore();
await cleanDb();
});

it("should return 404 when not in dev mode", function (done) {
chai
.request(app)
.delete(`/discord-actions/groups/${groupId}`)
.set("cookie", `${cookieName}=${superUserAuthToken}`)
.end((err, res) => {
expect(res).to.have.status(404);
expect(res.body.error).to.equal("Not Found");
done(err);
});
});

it("should return 404 if group role not found", function (done) {
sinon.stub(discordRolesModel, "isGroupRoleExists").resolves({
roleExists: false,
existingRoles: { data: () => ({ ...roleData, roleid: roleData.roleid }) },
});

chai
.request(app)
.delete(`/discord-actions/groups/${groupId}?dev=true`)
.set("cookie", `${cookieName}=${superUserAuthToken}`)
.end((err, res) => {
expect(res).to.have.status(404);
expect(res.body.error).to.equal("Not Found");
done(err);
});
});

it("should successfully delete the group role from discord server", function (done) {
sinon.stub(discordRolesModel, "isGroupRoleExists").resolves({
roleExists: true,
existingRoles: { data: () => ({ ...roleData, roleid: roleData.roleid }) },
});

sinon.stub(discordServices, "deleteGroupRoleFromDiscord").resolves({
success: true,
message: "Role deleted successfully",
});

chai
.request(app)
.delete(`/discord-actions/groups/${groupId}?dev=true`)
.set("cookie", `${cookieName}=${superUserAuthToken}`)
.end((err, res) => {
expect(res).to.have.status(200);
expect(res.body.message).to.equal("Group role deleted successfully");
done(err);
});
});

it("should return 500 when discord role deletion fails", function (done) {
sinon.stub(discordRolesModel, "isGroupRoleExists").resolves({
roleExists: true,
existingRoles: { data: () => ({ ...roleData, roleid: roleData.roleid }) },
});

sinon.stub(discordServices, "deleteGroupRoleFromDiscord").resolves({
success: false,
message: "Failed to delete role from Discord",
});

chai
.request(app)
.delete(`/discord-actions/groups/${groupId}?dev=true`)
.set("cookie", `${cookieName}=${superUserAuthToken}`)
.end((err, res) => {
expect(res).to.have.status(500);
expect(res.body.error).to.equal("Internal Server Error");
done(err);
});
});

it("should return 500 when discord service throws an error", function (done) {
sinon.stub(discordRolesModel, "isGroupRoleExists").resolves({
roleExists: true,
existingRoles: { data: () => ({ ...roleData, roleid: roleData.roleid }) },
});

sinon.stub(discordServices, "deleteGroupRoleFromDiscord").resolves({
success: false,
message: "Internal server error",
});

chai
.request(app)
.delete(`/discord-actions/groups/${groupId}?dev=true`)
.set("cookie", `${cookieName}=${superUserAuthToken}`)
.end((err, res) => {
expect(res).to.have.status(500);
expect(res.body.error).to.equal("Internal Server Error");
done(err);
});
});

it("should successfully delete a group role from database", function (done) {
sinon.stub(discordRolesModel, "isGroupRoleExists").resolves({
roleExists: true,
existingRoles: { data: () => ({ ...roleData, roleid: roleData.roleid }) },
});

sinon.stub(discordServices, "deleteGroupRoleFromDiscord").resolves({
success: true,
message: "Role deleted successfully",
});

chai
.request(app)
.delete(`/discord-actions/groups/${groupId}?dev=true`)
.set("cookie", `${cookieName}=${superUserAuthToken}`)
.end((err, res) => {
expect(res).to.have.status(200);
expect(res.body.message).to.equal("Group role deleted successfully");
done(err);
});
});

it("should return 500 when deletion fails", function (done) {
sinon.restore();
sinon.stub(discordRolesModel, "isGroupRoleExists").resolves({
roleExists: true,
existingRoles: { data: () => ({ ...roleData, roleid: roleData.roleid }) },
});

sinon.stub(discordServices, "deleteGroupRoleFromDiscord").resolves({
success: true,
message: "Role deleted successfully",
});

sinon.stub(discordRolesModel, "deleteGroupRole").resolves({ isSuccess: false });
chai
.request(app)
.delete(`/discord-actions/groups/${groupId}?dev=true`)
.set("cookie", `${cookieName}=${superUserAuthToken}`)
.end((err, res) => {
expect(res).to.have.status(500);
expect(res.body.error).to.equal("Internal Server Error");
done(err);
});
});

it("should return 500 when an internal error occurs", function (done) {
sinon.restore();
sinon.stub(discordRolesModel, "isGroupRoleExists").throws(new Error("Database error"));
chai
.request(app)
.delete(`/discord-actions/groups/${groupId}?dev=true`)
.set("cookie", `${cookieName}=${superUserAuthToken}`)
.end((err, res) => {
expect(res).to.have.status(500);
expect(res.body.error).to.equal("Internal Server Error");
done(err);
});
});
});

describe("POST /discord-actions/roles", function () {
let roleid;

Expand Down
Loading

0 comments on commit 24d91f5

Please sign in to comment.