From 62780c88897b19930046e444db1908851ac3f4c0 Mon Sep 17 00:00:00 2001 From: Maulana Sodiqin Date: Wed, 26 Nov 2025 15:36:35 +0700 Subject: [PATCH] fix(hackathon): fix team leader access denied and member removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add loading state check before leader validation to prevent race condition - Fix useRemoveMember hook to use Supabase directly instead of missing API endpoint - Leader check now waits for team data to load before showing Access Denied 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/app/teams/[teamId]/members/page.tsx | 11 ++++- libs/service/src/hooks/teams/index.ts | 41 ++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/apps/hackathon/src/app/teams/[teamId]/members/page.tsx b/apps/hackathon/src/app/teams/[teamId]/members/page.tsx index 4987873..45d2ec9 100644 --- a/apps/hackathon/src/app/teams/[teamId]/members/page.tsx +++ b/apps/hackathon/src/app/teams/[teamId]/members/page.tsx @@ -23,7 +23,7 @@ const ManageMembersPage: FC = (): ReactElement => { const { session } = useAuthStore(); const [showInviteModal, setShowInviteModal] = useState(false); - const { data: teamData } = useTeamById(teamId || ''); + const { data: teamData, isLoading: isLoadingTeam } = useTeamById(teamId || ''); const { data: membersData, isLoading: isLoadingMembers } = useTeamMembers(teamId || ''); const { data: joinRequestsData } = useTeamJoinRequests(teamId || ''); @@ -42,6 +42,15 @@ const ManageMembersPage: FC = (): ReactElement => { mode: 'all', }); + // Show loading state while fetching team data + if (isLoadingTeam) { + return ( +
+

Loading...

+
+ ); + } + if (!isLeader) { return (
diff --git a/libs/service/src/hooks/teams/index.ts b/libs/service/src/hooks/teams/index.ts index 32b014e..b6a5e7c 100644 --- a/libs/service/src/hooks/teams/index.ts +++ b/libs/service/src/hooks/teams/index.ts @@ -312,9 +312,48 @@ export const useManageMember = (teamId: string) => { export const useRemoveMember = (teamId: string) => { const queryClient = useQueryClient(); + const { session } = useAuthStore(); return useMutation({ - mutationFn: (userId: string) => teamsApi.removeMember(teamId, userId), + mutationFn: async (userId: string) => { + if (!session?.user?.id) { + throw new Error('You must be logged in to remove a member'); + } + + // Verify the current user is the team leader + const { data: team, error: teamError } = await supabase + .from('teams') + .select('leader_id') + .eq('id', teamId) + .single(); + + if (teamError || !team) { + throw new Error('Team not found'); + } + + if (team.leader_id !== session.user.id) { + throw new Error('Only the team leader can remove members'); + } + + // Cannot remove the leader + if (userId === team.leader_id) { + throw new Error('Cannot remove the team leader'); + } + + // Delete the team member record + const { error: deleteError } = await supabase + .from('team_members') + .delete() + .eq('team_id', teamId) + .eq('user_id', userId); + + if (deleteError) { + console.error('Failed to remove member:', deleteError); + throw new Error(deleteError.message || 'Failed to remove member'); + } + + return { success: true }; + }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: teamKeys.members(teamId) }); queryClient.invalidateQueries({ queryKey: teamKeys.detail(teamId) });