Skip to content
Merged
7 changes: 7 additions & 0 deletions backend/src/entities/GroupMember.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,23 @@ export class GroupMember extends BaseEntity {
@Field()
lastTempstampVu: Date;

@Field(() => User)
@ManyToOne(
() => User,
(user) => user.groupMember,
{
onDelete: "CASCADE",
},
)
@JoinColumn({ name: "userId" })
user: User;

@ManyToOne(
() => Group,
(group) => group.groupMember,
{
onDelete: "CASCADE",
},
)
@JoinColumn({ name: "groupId" })
group: Group;
Expand Down
3 changes: 3 additions & 0 deletions backend/src/entities/Message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ export class Message extends BaseEntity {
@ManyToOne(
() => Group,
(group) => group.messages,
{
onDelete: "CASCADE",
},
)
@Field(() => Group)
group: Group;
Expand Down
202 changes: 201 additions & 1 deletion backend/src/resolvers/GroupResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import {
Root,
UseMiddleware,
} from "type-graphql";

import Group from "../entities/Group";
import { GroupMember } from "../entities/GroupMember";
import { Message } from "../entities/Message";
import User from "../entities/User";
import { getVariableEnv } from "../lib/envManager/envManager";
import { RoleMiddleware } from "../middleware/RoleMiddleware";
import { addMembersToGroup, removeMembersFromGroup } from "../services/groupMemberService";
import type { ContextType } from "../types/context";

@InputType()
Expand Down Expand Up @@ -49,6 +51,36 @@ class AddFundsInput {
amount!: number;
}

@InputType()
class UpdateGroupInput {
@Field()
name!: string;

@Field()
event_type!: string;

@Field()
piggy_bank!: number;

@Field()
deadline!: Date;

@Field(() => [String], { nullable: true })
users?: string[];

@Field({ nullable: true })
user_beneficiary?: string;
}

@InputType()
class RemoveMembersInput {
@Field(() => [Number], { nullable: true })
userIds?: number[];

@Field(() => [String], { nullable: true })
userEmails?: string[];
}

@ObjectType()
export class MyGroupsResponse {
@Field(() => [Group])
Expand All @@ -72,7 +104,13 @@ export default class GroupResolver {
user: { id: ctx.user?.id },
},
},
relations: { groupMember: true, user_admin: true, user_beneficiary: true },
relations: {
groupMember: {
user: true, // 👈 THIS is the key
},
user_admin: true,
user_beneficiary: true,
},
order: { id: "DESC" },
});

Expand All @@ -83,10 +121,27 @@ export default class GroupResolver {
return { groups, groupToken };
}

@Query(() => Group)
async getGroupById(@Arg("id") id: number) {
const group = await Group.findOne({
where: { id: id },
relations: {
user_admin: true,
user_beneficiary: true,
groupMember: {
user: true,
},
},
});
if (!group) throw new Error("Groupe non trouvé");
return group;
}

@FieldResolver(() => [GroupMember])
async groupMember(@Root() group: Group) {
const groupMembers = await GroupMember.find({
where: { groupId: group.id },
relations: { user: true },
});

return groupMembers || []; // >>> not null
Expand Down Expand Up @@ -195,4 +250,149 @@ export default class GroupResolver {

return group;
}

@UseMiddleware(RoleMiddleware())
@Mutation(() => Group)
async updateGroup(@Arg("id") id: number, @Arg("data") data: UpdateGroupInput) {
const group = await Group.findOne({ where: { id: id } });
if (!group) throw new Error("Groupe non trouvé");
group.name = data.name;
group.event_type = data.event_type;
group.piggy_bank = data.piggy_bank;
group.deadline = data.deadline;
await group.save();

if (data.users?.length) {
await addMembersToGroup({
userEmails: data.users,
groupId: group.id,
});
}

return group;
}

@UseMiddleware(RoleMiddleware())
@Mutation(() => String)
async deleteGroup(@Arg("id") id: number, @Ctx() ctx: ContextType): Promise<string> {
if (!ctx.user) {
throw new Error("Utilisateur non connecté");
}

const group = await Group.findOne({
where: {
id,
user_admin: { id: ctx.user.id },
},
relations: { user_admin: true },
});

if (!group) {
throw new Error("Groupe introuvable ou accès refusé");
}

if (!group.user_admin || group.user_admin.id !== ctx.user.id) {
throw new Error("Il faut être administrateur du groupe pour pouvoir le supprimer");
}

try {
await Group.remove(group);
return "Le groupe a été supprimé";
} catch (err) {
console.error("deleteGroup error:", err);
throw new Error("Une erreur est survenue lors de la suppression du groupe");
}
}

@UseMiddleware(RoleMiddleware())
@Mutation(() => String)
async removeMembersFromGroup(
@Arg("groupId", () => Number) groupId: number,
@Arg("data") data: RemoveMembersInput,
@Ctx() ctx: ContextType,
): Promise<string> {
if (!ctx.user) {
throw new Error("Utilisateur non connecté");
}

// Ensure at least one input is provided
if ((!data.userIds || data.userIds.length === 0) && (!data.userEmails || data.userEmails.length === 0)) {
throw new Error("Vous devez fournir au moins un identifiant utilisateur ou un email");
}

const group = await Group.findOne({
where: {
id: groupId,
},
relations: { user_admin: true },
});

if (!group) {
throw new Error("Groupe introuvable ou accès refusé");
}

const currentUserId = ctx.user.id;
const isAdmin = ctx.user.id === group.user_admin.id;

// Convert emails to user IDs if provided
let userIdsToRemove: number[] = [];

if (data.userEmails && data.userEmails.length > 0) {
await Promise.all(
data.userEmails.map(async (email) => {
const user = await User.findOne({ where: { email } });
if (user) {
userIdsToRemove.push(user.id);
}
}),
);
}

// Add direct user IDs if provided
if (data.userIds && data.userIds.length > 0) {
userIdsToRemove.push(...data.userIds);
}

// Remove duplicates
userIdsToRemove = [...new Set(userIdsToRemove)];

if (userIdsToRemove.length === 0) {
throw new Error("Aucun utilisateur valide trouvé");
}

// Admin cannot remove itself from a group
if (isAdmin && userIdsToRemove.includes(group.user_admin.id)) {
throw new Error("L'administrateur du groupe ne peut pas être supprimé. Supprimez plutôt le groupe");
}

// Remove myself from a group
if (!isAdmin && userIdsToRemove.length === 1 && userIdsToRemove[0] === currentUserId) {
try {
await removeMembersFromGroup({
userIds: [currentUserId],
groupId,
});
return "Succès! Vous ne faites plus partie du groupe!";
} catch (err) {
console.error("removeMembersFromGroup error:", err);
throw new Error("Une erreur est survenue, nous n'avons pas pu vous supprimer du groupe");
}
}

// Admin removes users from a group
if (isAdmin && userIdsToRemove.length > 0 && !userIdsToRemove.includes(group.user_admin.id)) {
try {
await removeMembersFromGroup({
userIds: userIdsToRemove,
groupId,
});
return "Succès! Les utilisateurs ont été supprimés du groupe!";
} catch (err) {
console.error("removeMembersFromGroup error:", err);
throw new Error("Une erreur est survenue, nous n'avons pas pu supprimer les utilisateurs du groupe");
}
}

throw new Error("Vous n'avez pas les permissions nécessaires pour effectuer cette action");
}
}
2 changes: 1 addition & 1 deletion backend/src/resolvers/MessageResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export default class MessageResolver {

if (!userId) throw new Error("Utilisateur non authentifié");

const group = await GroupMember.findOne({ where: { user: { id: userId }, group: { id: groupId } } });
const group = await GroupMember.findOne({ where: { userId: userId, groupId: groupId } });
// verifier que l'utilisateur fait bien partie du groupe
if (!group) throw new Error("Groupe non trouvé");

Expand Down
32 changes: 30 additions & 2 deletions backend/src/services/groupMemberService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ type AddMembersInput = {
groupId: number;
};

type RemoveMembersInput = {
userIds: number[];
groupId: number;
};

/**
* Add users to a group from a list of emails.
* - Silently skips emails that do not match a user.
Expand All @@ -17,7 +22,8 @@ export async function addMembersToGroup({ userEmails, groupId }: AddMembersInput
await Promise.all(
userEmails.map(async (userEmail) => {
const userToAdd = await User.findOne({ where: { email: userEmail } });
if (!userToAdd) {
const userPending = await PendingInvitation.findOne({ where: { userEmail: userEmail } });
if (!userToAdd && !userPending) {
// if the user does not exist in the db, add it to the pendinginvitationList
const pendingInvitation = PendingInvitation.create({
userEmail: userEmail,
Expand All @@ -29,11 +35,33 @@ export async function addMembersToGroup({ userEmails, groupId }: AddMembersInput
}

const groupMember = GroupMember.create({
userId: userToAdd.id,
userId: userToAdd?.id,
groupId,
});

await groupMember.save();
}),
);
}

export async function removeMembersFromGroup({ userIds, groupId }: RemoveMembersInput) {
if (!userIds || userIds.length === 0) return;

// Remove all group members
await Promise.all(
userIds.map(async (userId) => {
const groupMember = await GroupMember.findOne({
where: { userId: userId, groupId: groupId },
});

if (!groupMember) return;

try {
await GroupMember.remove(groupMember);
} catch (err) {
console.error("removeMembersFromGroup error:", err);
throw new Error("Une erreur est survenue lors de la suppression de l'utilisateur du groupe");
}
}),
);
}
Loading
Loading