diff --git a/server/db/db.js b/server/db/db.js
index 9a2be3c..b16361b 100644
--- a/server/db/db.js
+++ b/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,
};
diff --git a/server/socket/socket.js b/server/socket/socket.js
index 4f70c58..f2fcc1e 100644
--- a/server/socket/socket.js
+++ b/server/socket/socket.js
@@ -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);
});