added ability to remove and add administrators
This commit is contained in:
255
server/db/db.js
255
server/db/db.js
@@ -112,54 +112,53 @@ async function createTables() {
|
||||
|
||||
// Create GroupAdmins Table with Trigger, referencing UUID
|
||||
await client.query(`
|
||||
-- Create the base table
|
||||
CREATE TABLE IF NOT EXISTS GroupAdmins (
|
||||
conversation_id UUID NOT NULL REFERENCES Conversations(conversation_id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES Accounts(user_id) ON DELETE CASCADE,
|
||||
granted_by UUID NOT NULL REFERENCES Accounts(user_id) ON DELETE CASCADE,
|
||||
granted_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
PRIMARY KEY (conversation_id, user_id)
|
||||
);
|
||||
-- Create the GroupAdmins table with owner flag
|
||||
CREATE TABLE IF NOT EXISTS GroupAdmins (
|
||||
conversation_id UUID NOT NULL REFERENCES Conversations(conversation_id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES Accounts(user_id) ON DELETE CASCADE,
|
||||
granted_by UUID NOT NULL REFERENCES Accounts(user_id) ON DELETE CASCADE,
|
||||
granted_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
is_owner BOOLEAN DEFAULT FALSE,
|
||||
PRIMARY KEY (conversation_id, user_id),
|
||||
-- Ensure only one owner per group
|
||||
CONSTRAINT single_owner EXCLUDE USING btree (conversation_id WITH =)
|
||||
WHERE (is_owner = true)
|
||||
);
|
||||
|
||||
-- Create indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_group_admins_conversation_id ON GroupAdmins (conversation_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_group_admins_user_id ON GroupAdmins (user_id);
|
||||
-- Create indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_group_admins_conversation_id ON GroupAdmins (conversation_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_group_admins_user_id ON GroupAdmins (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_group_admins_owner ON GroupAdmins (conversation_id) WHERE is_owner = true;
|
||||
|
||||
-- Create the validation function
|
||||
CREATE OR REPLACE FUNCTION validate_admin_grant()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Allow self-grant for the first admin of a conversation
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM GroupAdmins
|
||||
WHERE conversation_id = NEW.conversation_id
|
||||
) THEN
|
||||
-- For subsequent admins, verify that the granter is an admin
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM GroupAdmins
|
||||
WHERE conversation_id = NEW.conversation_id
|
||||
AND user_id = NEW.granted_by
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Only existing admins can grant admin privileges';
|
||||
END IF;
|
||||
ELSE
|
||||
-- For the first admin, only allow self-grant
|
||||
IF NEW.granted_by != NEW.user_id THEN
|
||||
RAISE EXCEPTION 'First admin must be self-granted';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create the trigger
|
||||
DROP TRIGGER IF EXISTS validate_admin_grant_trigger ON GroupAdmins;
|
||||
CREATE TRIGGER validate_admin_grant_trigger
|
||||
BEFORE INSERT OR UPDATE ON GroupAdmins
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION validate_admin_grant();
|
||||
`);
|
||||
-- Create the validation function
|
||||
CREATE OR REPLACE FUNCTION validate_admin_grant()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Check if this is the first admin (owner) of the group
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM GroupAdmins
|
||||
WHERE conversation_id = NEW.conversation_id
|
||||
) THEN
|
||||
-- First admin must be self-granted and marked as owner
|
||||
IF NEW.granted_by != NEW.user_id THEN
|
||||
RAISE EXCEPTION 'First admin must be self-granted';
|
||||
END IF;
|
||||
NEW.is_owner := true;
|
||||
ELSE
|
||||
-- For non-owner admins, verify that the granter is an admin
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM GroupAdmins
|
||||
WHERE conversation_id = NEW.conversation_id
|
||||
AND user_id = NEW.granted_by
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Only existing admins can grant admin privileges';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
`);
|
||||
console.log("Successfully created GroupAdmins table with trigger");
|
||||
} catch (e) {
|
||||
console.error("Failed to create tables: ", e);
|
||||
@@ -252,8 +251,8 @@ async function createGroup(user_id, groupName) {
|
||||
`;
|
||||
|
||||
const insertGroupAdminQuery = `
|
||||
INSERT INTO GroupAdmins (conversation_id, user_id, granted_by, granted_at)
|
||||
VALUES ($1, $2, $3, NOW())
|
||||
INSERT INTO GroupAdmins (conversation_id, user_id, granted_by, is_owner)
|
||||
VALUES ($1, $2, $3, true)
|
||||
RETURNING granted_at;
|
||||
`;
|
||||
|
||||
@@ -263,6 +262,7 @@ async function createGroup(user_id, groupName) {
|
||||
]);
|
||||
const group_id = createConversation.rows[0].group_id;
|
||||
|
||||
// Make the creator the owner and first admin
|
||||
const insertGroupAdmin = await client.query(insertGroupAdminQuery, [
|
||||
group_id,
|
||||
user_id,
|
||||
@@ -271,11 +271,6 @@ async function createGroup(user_id, groupName) {
|
||||
|
||||
if (insertGroupAdmin.rowCount > 0) {
|
||||
const contact_user_id = await addMemberToGroupById(group_id, user_id);
|
||||
// if (errorMessage) {
|
||||
// console.error("You are not an admin of the conversation");
|
||||
// return errorMessage;
|
||||
// }
|
||||
console.log("create group: ", group_id, contact_user_id);
|
||||
insertContactById(user_id, group_id, true);
|
||||
return { group_id, contact_user_id };
|
||||
}
|
||||
@@ -857,7 +852,8 @@ async function getMembers(conversation_id) {
|
||||
CASE
|
||||
WHEN ga.user_id IS NOT NULL THEN TRUE
|
||||
ELSE FALSE
|
||||
END AS isAdmin
|
||||
END AS isAdmin,
|
||||
COALESCE(ga.is_owner, FALSE) AS isOwner
|
||||
FROM Memberships m
|
||||
JOIN Accounts a ON m.user_id = a.user_id
|
||||
LEFT JOIN GroupAdmins ga ON m.user_id = ga.user_id AND m.conversation_id = ga.conversation_id
|
||||
@@ -925,45 +921,34 @@ async function removeUserFromGroupById(conversation_id, user_id) {
|
||||
WHERE conversation_id = $1 AND user_id = $2;
|
||||
`;
|
||||
|
||||
// const removeConversationContactQuery = `
|
||||
// DELETE FROM Contacts
|
||||
// WHERE conversation_id = $1 AND user_id = $2;
|
||||
// `;
|
||||
|
||||
try {
|
||||
// First, remove the user from the Memberships table
|
||||
// Check if the user being removed is the owner
|
||||
const isOwner = await isGroupOwner(user_id, conversation_id);
|
||||
if (isOwner) {
|
||||
return { message: "Cannot remove the group owner" };
|
||||
}
|
||||
|
||||
const removeMembershipResult = await client.query(
|
||||
removeUserFromGroupQuery,
|
||||
[conversation_id, user_id],
|
||||
);
|
||||
|
||||
if (removeMembershipResult.rowCount === 0) {
|
||||
console.log(
|
||||
`No membership found for user_id: ${user_id} in conversation_id: ${conversation_id}`,
|
||||
);
|
||||
return {
|
||||
message: `No membership found for user_id: ${user_id} in conversation_id: ${conversation_id}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Then, remove the user from the Contacts table
|
||||
// const removeContactResult = await client.query(removeConversationContactQuery, [
|
||||
// conversation_id,
|
||||
// user_id,
|
||||
// ]);
|
||||
//
|
||||
// if (removeContactResult.rowCount === 0) {
|
||||
// console.log(
|
||||
// `No contact found for user_id: ${user_id} in conversation_id: ${conversation_id}`,
|
||||
// );
|
||||
// return {
|
||||
// message: `No contact found for user_id: ${user_id} in conversation_id: ${conversation_id}`,
|
||||
// };
|
||||
// }
|
||||
|
||||
console.log(
|
||||
`Successfully removed user_id: ${user_id} from conversation_id: ${conversation_id}`,
|
||||
// Also remove from GroupAdmins if they were an admin
|
||||
await client.query(
|
||||
`
|
||||
DELETE FROM GroupAdmins
|
||||
WHERE conversation_id = $1 AND user_id = $2;
|
||||
`,
|
||||
[conversation_id, user_id],
|
||||
);
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
console.error("Failed to remove user from group ", e);
|
||||
return {
|
||||
@@ -995,6 +980,114 @@ async function isConversationMember(user_id, conversation_id) {
|
||||
}
|
||||
}
|
||||
|
||||
async function addAdministrator(conversation_id, user_id, granted_by) {
|
||||
const checkMembershipQuery = `
|
||||
SELECT 1 FROM Memberships
|
||||
WHERE conversation_id = $1 AND user_id = $2
|
||||
LIMIT 1;
|
||||
`;
|
||||
|
||||
const addAdminQuery = `
|
||||
INSERT INTO GroupAdmins (conversation_id, user_id, granted_by, is_owner)
|
||||
VALUES ($1, $2, $3, false)
|
||||
RETURNING granted_at;
|
||||
`;
|
||||
|
||||
try {
|
||||
// Check if the granter is the owner
|
||||
const isOwner = await isGroupOwner(granted_by, conversation_id);
|
||||
if (!isOwner) {
|
||||
console.error("Only the group owner can add administrators");
|
||||
return { message: "Only the group owner can add administrators" };
|
||||
}
|
||||
|
||||
// Check if user is a member
|
||||
const membershipCheck = await client.query(checkMembershipQuery, [
|
||||
conversation_id,
|
||||
user_id,
|
||||
]);
|
||||
if (membershipCheck.rows.length === 0) {
|
||||
console.error("User is not a member of the conversation");
|
||||
return { message: "User is not a member of this conversation" };
|
||||
}
|
||||
|
||||
const result = await client.query(addAdminQuery, [
|
||||
conversation_id,
|
||||
user_id,
|
||||
granted_by,
|
||||
]);
|
||||
console.log("User added as admin successfully");
|
||||
return result.rows[0].granted_at;
|
||||
} catch (e) {
|
||||
console.error("Failed to add administrator ", e);
|
||||
return { message: "Failed to add administrator" };
|
||||
}
|
||||
}
|
||||
|
||||
async function removeAdministrator(conversation_id, user_id, removed_by) {
|
||||
const removeAdminQuery = `
|
||||
DELETE FROM GroupAdmins
|
||||
WHERE conversation_id = $1
|
||||
AND user_id = $2
|
||||
RETURNING user_id;
|
||||
`;
|
||||
|
||||
const checkMembershipQuery = `
|
||||
SELECT 1 FROM Memberships
|
||||
WHERE conversation_id = $1 AND user_id = $2
|
||||
LIMIT 1;
|
||||
`;
|
||||
|
||||
try {
|
||||
const isOwner = await isGroupOwner(removed_by, conversation_id);
|
||||
if (!isOwner) {
|
||||
console.error("Only the group owner can remove administrators");
|
||||
return { message: "Only the group owner can remove administrators" };
|
||||
}
|
||||
|
||||
const membershipCheck = await client.query(checkMembershipQuery, [
|
||||
conversation_id,
|
||||
user_id,
|
||||
]);
|
||||
if (membershipCheck.rows.length === 0) {
|
||||
console.error("User is not a member of the conversation");
|
||||
return { message: "User is not a member of this group" };
|
||||
}
|
||||
|
||||
const removeAdminResult = await client.query(removeAdminQuery, [
|
||||
conversation_id,
|
||||
user_id,
|
||||
]);
|
||||
if (removeAdminResult.rows.length > 0) {
|
||||
return null;
|
||||
} else {
|
||||
return { message: "User is not an administrator of this group" };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to remove administrator ", e);
|
||||
return { message: "Failed to remove administrator" };
|
||||
}
|
||||
}
|
||||
|
||||
async function isGroupOwner(user_id, conversation_id) {
|
||||
const query = `
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM GroupAdmins
|
||||
WHERE conversation_id = $1
|
||||
AND user_id = $2
|
||||
AND is_owner = true
|
||||
) AS is_owner;
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await client.query(query, [conversation_id, user_id]);
|
||||
return result.rows[0].is_owner;
|
||||
} catch (e) {
|
||||
console.error("Failed to check owner status", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
client,
|
||||
insertUser,
|
||||
@@ -1017,4 +1110,6 @@ module.exports = {
|
||||
removeUserFromGroupById,
|
||||
isConversationMember,
|
||||
isAdmin,
|
||||
addAdministrator,
|
||||
removeAdministrator,
|
||||
};
|
||||
|
||||
@@ -5,11 +5,12 @@ const {
|
||||
deleteMessage,
|
||||
removeUserFromGroupById,
|
||||
isConversationMember,
|
||||
isAdmin,
|
||||
addAdministrator,
|
||||
removeAdministrator,
|
||||
} = require("../db/db");
|
||||
const { isValidUsername } = require("../utils/filter");
|
||||
const { verifyJwtToken } = require("../auth/jwt");
|
||||
const console = require("node:console");
|
||||
const { call } = require("express");
|
||||
|
||||
function initializeSocket(io) {
|
||||
io.use((socket, next) => {
|
||||
@@ -226,11 +227,11 @@ function initializeSocket(io) {
|
||||
socket.on("remove user from group", async (msg, callback) => {
|
||||
const { group_id, user_id } = msg;
|
||||
|
||||
if (!group_id || !user_id) {
|
||||
return callback({
|
||||
status: "error",
|
||||
message: "Missing required parameters",
|
||||
});
|
||||
if (!group_id) {
|
||||
return callback({ status: "error", message: "No group id provided" });
|
||||
}
|
||||
if (!user_id) {
|
||||
return callback({ status: "error", message: "No user id provided" });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -242,7 +243,7 @@ function initializeSocket(io) {
|
||||
|
||||
// Get all sockets in the room
|
||||
const socketsInRoom = await io.in(group_id).fetchSockets();
|
||||
|
||||
// Remove user from room
|
||||
for (const socketInstance of socketsInRoom) {
|
||||
if (socketInstance.user_id === user_id) {
|
||||
socketInstance.leave(group_id);
|
||||
@@ -253,7 +254,9 @@ function initializeSocket(io) {
|
||||
group_id,
|
||||
user_id,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Successfully removed user: ${user_id}, from group: ${group_id}`,
|
||||
);
|
||||
return callback({
|
||||
status: "ok",
|
||||
message: "Successfully removed user from group",
|
||||
@@ -267,6 +270,75 @@ function initializeSocket(io) {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("added administrator", async (msg, callback) => {
|
||||
const { group_id, user_id } = msg;
|
||||
if (!group_id) {
|
||||
return callback({
|
||||
status: "error",
|
||||
message: "No conversation id provided",
|
||||
});
|
||||
}
|
||||
if (!user_id) {
|
||||
return callback({ status: "error", message: "No user id provided" });
|
||||
}
|
||||
const isUserAdmin = await isAdmin(socket.user_id, group_id);
|
||||
if (!isUserAdmin) {
|
||||
return callback({
|
||||
status: "error",
|
||||
message: "You is not an administrator",
|
||||
});
|
||||
}
|
||||
const result = await addAdministrator(group_id, user_id, socket.user_id);
|
||||
if (result?.message) {
|
||||
return callback({ status: "error", message: result.message });
|
||||
}
|
||||
console.log(
|
||||
`Successfully added user: ${user_id} to administrators to group: ${group_id}`,
|
||||
);
|
||||
io.to(group_id).emit("added administrator", {
|
||||
group_id,
|
||||
user_id,
|
||||
});
|
||||
|
||||
return callback({
|
||||
status: "ok",
|
||||
message: "Successfully added administrator",
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("removed administrator", async (msg, callback) => {
|
||||
const { group_id, user_id } = msg;
|
||||
if (!group_id) {
|
||||
return callback({
|
||||
status: "error",
|
||||
message: "No conversation id provided",
|
||||
});
|
||||
}
|
||||
if (!user_id) {
|
||||
return callback({ status: "error", message: "No user id provided" });
|
||||
}
|
||||
|
||||
const result = await removeAdministrator(
|
||||
group_id,
|
||||
user_id,
|
||||
socket.user_id,
|
||||
);
|
||||
if (result?.message) {
|
||||
return callback({ status: "error", message: result.message });
|
||||
}
|
||||
console.log(
|
||||
`Successfully removed user: ${user_id} from administrators in group: ${group_id}`,
|
||||
);
|
||||
io.to(group_id).emit("removed administrator", {
|
||||
group_id,
|
||||
user_id,
|
||||
});
|
||||
return callback({
|
||||
status: "ok",
|
||||
message: "Successfully removed administrator",
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("disconnect", (reason) => {
|
||||
console.log("(socket)", socket.id, " disconnected due to: ", reason);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user