fix(organization): scope setActiveTeam to active organization (#9239)

Signed-off-by: Gautam Manchandani <manchandanigautam@gmail.com>
Co-authored-by: Maxwell <145994855+ping-maxwell@users.noreply.github.com>
This commit is contained in:
Gautam Manchandani
2026-04-25 05:18:16 +05:30
committed by GitHub
parent 7fbe9282ba
commit c1336c563d
4 changed files with 280 additions and 5 deletions

View File

@@ -0,0 +1,5 @@
---
"better-auth": patch
---
Fix `organization.setActiveTeam` so it only accepts teams from the current active organization.

View File

@@ -1860,15 +1860,16 @@ Delete a team from an organization:
#### Set Active Team
Sets the given team as the current active team. If `teamId` is `null` the current active team is unset.
Sets the given team as the current active team for the current active organization. If `teamId` is `null` the current active team is unset.
<APIMethod path="/organization/set-active-team" method="POST" requireSession>
```ts
type setActiveTeam = {
/**
* The team ID of the team to set as the current active team.
* The team must belong to the current active organization.
*/
teamId?: string = "team-id"
teamId?: string | null = "team-id"
}
```
</APIMethod>

View File

@@ -679,7 +679,8 @@ export const setActiveTeam = <O extends OrganizationOptions>(options: O) =>
use: [orgSessionMiddleware, orgMiddleware],
metadata: {
openapi: {
description: "Set the active team",
description:
"Set the active team for the current active organization",
responses: {
"200": {
description: "Success",
@@ -734,7 +735,19 @@ export const setActiveTeam = <O extends OrganizationOptions>(options: O) =>
teamId = ctx.body.teamId;
}
const team = await adapter.findTeamById({ teamId });
const activeOrganizationId = session.session.activeOrganizationId;
if (!activeOrganizationId) {
throw APIError.from(
"BAD_REQUEST",
ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
);
}
const team = await adapter.findTeamById({
teamId,
organizationId: activeOrganizationId,
});
if (!team) {
throw APIError.from(

View File

@@ -1,8 +1,10 @@
import { describe, expect, it } from "vitest";
import { beforeAll, describe, expect, it } from "vitest";
import { createAuthClient } from "../../client";
import { setCookieToHeader } from "../../cookies";
import { getTestInstance } from "../../test-utils/test-instance";
import { isAPIError } from "../../utils/is-api-error";
import { organizationClient } from "./client";
import { ORGANIZATION_ERROR_CODES } from "./error-codes";
import { organization } from "./organization";
describe("team", async () => {
@@ -500,6 +502,260 @@ describe("team", async () => {
});
});
/**
* @see https://github.com/better-auth/better-auth/issues/9237
*/
describe("setActiveTeam org scoping", async () => {
const { auth, db, signInWithTestUser } = await getTestInstance({
plugins: [
organization({
async sendInvitationEmail() {},
teams: {
enabled: true,
},
}),
],
logger: {
level: "error",
},
});
const { headers } = await signInWithTestUser();
const client = createAuthClient({
plugins: [
organizationClient({
teams: {
enabled: true,
},
}),
],
baseURL: "http://localhost:3000/api/auth",
fetchOptions: {
customFetchImpl: async (url, init) => {
return auth.handler(new Request(url, init));
},
},
});
let activeOrganizationId: string;
let scopedTeamId: string;
let outOfScopeOrganizationId: string;
let outOfScopeTeamId: string;
let sessionToken: string;
let userId: string;
beforeAll(async () => {
const session = await client.getSession({
fetchOptions: { headers },
});
userId = session.data?.user.id as string;
sessionToken = session.data?.session.token as string;
const firstOrganization = await client.organization.create({
name: "Scoped Org One",
slug: "scoped-org-one",
fetchOptions: { headers },
});
activeOrganizationId = firstOrganization.data?.id as string;
expect(activeOrganizationId).toBeDefined();
const activeOrganizationTeam = await client.organization.createTeam(
{
name: "Scoped Team One",
organizationId: activeOrganizationId,
},
{
headers,
},
);
scopedTeamId = activeOrganizationTeam.data?.id as string;
expect(scopedTeamId).toBeDefined();
await auth.api.addTeamMember({
headers,
body: {
userId,
teamId: scopedTeamId,
organizationId: activeOrganizationId,
},
});
const secondOrganization = await client.organization.create({
name: "Scoped Org Two",
slug: "scoped-org-two",
keepCurrentActiveOrganization: true,
fetchOptions: { headers },
});
outOfScopeOrganizationId = secondOrganization.data?.id as string;
expect(outOfScopeOrganizationId).toBeDefined();
const outOfScopeTeam = await client.organization.createTeam(
{
name: "Scoped Team Two",
organizationId: outOfScopeOrganizationId,
},
{
headers,
},
);
outOfScopeTeamId = outOfScopeTeam.data?.id as string;
expect(outOfScopeTeamId).toBeDefined();
await auth.api.addTeamMember({
headers,
body: {
userId,
teamId: outOfScopeTeamId,
organizationId: outOfScopeOrganizationId,
},
});
const setActiveOrganization = await client.organization.setActive({
organizationId: activeOrganizationId,
fetchOptions: { headers },
});
expect(setActiveOrganization.error).toBeNull();
const setScopedTeam = await client.organization.setActiveTeam(
{
teamId: scopedTeamId,
},
{
headers,
},
);
expect(setScopedTeam.error).toBeNull();
expect(setScopedTeam.data?.id).toBe(scopedTeamId);
const sessionAfterScopedTeam = await client.getSession({
fetchOptions: { headers },
});
expect(
(sessionAfterScopedTeam.data?.session as any).activeOrganizationId,
).toBe(activeOrganizationId);
expect((sessionAfterScopedTeam.data?.session as any).activeTeamId).toBe(
scopedTeamId,
);
});
it("should reject teams outside the active organization and preserve the active team", async () => {
const setOutOfScopeTeam = await client.organization.setActiveTeam(
{
teamId: outOfScopeTeamId,
},
{
headers,
},
);
expect(setOutOfScopeTeam.error?.status).toBe(400);
expect(setOutOfScopeTeam.error?.code).toContain(
ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND.code,
);
expect(setOutOfScopeTeam.data).toBeNull();
const sessionAfterRejectedTeam = await client.getSession({
fetchOptions: { headers },
});
expect(
(sessionAfterRejectedTeam.data?.session as any).activeOrganizationId,
).toBe(activeOrganizationId);
expect((sessionAfterRejectedTeam.data?.session as any).activeTeamId).toBe(
scopedTeamId,
);
});
it("should reject refreshing the current active team when the session org changes externally", async () => {
await db.update({
model: "session",
where: [
{
field: "token",
value: sessionToken,
},
],
update: {
activeOrganizationId: outOfScopeOrganizationId,
},
});
let error: unknown = null;
await auth.api
.setActiveTeam({
body: {},
headers,
})
.catch((e) => {
error = e;
});
expect(error).not.toBeNull();
expect(isAPIError(error)).toBeTruthy();
if (!isAPIError(error)) {
throw new Error("Expected setActiveTeam to throw an APIError");
}
expect(error.message).toBe(ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND.message);
const sessionAfterRefreshAttempt = await client.getSession({
fetchOptions: { headers },
});
expect(
(sessionAfterRefreshAttempt.data?.session as any).activeOrganizationId,
).toBe(outOfScopeOrganizationId);
expect((sessionAfterRefreshAttempt.data?.session as any).activeTeamId).toBe(
scopedTeamId,
);
await db.update({
model: "session",
where: [
{
field: "token",
value: sessionToken,
},
],
update: {
activeOrganizationId: activeOrganizationId,
},
});
});
it("should require an active organization before setting the active team", async () => {
expect(activeOrganizationId).toBeDefined();
expect(scopedTeamId).toBeDefined();
expect(userId).toBeDefined();
const unsetActiveOrganization = await client.organization.setActive({
organizationId: null,
fetchOptions: { headers },
});
expect(unsetActiveOrganization.error).toBeNull();
const result = await client.organization.setActiveTeam(
{
teamId: scopedTeamId,
},
{
headers,
},
);
expect(result.error?.status).toBe(400);
expect(result.error?.code).toContain(
ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION.code,
);
expect(result.data).toBeNull();
const sessionAfterMissingOrganization = await client.getSession({
fetchOptions: { headers },
});
expect(
(sessionAfterMissingOrganization.data?.session as any)
.activeOrganizationId,
).toBeNull();
});
});
describe("multi team support", async () => {
const { auth, signInWithTestUser } = await getTestInstance(
{