From 8f47015af223e5db9ff406f6ca8247f6b8dcd9fe Mon Sep 17 00:00:00 2001 From: Yanqi Zong Date: Fri, 13 Mar 2026 16:18:14 -0700 Subject: [PATCH] feat(organization): explicit `organizationId` in team endpoints (#5062) Co-authored-by: Alex Yang --- .../plugins/organization/routes/crud-team.ts | 50 ++++-- .../src/plugins/organization/team.test.ts | 152 ++++++++++++++++++ 2 files changed, 186 insertions(+), 16 deletions(-) diff --git a/packages/better-auth/src/plugins/organization/routes/crud-team.ts b/packages/better-auth/src/plugins/organization/routes/crud-team.ts index 81e8cf1237..d8aa76e9e6 100644 --- a/packages/better-auth/src/plugins/organization/routes/crud-team.ts +++ b/packages/better-auth/src/plugins/organization/routes/crud-team.ts @@ -912,6 +912,14 @@ const addTeamMemberBodySchema = z.object({ description: "The user Id which represents the user to be added as a member.", }), + + organizationId: z + .string() + .meta({ + description: + "The organization ID which the team falls under. If not provided, it will default to the user's active organization.", + }) + .optional(), }); export const addTeamMember = (options: O) => @@ -967,7 +975,10 @@ export const addTeamMember = (options: O) => const session = ctx.context.session; const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions); - if (!session.session.activeOrganizationId) { + const organizationId = + ctx.body.organizationId || session.session.activeOrganizationId; + + if (!organizationId) { throw APIError.from( "BAD_REQUEST", ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION, @@ -976,7 +987,7 @@ export const addTeamMember = (options: O) => const currentMember = await adapter.findMemberByOrgId({ userId: session.user.id, - organizationId: session.session.activeOrganizationId, + organizationId: organizationId, }); if (!currentMember) { @@ -993,7 +1004,7 @@ export const addTeamMember = (options: O) => permissions: { member: ["update"], }, - organizationId: session.session.activeOrganizationId, + organizationId: organizationId, }, ctx, ); @@ -1007,7 +1018,7 @@ export const addTeamMember = (options: O) => const toBeAddedMember = await adapter.findMemberByOrgId({ userId: ctx.body.userId, - organizationId: session.session.activeOrganizationId, + organizationId: organizationId, }); if (!toBeAddedMember) { @@ -1019,7 +1030,7 @@ export const addTeamMember = (options: O) => const team = await adapter.findTeamById({ teamId: ctx.body.teamId, - organizationId: session.session.activeOrganizationId, + organizationId: organizationId, }); if (!team) { @@ -1029,9 +1040,7 @@ export const addTeamMember = (options: O) => ); } - const organization = await adapter.findOrganizationById( - session.session.activeOrganizationId, - ); + const organization = await adapter.findOrganizationById(organizationId); if (!organization) { throw APIError.from( "BAD_REQUEST", @@ -1091,6 +1100,14 @@ const removeTeamMemberBodySchema = z.object({ userId: z.coerce.string().meta({ description: "The user which should be removed from the team.", }), + + organizationId: z + .string() + .meta({ + description: + "The organization ID which the team falls under. If not provided, it will default to the user's active organization.", + }) + .optional(), }); export const removeTeamMember = (options: O) => @@ -1132,7 +1149,10 @@ export const removeTeamMember = (options: O) => const session = ctx.context.session; const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions); - if (!session.session.activeOrganizationId) { + const organizationId = + ctx.body.organizationId || session.session.activeOrganizationId; + + if (!organizationId) { throw APIError.from( "BAD_REQUEST", ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION, @@ -1141,7 +1161,7 @@ export const removeTeamMember = (options: O) => const currentMember = await adapter.findMemberByOrgId({ userId: session.user.id, - organizationId: session.session.activeOrganizationId, + organizationId: organizationId, }); if (!currentMember) { @@ -1158,7 +1178,7 @@ export const removeTeamMember = (options: O) => permissions: { member: ["delete"], }, - organizationId: session.session.activeOrganizationId, + organizationId: organizationId, }, ctx, ); @@ -1172,7 +1192,7 @@ export const removeTeamMember = (options: O) => const toBeAddedMember = await adapter.findMemberByOrgId({ userId: ctx.body.userId, - organizationId: session.session.activeOrganizationId, + organizationId: organizationId, }); if (!toBeAddedMember) { @@ -1184,7 +1204,7 @@ export const removeTeamMember = (options: O) => const team = await adapter.findTeamById({ teamId: ctx.body.teamId, - organizationId: session.session.activeOrganizationId, + organizationId: organizationId, }); if (!team) { @@ -1194,9 +1214,7 @@ export const removeTeamMember = (options: O) => ); } - const organization = await adapter.findOrganizationById( - session.session.activeOrganizationId, - ); + const organization = await adapter.findOrganizationById(organizationId); if (!organization) { throw APIError.from( "BAD_REQUEST", diff --git a/packages/better-auth/src/plugins/organization/team.test.ts b/packages/better-auth/src/plugins/organization/team.test.ts index e5b14ffcb7..18c9d8af3a 100644 --- a/packages/better-auth/src/plugins/organization/team.test.ts +++ b/packages/better-auth/src/plugins/organization/team.test.ts @@ -248,6 +248,158 @@ describe("team", async () => { } }); + it("should update a team with explicit organizationId in data", async () => { + // Get current session to ensure new org is different from active org + const currentSession = await client.getSession({ + fetchOptions: { headers }, + }); + const currentActiveOrgId = (currentSession.data?.session as any) + .activeOrganizationId; + + // Create a new organization + const newOrg = await client.organization.create({ + name: "New Test Organization", + slug: "new-test-org", + fetchOptions: { headers }, + }); + + expect(newOrg.data?.id).toBeDefined(); + const newOrgId = newOrg.data?.id as string; + + // Ensure the new org is different from the session's active org + expect(newOrgId).not.toBe(currentActiveOrgId); + + // Create a team in the new organization + const newTeam = await client.organization.createTeam( + { + name: "Team in New Org", + organizationId: newOrgId, + }, + { + headers, + }, + ); + + expect(newTeam.data?.id).toBeDefined(); + expect(newTeam.data?.organizationId).toBe(newOrgId); + const newTeamId = newTeam.data?.id as string; + + // Update the team with explicit organizationId in data + const updatedTeam = await client.organization.updateTeam({ + teamId: newTeamId, + data: { + name: "Updated Team Name", + organizationId: newOrgId, + }, + fetchOptions: { headers }, + }); + + expect(updatedTeam.data?.name).toBe("Updated Team Name"); + expect(updatedTeam.data?.id).toBe(newTeamId); + expect(updatedTeam.data?.organizationId).toBe(newOrgId); + }); + + it("should add and remove team member with explicit organizationId", async () => { + const testOrgId = organizationId; + + // Create a new team specifically for this test + const newTeamRes = await client.organization.createTeam( + { + name: "Team for Member Test", + organizationId: testOrgId, + }, + { + headers, + }, + ); + expect(newTeamRes.data?.id).toBeDefined(); + const testTeamId = newTeamRes.data?.id as string; + + // Get the current user's ID + const currentSession = await client.getSession({ + fetchOptions: { headers }, + }); + const currentUserId = currentSession.data?.user.id as string; + + // Add current user to the team so they can list members + await auth.api.addTeamMember({ + headers, + body: { + userId: currentUserId, + teamId: testTeamId, + organizationId: testOrgId, + }, + }); + + // Create and add a new user + const newUserHeaders = new Headers(); + const newUserRes = await client.signUp.email( + { + email: "teamuser@email.com", + password: "password", + name: "Team User", + }, + { + onSuccess: cookieSetter(newUserHeaders), + }, + ); + + expect(newUserRes.data?.user.id).toBeDefined(); + const newUserId = newUserRes.data?.user.id as string; + + // Add user to organization first + await auth.api.addMember({ + body: { + organizationId: testOrgId, + userId: newUserId, + role: "member", + }, + }); + + // Add team member with explicit organizationId + const addedMember = await auth.api.addTeamMember({ + headers, + body: { + userId: newUserId, + teamId: testTeamId, + organizationId: testOrgId, + }, + }); + + expect(addedMember.teamId).toBe(testTeamId); + expect(addedMember.userId).toBe(newUserId); + + // Verify team member was added by listing team members + const teamMembersBefore = await auth.api.listTeamMembers({ + headers, + query: { + teamId: testTeamId, + }, + }); + expect(teamMembersBefore.some((m) => m.userId === newUserId)).toBe(true); + + // Remove team member with explicit organizationId + const removeRes = await auth.api.removeTeamMember({ + headers, + body: { + userId: newUserId, + teamId: testTeamId, + organizationId: testOrgId, + }, + }); + + expect(removeRes.message).toBe("Team member removed successfully."); + + // Verify team member was removed by listing team members again + const teamMembersAfter = await auth.api.listTeamMembers({ + headers, + query: { + teamId: testTeamId, + }, + }); + expect(teamMembersAfter.some((m) => m.userId === newUserId)).toBe(false); + }); + it("should not be allowed to invite a member to a team that's reached maximum members", async () => { const { auth, signInWithTestUser } = await getTestInstance({ user: {