implemented group admins
This commit is contained in:
230
server/db/db.js
230
server/db/db.js
@@ -43,8 +43,10 @@ async function createTables() {
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_username ON Accounts (username);
|
||||
`);
|
||||
console.log("Successfully created Accounts table");
|
||||
} catch (e) {
|
||||
console.error("Failed to create Accounts table: ", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -59,8 +61,10 @@ async function createTables() {
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_conversation_type ON Conversations (conversation_type);
|
||||
`);
|
||||
console.log("Successfully created Conversations table");
|
||||
} catch (e) {
|
||||
console.error("Failed to create Conversations table: ", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -78,8 +82,10 @@ async function createTables() {
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_user_id ON Messages (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_conversation_sent_at ON Messages (conversation_id, sent_at);
|
||||
`);
|
||||
console.log("Successfully created Messages table");
|
||||
} catch (e) {
|
||||
console.error("Failed to create Messages table: ", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -89,14 +95,16 @@ async function createTables() {
|
||||
conversation_id INT NOT NULL REFERENCES Conversations(conversation_id) ON DELETE CASCADE,
|
||||
user_id INT REFERENCES Accounts(user_id) ON DELETE CASCADE,
|
||||
joined_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
PRIMARY KEY (conversation_id, user_id) -- This composite primary key ensures uniqueness, but make sure that these fields are frequently queried together to avoid unnecessary performance overhead.
|
||||
PRIMARY KEY (conversation_id, user_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_memberships_conversation_id ON Memberships (conversation_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_memberships_user_id ON Memberships (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_memberships_conversation_joined_at ON Memberships (conversation_id, joined_at);
|
||||
`);
|
||||
console.log("Successfully created Memberships table");
|
||||
} catch (e) {
|
||||
console.error("Failed to create Memberships table: ", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -114,35 +122,70 @@ async function createTables() {
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_conversation_id ON Contacts (conversation_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_contact_id ON Contacts (contact_id);
|
||||
`);
|
||||
console.log("Successfully created Contacts table");
|
||||
} catch (e) {
|
||||
console.error("Failed to create Contacts table: ", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create GroupAdmins Table
|
||||
// Create GroupAdmins Table with Trigger
|
||||
await client.query(`
|
||||
-- Create the base table
|
||||
CREATE TABLE IF NOT EXISTS GroupAdmins (
|
||||
conversation_id INT NOT NULL REFERENCES Conversations(conversation_id) ON DELETE CASCADE,
|
||||
user_id INT NOT NULL REFERENCES Accounts(user_id) ON DELETE CASCADE,
|
||||
granted_by INT NOT NULL REFERENCES Accounts(user_id) ON DELETE CASCADE,
|
||||
granted_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
PRIMARY KEY (conversation_id, user_id),
|
||||
CONSTRAINT group_conversation_only CHECK (conversation_type = 'group'),
|
||||
CONSTRAINT admin_grants_admin CHECK (
|
||||
granted_by = user_id OR
|
||||
EXISTS (
|
||||
SELECT 1 FROM GroupAdmins ga
|
||||
WHERE ga.conversation_id = GroupAdmins.conversation_id
|
||||
AND ga.user_id = GroupAdmins.granted_by
|
||||
)
|
||||
)
|
||||
PRIMARY KEY (conversation_id, 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 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();
|
||||
`);
|
||||
console.log("Successfully created GroupAdmins table with trigger");
|
||||
} catch (e) {
|
||||
console.error("Failed to create GroupAdmins table: ", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
console.log("All tables created successfully");
|
||||
}
|
||||
|
||||
async function insertUser(username, passwordHash) {
|
||||
@@ -167,7 +210,6 @@ async function getUserId(username) {
|
||||
try {
|
||||
const result = await client.query(query, [username]);
|
||||
if (result.rows.length > 0) {
|
||||
console.log("GETUSERID: ", result.rows[0]);
|
||||
return result.rows[0];
|
||||
} else {
|
||||
console.log("No user found with username: ", username);
|
||||
@@ -222,25 +264,53 @@ async function insertMessage(
|
||||
}
|
||||
|
||||
async function createGroup(user_id, groupName) {
|
||||
const query = `
|
||||
const createConversationQuery = `
|
||||
INSERT INTO Conversations (conversation_type, name)
|
||||
VALUES ('group', $1)
|
||||
RETURNING conversation_id AS group_id;
|
||||
`;
|
||||
try {
|
||||
const result = await client.query(query, [groupName]);
|
||||
const group_id = result.rows[0].group_id;
|
||||
|
||||
const contact_user_id = await addMemberToGroup(group_id, user_id);
|
||||
insertContactById(user_id, group_id, true);
|
||||
return { group_id, contact_user_id };
|
||||
const insertGroupAdminQuery = `
|
||||
INSERT INTO GroupAdmins (conversation_id, user_id, granted_by, granted_at)
|
||||
VALUES ($1, $2, $3, NOW())
|
||||
RETURNING granted_at;
|
||||
`;
|
||||
|
||||
try {
|
||||
const createConversation = await client.query(createConversationQuery, [
|
||||
groupName,
|
||||
]);
|
||||
const group_id = createConversation.rows[0].group_id;
|
||||
|
||||
const insertGroupAdmin = await client.query(insertGroupAdminQuery, [
|
||||
group_id,
|
||||
user_id,
|
||||
user_id,
|
||||
]);
|
||||
|
||||
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 };
|
||||
}
|
||||
console.error("Failed to insert group admin");
|
||||
} catch (e) {
|
||||
console.error("Failed to create conversation ", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function addMemberToGroup(conversation_id, user_id) {
|
||||
async function addMemberToGroupById(conversation_id, user_id) {
|
||||
// const isAdminResult = await isAdmin(user_id, conversation_id);
|
||||
// if (!isAdminResult) {
|
||||
// return { errorMessage: "You are not an admin of the conversation" };
|
||||
// }
|
||||
|
||||
const query = `
|
||||
INSERT INTO Memberships (conversation_id, user_id)
|
||||
VALUES ($1, $2)
|
||||
@@ -259,6 +329,16 @@ async function addMemberToGroup(conversation_id, user_id) {
|
||||
}
|
||||
|
||||
async function addMemberToGroupByUsername(conversation_id, username) {
|
||||
// const { user_id } = await getUserId(username);
|
||||
// if (!user_id) {
|
||||
// return null;
|
||||
// }
|
||||
// const isAdminResult = await isAdmin(user_id, conversation_id);
|
||||
// if (!isAdminResult) {
|
||||
// console.error("You are not an admin of the conversation");
|
||||
// return null;
|
||||
// }
|
||||
|
||||
const query = `
|
||||
WITH user_id_query AS (
|
||||
SELECT user_id
|
||||
@@ -425,12 +505,7 @@ async function insertContactById(senderId, conversation_id, read) {
|
||||
|
||||
try {
|
||||
const result = await client.query(query, [senderId, conversation_id, read]);
|
||||
console.log(
|
||||
"Insertcontactbyid: ",
|
||||
result.rows[0],
|
||||
senderId,
|
||||
conversation_id,
|
||||
);
|
||||
console.log("Insertcontactbyid: ", senderId, conversation_id);
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
console.error("Failed to insert contact by IDs:", error);
|
||||
@@ -795,9 +870,14 @@ async function getMembers(conversation_id) {
|
||||
const query = `
|
||||
SELECT
|
||||
a.user_id,
|
||||
a.username
|
||||
a.username,
|
||||
CASE
|
||||
WHEN ga.user_id IS NOT NULL THEN TRUE
|
||||
ELSE FALSE
|
||||
END AS isAdmin
|
||||
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
|
||||
WHERE m.conversation_id = $1;
|
||||
`;
|
||||
|
||||
@@ -814,10 +894,102 @@ async function getMembers(conversation_id) {
|
||||
`Failed to get members for conversation_id ${conversation_id}`,
|
||||
e,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function isAdmin(user_id, conversation_id) {
|
||||
const query = `
|
||||
SELECT 1 FROM GroupAdmins
|
||||
WHERE user_id = $1
|
||||
AND conversation_id = $2;
|
||||
`;
|
||||
try {
|
||||
const result = await client.query(query, [user_id, conversation_id]);
|
||||
return result.rows.length > 0;
|
||||
} catch (e) {
|
||||
console.error("Failed to check admin status", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function getGroupAdmins(conversation_id) {
|
||||
const query = `
|
||||
SELECT user_id, granted_by
|
||||
FROM GroupAdmins
|
||||
WHERE conversation_id = $1;
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await client.query(query, [conversation_id]);
|
||||
const admins = result.rows.map((row) => ({
|
||||
user_id: row.user_id,
|
||||
granted_by: row.granted_by,
|
||||
}));
|
||||
return admins;
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Failed to get admins for conversation_id ${conversation_id}`,
|
||||
e,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeUserFromGroupById(conversation_id, user_id) {
|
||||
const removeUserFromGroupQuery = `
|
||||
DELETE FROM Memberships
|
||||
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
|
||||
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}`,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("Failed to remove user from group ", e);
|
||||
return {
|
||||
message: `Failed to remove user_id: ${user_id} from conversation_id: ${conversation_id}`,
|
||||
error: e.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
client,
|
||||
insertUser,
|
||||
@@ -832,10 +1004,10 @@ module.exports = {
|
||||
getContacts,
|
||||
updateContactStatus,
|
||||
createGroup,
|
||||
addMemberToGroup,
|
||||
addMemberToGroupByUsername,
|
||||
getConversationsForUser,
|
||||
contactSuggestion,
|
||||
deleteMessage,
|
||||
getMembers,
|
||||
removeUserFromGroupById,
|
||||
};
|
||||
|
||||
@@ -199,6 +199,7 @@ app.get("/api/auth/validate", authorizeUser, (req, res) => {
|
||||
return res.status(200).json({
|
||||
message: "Authorized",
|
||||
username: req.user.username,
|
||||
user_id: req.user.user_id,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -297,9 +298,6 @@ app.get(
|
||||
}
|
||||
try {
|
||||
const suggestions = await contactSuggestion(contact);
|
||||
console.log(
|
||||
`contacts suggestions for contact: ${contact}, suggestions: ${suggestions}`,
|
||||
);
|
||||
return res.status(200).json(suggestions);
|
||||
} catch (e) {
|
||||
res.status(500).json({ message: "Failed to get contact suggestions" });
|
||||
@@ -351,7 +349,13 @@ app.post("/api/chat/groups/create", authorizeUser, async (req, res) => {
|
||||
if (!groupname) {
|
||||
return res.status(400).json({ message: "Groupname not provided" });
|
||||
}
|
||||
const { group_id, contact_user_id } = await createGroup(user_id, groupname);
|
||||
const { group_id, contact_user_id, errorMessage } = await createGroup(
|
||||
user_id,
|
||||
groupname,
|
||||
);
|
||||
if (errorMessage) {
|
||||
return res.status(401).json({ message: errorMessage });
|
||||
}
|
||||
if (!group_id) {
|
||||
return res.status(500).json({ message: "Failed to create group" });
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ const {
|
||||
getConversationsForUser,
|
||||
deleteContact,
|
||||
deleteMessage,
|
||||
removeUserFromGroupById,
|
||||
} = require("../db/db");
|
||||
const { isValidUsername } = require("../utils/filter");
|
||||
const { verifyJwtToken } = require("../auth/jwt");
|
||||
@@ -202,6 +203,33 @@ function initializeSocket(io) {
|
||||
callback({ status: "ok", message: "Successfully deleted contact" });
|
||||
io.to(conversation_id).emit("left group", { conversation_id, user_id });
|
||||
});
|
||||
|
||||
socket.on("remove user from group", async (msg, callback) => {
|
||||
const { conversation_id, user_id } = msg;
|
||||
if (!conversation_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 removeUserFromGroupById(conversation_id, user_id);
|
||||
|
||||
if (result?.message) {
|
||||
return callback({ status: "error", messsage: result.message });
|
||||
}
|
||||
|
||||
io.to(conversation_id).emit("left group", { conversation_id, user_id });
|
||||
|
||||
return callback({
|
||||
status: "ok",
|
||||
message: "Successfully removed user from group",
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("disconnect", (reason) => {
|
||||
console.log("(socket)", socket.id, " disconnected due to: ", reason);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user