added ability to remove and add administrators
This commit is contained in:
@@ -122,7 +122,7 @@ function ContactForm({ InitializeContact, setContactsList }: ContactFormProps) {
|
|||||||
<input
|
<input
|
||||||
className="text-black bg-green-50 pl-2 shadow-lg rounded-md h-8 mb-2 mt-2"
|
className="text-black bg-green-50 pl-2 shadow-lg rounded-md h-8 mb-2 mt-2"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter contact"
|
placeholder="Search for user"
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
{...register('contact', {
|
{...register('contact', {
|
||||||
minLength: 4,
|
minLength: 4,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { axiosClient } from '@/App.tsx';
|
import { axiosClient } from '@/App.tsx';
|
||||||
import { ContactsProps } from '@/pages/Chat.tsx';
|
import { ContactsProps } from '@/pages/Chat.tsx';
|
||||||
import { socket } from '@/socket/socket.tsx';
|
import { socket } from '@/socket/socket.tsx';
|
||||||
import { Sword } from 'lucide-react';
|
import { Crown, Sword } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
@@ -11,38 +11,35 @@ import {
|
|||||||
} from '@/components/ui/context-menu';
|
} from '@/components/ui/context-menu';
|
||||||
import { UsernameType } from '@/utils/ProtectedRoutes.tsx';
|
import { UsernameType } from '@/utils/ProtectedRoutes.tsx';
|
||||||
import { useOutletContext } from 'react-router-dom';
|
import { useOutletContext } from 'react-router-dom';
|
||||||
import LoadingWheel from '@/components/chat/LoadingWheel.tsx';
|
|
||||||
|
|
||||||
type ParticipantsProps = {
|
type ParticipantsProps = {
|
||||||
user_id: string;
|
user_id: string;
|
||||||
username: string;
|
username: string;
|
||||||
isadmin: boolean;
|
isadmin: boolean;
|
||||||
|
isowner: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ParticipantsBarProps = {
|
type ParticipantsBarProps = {
|
||||||
contact: ContactsProps | null;
|
|
||||||
initializeContact: (contact: ContactsProps) => void;
|
initializeContact: (contact: ContactsProps) => void;
|
||||||
currentContact: ContactsProps | null;
|
currentContact: ContactsProps | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
function ParticipantsBar({
|
function ParticipantsBar({
|
||||||
contact,
|
|
||||||
initializeContact,
|
initializeContact,
|
||||||
currentContact,
|
currentContact,
|
||||||
}: ParticipantsBarProps) {
|
}: ParticipantsBarProps) {
|
||||||
const [participants, setParticipants] = useState<ParticipantsProps[]>([]);
|
const [participants, setParticipants] = useState<ParticipantsProps[]>([]);
|
||||||
const [isGroupAdmin, setIsGroupAdmin] = useState<boolean>(false);
|
const [isGroupAdmin, setIsGroupAdmin] = useState<boolean>(false);
|
||||||
const user: UsernameType = useOutletContext();
|
const user: UsernameType = useOutletContext();
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const getParticipants = async () => {
|
const getParticipants = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axiosClient.get(
|
const response = await axiosClient.get(
|
||||||
`/api/chat/groups/getMembers/${contact?.conversation_id}`,
|
`/api/chat/groups/getMembers/${currentContact?.conversation_id}`,
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
'getParticipants for: ',
|
'getParticipants for: ',
|
||||||
contact?.conversation_id,
|
currentContact?.conversation_id,
|
||||||
'response: ',
|
'response: ',
|
||||||
response.data,
|
response.data,
|
||||||
);
|
);
|
||||||
@@ -52,6 +49,60 @@ function ParticipantsBar({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRemoveUser = async (userId: string) => {
|
||||||
|
socket?.emit(
|
||||||
|
'remove user from group',
|
||||||
|
{
|
||||||
|
group_id: currentContact?.conversation_id,
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
(response: { status: 'ok' | 'error'; message: string }) => {
|
||||||
|
if (response.status == 'ok') {
|
||||||
|
console.log(response.message);
|
||||||
|
} else {
|
||||||
|
console.error('Failed to remove user from group: ', response.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// setParticipants((prevMembers) =>
|
||||||
|
// prevMembers.filter((member) => member.user_id !== userId),
|
||||||
|
// );
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddToAdministrators = async (userId: string) => {
|
||||||
|
socket?.emit(
|
||||||
|
'added administrator',
|
||||||
|
{ group_id: currentContact?.conversation_id, user_id: userId },
|
||||||
|
(response: { status: 'ok' | 'error'; message: string }) => {
|
||||||
|
if (response.status == 'ok') {
|
||||||
|
console.log(response.message);
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
'Failed to add user to administrators: ',
|
||||||
|
response.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFromAdministrators = async (userId: string) => {
|
||||||
|
socket?.emit(
|
||||||
|
'removed administrator',
|
||||||
|
{ group_id: currentContact?.conversation_id, user_id: userId },
|
||||||
|
(response: { status: 'ok' | 'error'; message: string }) => {
|
||||||
|
if (response.status == 'ok') {
|
||||||
|
console.log(response.message);
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
'Failed to remove user from administrators: ',
|
||||||
|
response.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (participants.length > 0 && user?.user_id) {
|
if (participants.length > 0 && user?.user_id) {
|
||||||
const userIsAdmin = participants.some(
|
const userIsAdmin = participants.some(
|
||||||
@@ -63,19 +114,20 @@ function ParticipantsBar({
|
|||||||
}, [participants, user?.user_id]);
|
}, [participants, user?.user_id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (contact) {
|
if (currentContact) {
|
||||||
getParticipants();
|
getParticipants();
|
||||||
}
|
}
|
||||||
}, [contact]);
|
}, [currentContact]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!socket || !contact) return;
|
if (!socket || !currentContact) return;
|
||||||
|
|
||||||
const handleAddedToGroup = (msg: {
|
const handleAddedToGroup = (msg: {
|
||||||
username: string;
|
username: string;
|
||||||
user_id: string;
|
user_id: string;
|
||||||
group_id: string;
|
group_id: string;
|
||||||
isadmin: false;
|
isadmin: boolean;
|
||||||
|
isowner: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { group_id } = msg;
|
const { group_id } = msg;
|
||||||
if (
|
if (
|
||||||
@@ -93,7 +145,7 @@ function ParticipantsBar({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (group_id === contact.conversation_id) {
|
if (group_id === currentContact.conversation_id) {
|
||||||
setParticipants((prevMembers) => {
|
setParticipants((prevMembers) => {
|
||||||
const existingMember = prevMembers.some(
|
const existingMember = prevMembers.some(
|
||||||
(m) => m.user_id === msg.user_id,
|
(m) => m.user_id === msg.user_id,
|
||||||
@@ -111,6 +163,7 @@ function ParticipantsBar({
|
|||||||
msg.group_id == currentContact?.conversation_id &&
|
msg.group_id == currentContact?.conversation_id &&
|
||||||
msg.user_id == user?.user_id
|
msg.user_id == user?.user_id
|
||||||
) {
|
) {
|
||||||
|
setParticipants([]);
|
||||||
initializeContact(currentContact);
|
initializeContact(currentContact);
|
||||||
}
|
}
|
||||||
setParticipants((prevMembers) =>
|
setParticipants((prevMembers) =>
|
||||||
@@ -118,6 +171,36 @@ function ParticipantsBar({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddToAdmins = (msg: { user_id: string; group_id: string }) => {
|
||||||
|
if (msg.group_id === currentContact.conversation_id) {
|
||||||
|
setParticipants((prevMembers) =>
|
||||||
|
prevMembers.map((member) =>
|
||||||
|
member.user_id === msg.user_id
|
||||||
|
? { ...member, isadmin: true }
|
||||||
|
: member,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFromAdmins = (msg: {
|
||||||
|
user_id: string;
|
||||||
|
group_id: string;
|
||||||
|
}) => {
|
||||||
|
console.log('(socket) removed administrator: ', msg);
|
||||||
|
if (msg.group_id === currentContact.conversation_id) {
|
||||||
|
setParticipants((prevMembers) =>
|
||||||
|
prevMembers.map((member) =>
|
||||||
|
member.user_id === msg.user_id
|
||||||
|
? { ...member, isadmin: false }
|
||||||
|
: member,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on('added administrator', handleAddToAdmins);
|
||||||
|
socket.on('removed administrator', handleRemoveFromAdmins);
|
||||||
socket.on('added to group', handleAddedToGroup);
|
socket.on('added to group', handleAddedToGroup);
|
||||||
socket.on('left group', handleLeftGroup);
|
socket.on('left group', handleLeftGroup);
|
||||||
|
|
||||||
@@ -125,36 +208,7 @@ function ParticipantsBar({
|
|||||||
socket?.off('added to group', handleAddedToGroup);
|
socket?.off('added to group', handleAddedToGroup);
|
||||||
socket?.off('left group', handleLeftGroup);
|
socket?.off('left group', handleLeftGroup);
|
||||||
};
|
};
|
||||||
}, [socket, contact, currentContact, user?.user_id]);
|
}, [socket, currentContact, currentContact, user?.user_id]);
|
||||||
|
|
||||||
const handleRemoveUser = async (userId: string) => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
socket?.emit(
|
|
||||||
'remove user from group',
|
|
||||||
{
|
|
||||||
group_id: contact?.conversation_id,
|
|
||||||
user_id: userId,
|
|
||||||
},
|
|
||||||
(response: { status: 'ok' | 'error'; message: string }) => {
|
|
||||||
if (response.status == 'ok') {
|
|
||||||
setIsLoading(false);
|
|
||||||
} else {
|
|
||||||
setIsLoading(false);
|
|
||||||
console.error(
|
|
||||||
'Failed to remove user from group: ',
|
|
||||||
response.message,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
// setParticipants((prevMembers) =>
|
|
||||||
// prevMembers.filter((member) => member.user_id !== userId),
|
|
||||||
// );
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to remove user:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const ParticipantsList = participants?.map(
|
const ParticipantsList = participants?.map(
|
||||||
(participant: ParticipantsProps) => (
|
(participant: ParticipantsProps) => (
|
||||||
@@ -163,6 +217,11 @@ function ParticipantsBar({
|
|||||||
<li className="p-2 hover:bg-gray-700 rounded-md flex items-center justify-between group">
|
<li className="p-2 hover:bg-gray-700 rounded-md flex items-center justify-between group">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
{participant.username}
|
{participant.username}
|
||||||
|
{participant.isowner ? (
|
||||||
|
<span className="flex items-center text-yellow-400 text-xs">
|
||||||
|
<Crown className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
{participant.isadmin && (
|
{participant.isadmin && (
|
||||||
<span className="flex items-center text-green-300 text-xs">
|
<span className="flex items-center text-green-300 text-xs">
|
||||||
<Sword className="h-3 w-3" />
|
<Sword className="h-3 w-3" />
|
||||||
@@ -174,21 +233,39 @@ function ParticipantsBar({
|
|||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
{isGroupAdmin && user.user_id !== participant.user_id && (
|
{user.user_id !== participant.user_id && isGroupAdmin ? (
|
||||||
<ContextMenuContent className="p-0">
|
<ContextMenuContent className="p-0">
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
className="bg-zinc-900 text-white outline-1 hover:bg-zinc-800 hover:cursor-pointer"
|
className="bg-zinc-900 text-white outline-1 hover:bg-zinc-800 hover:cursor-pointer"
|
||||||
onClick={() => handleRemoveUser(participant.user_id)}
|
onClick={() => handleRemoveUser(participant.user_id)}
|
||||||
>
|
>
|
||||||
{isLoading ? <LoadingWheel /> : <p>Remove from group</p>}
|
<p>Remove from group</p>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
|
{!participant.isadmin ? (
|
||||||
|
<ContextMenuItem
|
||||||
|
className="bg-zinc-900 text-white outline-1 hover:bg-zinc-800 hover:cursor-pointer"
|
||||||
|
onClick={() => handleAddToAdministrators(participant.user_id)}
|
||||||
|
>
|
||||||
|
<p>Add to group administrator</p>
|
||||||
|
</ContextMenuItem>
|
||||||
|
) : null}
|
||||||
|
{participant.isadmin ? (
|
||||||
|
<ContextMenuItem
|
||||||
|
className="bg-zinc-900 text-white outline-1 hover:bg-zinc-800 hover:cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
handleRemoveFromAdministrators(participant.user_id)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p>Remove from administrator</p>
|
||||||
|
</ContextMenuItem>
|
||||||
|
) : null}
|
||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
)}
|
) : null}
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!contact) {
|
if (!currentContact) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -238,7 +238,6 @@ function Chat() {
|
|||||||
{currentContact?.type == 'group' ? (
|
{currentContact?.type == 'group' ? (
|
||||||
<div className="w-80 bg-[#1E1E1E] flex-shrink-0">
|
<div className="w-80 bg-[#1E1E1E] flex-shrink-0">
|
||||||
<ParticipantsBar
|
<ParticipantsBar
|
||||||
contact={currentContact}
|
|
||||||
initializeContact={initializeContact}
|
initializeContact={initializeContact}
|
||||||
currentContact={currentContact}
|
currentContact={currentContact}
|
||||||
/>
|
/>
|
||||||
|
|||||||
255
server/db/db.js
255
server/db/db.js
@@ -112,54 +112,53 @@ async function createTables() {
|
|||||||
|
|
||||||
// Create GroupAdmins Table with Trigger, referencing UUID
|
// Create GroupAdmins Table with Trigger, referencing UUID
|
||||||
await client.query(`
|
await client.query(`
|
||||||
-- Create the base table
|
-- Create the GroupAdmins table with owner flag
|
||||||
CREATE TABLE IF NOT EXISTS GroupAdmins (
|
CREATE TABLE IF NOT EXISTS GroupAdmins (
|
||||||
conversation_id UUID NOT NULL REFERENCES Conversations(conversation_id) ON DELETE CASCADE,
|
conversation_id UUID NOT NULL REFERENCES Conversations(conversation_id) ON DELETE CASCADE,
|
||||||
user_id UUID NOT NULL REFERENCES Accounts(user_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_by UUID NOT NULL REFERENCES Accounts(user_id) ON DELETE CASCADE,
|
||||||
granted_at TIMESTAMPTZ DEFAULT NOW(),
|
granted_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
PRIMARY KEY (conversation_id, user_id)
|
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 indexes
|
||||||
CREATE INDEX IF NOT EXISTS idx_group_admins_conversation_id ON GroupAdmins (conversation_id);
|
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_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 the validation function
|
||||||
CREATE OR REPLACE FUNCTION validate_admin_grant()
|
CREATE OR REPLACE FUNCTION validate_admin_grant()
|
||||||
RETURNS TRIGGER AS $$
|
RETURNS TRIGGER AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
-- Allow self-grant for the first admin of a conversation
|
-- Check if this is the first admin (owner) of the group
|
||||||
IF EXISTS (
|
IF NOT EXISTS (
|
||||||
SELECT 1 FROM GroupAdmins
|
SELECT 1 FROM GroupAdmins
|
||||||
WHERE conversation_id = NEW.conversation_id
|
WHERE conversation_id = NEW.conversation_id
|
||||||
) THEN
|
) THEN
|
||||||
-- For subsequent admins, verify that the granter is an admin
|
-- First admin must be self-granted and marked as owner
|
||||||
IF NOT EXISTS (
|
IF NEW.granted_by != NEW.user_id THEN
|
||||||
SELECT 1 FROM GroupAdmins
|
RAISE EXCEPTION 'First admin must be self-granted';
|
||||||
WHERE conversation_id = NEW.conversation_id
|
END IF;
|
||||||
AND user_id = NEW.granted_by
|
NEW.is_owner := true;
|
||||||
) THEN
|
ELSE
|
||||||
RAISE EXCEPTION 'Only existing admins can grant admin privileges';
|
-- For non-owner admins, verify that the granter is an admin
|
||||||
END IF;
|
IF NOT EXISTS (
|
||||||
ELSE
|
SELECT 1 FROM GroupAdmins
|
||||||
-- For the first admin, only allow self-grant
|
WHERE conversation_id = NEW.conversation_id
|
||||||
IF NEW.granted_by != NEW.user_id THEN
|
AND user_id = NEW.granted_by
|
||||||
RAISE EXCEPTION 'First admin must be self-granted';
|
) THEN
|
||||||
END IF;
|
RAISE EXCEPTION 'Only existing admins can grant admin privileges';
|
||||||
END IF;
|
END IF;
|
||||||
|
END IF;
|
||||||
RETURN NEW;
|
|
||||||
END;
|
RETURN NEW;
|
||||||
$$ LANGUAGE plpgsql;
|
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");
|
console.log("Successfully created GroupAdmins table with trigger");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to create tables: ", e);
|
console.error("Failed to create tables: ", e);
|
||||||
@@ -252,8 +251,8 @@ async function createGroup(user_id, groupName) {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const insertGroupAdminQuery = `
|
const insertGroupAdminQuery = `
|
||||||
INSERT INTO GroupAdmins (conversation_id, user_id, granted_by, granted_at)
|
INSERT INTO GroupAdmins (conversation_id, user_id, granted_by, is_owner)
|
||||||
VALUES ($1, $2, $3, NOW())
|
VALUES ($1, $2, $3, true)
|
||||||
RETURNING granted_at;
|
RETURNING granted_at;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -263,6 +262,7 @@ async function createGroup(user_id, groupName) {
|
|||||||
]);
|
]);
|
||||||
const group_id = createConversation.rows[0].group_id;
|
const group_id = createConversation.rows[0].group_id;
|
||||||
|
|
||||||
|
// Make the creator the owner and first admin
|
||||||
const insertGroupAdmin = await client.query(insertGroupAdminQuery, [
|
const insertGroupAdmin = await client.query(insertGroupAdminQuery, [
|
||||||
group_id,
|
group_id,
|
||||||
user_id,
|
user_id,
|
||||||
@@ -271,11 +271,6 @@ async function createGroup(user_id, groupName) {
|
|||||||
|
|
||||||
if (insertGroupAdmin.rowCount > 0) {
|
if (insertGroupAdmin.rowCount > 0) {
|
||||||
const contact_user_id = await addMemberToGroupById(group_id, user_id);
|
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);
|
insertContactById(user_id, group_id, true);
|
||||||
return { group_id, contact_user_id };
|
return { group_id, contact_user_id };
|
||||||
}
|
}
|
||||||
@@ -857,7 +852,8 @@ async function getMembers(conversation_id) {
|
|||||||
CASE
|
CASE
|
||||||
WHEN ga.user_id IS NOT NULL THEN TRUE
|
WHEN ga.user_id IS NOT NULL THEN TRUE
|
||||||
ELSE FALSE
|
ELSE FALSE
|
||||||
END AS isAdmin
|
END AS isAdmin,
|
||||||
|
COALESCE(ga.is_owner, FALSE) AS isOwner
|
||||||
FROM Memberships m
|
FROM Memberships m
|
||||||
JOIN Accounts a ON m.user_id = a.user_id
|
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
|
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;
|
WHERE conversation_id = $1 AND user_id = $2;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// const removeConversationContactQuery = `
|
|
||||||
// DELETE FROM Contacts
|
|
||||||
// WHERE conversation_id = $1 AND user_id = $2;
|
|
||||||
// `;
|
|
||||||
|
|
||||||
try {
|
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(
|
const removeMembershipResult = await client.query(
|
||||||
removeUserFromGroupQuery,
|
removeUserFromGroupQuery,
|
||||||
[conversation_id, user_id],
|
[conversation_id, user_id],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (removeMembershipResult.rowCount === 0) {
|
if (removeMembershipResult.rowCount === 0) {
|
||||||
console.log(
|
|
||||||
`No membership found for user_id: ${user_id} in conversation_id: ${conversation_id}`,
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
message: `No membership found for user_id: ${user_id} in conversation_id: ${conversation_id}`,
|
message: `No membership found for user_id: ${user_id} in conversation_id: ${conversation_id}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then, remove the user from the Contacts table
|
// Also remove from GroupAdmins if they were an admin
|
||||||
// const removeContactResult = await client.query(removeConversationContactQuery, [
|
await client.query(
|
||||||
// conversation_id,
|
`
|
||||||
// user_id,
|
DELETE FROM GroupAdmins
|
||||||
// ]);
|
WHERE conversation_id = $1 AND user_id = $2;
|
||||||
//
|
`,
|
||||||
// if (removeContactResult.rowCount === 0) {
|
[conversation_id, user_id],
|
||||||
// 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}`,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to remove user from group ", e);
|
console.error("Failed to remove user from group ", e);
|
||||||
return {
|
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 = {
|
module.exports = {
|
||||||
client,
|
client,
|
||||||
insertUser,
|
insertUser,
|
||||||
@@ -1017,4 +1110,6 @@ module.exports = {
|
|||||||
removeUserFromGroupById,
|
removeUserFromGroupById,
|
||||||
isConversationMember,
|
isConversationMember,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
|
addAdministrator,
|
||||||
|
removeAdministrator,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ const {
|
|||||||
deleteMessage,
|
deleteMessage,
|
||||||
removeUserFromGroupById,
|
removeUserFromGroupById,
|
||||||
isConversationMember,
|
isConversationMember,
|
||||||
|
isAdmin,
|
||||||
|
addAdministrator,
|
||||||
|
removeAdministrator,
|
||||||
} = require("../db/db");
|
} = require("../db/db");
|
||||||
const { isValidUsername } = require("../utils/filter");
|
const { isValidUsername } = require("../utils/filter");
|
||||||
const { verifyJwtToken } = require("../auth/jwt");
|
const { verifyJwtToken } = require("../auth/jwt");
|
||||||
const console = require("node:console");
|
|
||||||
const { call } = require("express");
|
|
||||||
|
|
||||||
function initializeSocket(io) {
|
function initializeSocket(io) {
|
||||||
io.use((socket, next) => {
|
io.use((socket, next) => {
|
||||||
@@ -226,11 +227,11 @@ function initializeSocket(io) {
|
|||||||
socket.on("remove user from group", async (msg, callback) => {
|
socket.on("remove user from group", async (msg, callback) => {
|
||||||
const { group_id, user_id } = msg;
|
const { group_id, user_id } = msg;
|
||||||
|
|
||||||
if (!group_id || !user_id) {
|
if (!group_id) {
|
||||||
return callback({
|
return callback({ status: "error", message: "No group id provided" });
|
||||||
status: "error",
|
}
|
||||||
message: "Missing required parameters",
|
if (!user_id) {
|
||||||
});
|
return callback({ status: "error", message: "No user id provided" });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -242,7 +243,7 @@ function initializeSocket(io) {
|
|||||||
|
|
||||||
// Get all sockets in the room
|
// Get all sockets in the room
|
||||||
const socketsInRoom = await io.in(group_id).fetchSockets();
|
const socketsInRoom = await io.in(group_id).fetchSockets();
|
||||||
|
// Remove user from room
|
||||||
for (const socketInstance of socketsInRoom) {
|
for (const socketInstance of socketsInRoom) {
|
||||||
if (socketInstance.user_id === user_id) {
|
if (socketInstance.user_id === user_id) {
|
||||||
socketInstance.leave(group_id);
|
socketInstance.leave(group_id);
|
||||||
@@ -253,7 +254,9 @@ function initializeSocket(io) {
|
|||||||
group_id,
|
group_id,
|
||||||
user_id,
|
user_id,
|
||||||
});
|
});
|
||||||
|
console.log(
|
||||||
|
`Successfully removed user: ${user_id}, from group: ${group_id}`,
|
||||||
|
);
|
||||||
return callback({
|
return callback({
|
||||||
status: "ok",
|
status: "ok",
|
||||||
message: "Successfully removed user from group",
|
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) => {
|
socket.on("disconnect", (reason) => {
|
||||||
console.log("(socket)", socket.id, " disconnected due to: ", reason);
|
console.log("(socket)", socket.id, " disconnected due to: ", reason);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user