@@ -92,24 +92,38 @@ export async function removeTeamMember({
9292 throw new ServiceValidationError ( "User does not have access to this organization" , 403 ) ;
9393 }
9494
95- // Scope the target to this org. A member id is a globally unique key, so
96- // deleting by id alone would remove members of other orgs; bind it to the
97- // resolved org and reject a foreign id.
98- const member = await prisma . orgMember . findFirst ( {
99- where : { id : memberId , organizationId : org . id } ,
100- include : {
101- organization : true ,
102- user : true ,
103- } ,
104- } ) ;
95+ // Serializable: the "keep at least one member" check and the delete must not
96+ // interleave with a concurrent removal, or two requests could each pass the
97+ // count and leave the org with zero members. Guard lives here (not per-caller)
98+ // so the dashboard and the management API both get it.
99+ return prisma . $transaction (
100+ async ( tx ) => {
101+ // Scope the target to this org. A member id is a globally unique key, so
102+ // deleting by id alone would remove members of other orgs; bind it to the
103+ // resolved org and reject a foreign id.
104+ const member = await tx . orgMember . findFirst ( {
105+ where : { id : memberId , organizationId : org . id } ,
106+ include : {
107+ organization : true ,
108+ user : true ,
109+ } ,
110+ } ) ;
105111
106- if ( ! member ) {
107- throw new ServiceValidationError ( "Member not found in this organization" , 404 ) ;
108- }
112+ if ( ! member ) {
113+ throw new ServiceValidationError ( "Member not found in this organization" , 404 ) ;
114+ }
109115
110- await prisma . orgMember . delete ( { where : { id : member . id } } ) ;
116+ const memberCount = await tx . orgMember . count ( { where : { organizationId : org . id } } ) ;
117+ if ( memberCount <= 1 ) {
118+ throw new ServiceValidationError ( "Cannot remove the last member of an organization" , 400 ) ;
119+ }
111120
112- return member ;
121+ await tx . orgMember . delete ( { where : { id : member . id } } ) ;
122+
123+ return member ;
124+ } ,
125+ { isolationLevel : PrismaNamespace . TransactionIsolationLevel . Serializable }
126+ ) ;
113127}
114128
115129export async function inviteMembers ( {
0 commit comments