mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-21 22:06:04 -05:00
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:
committed by
GitHub
parent
7fbe9282ba
commit
c1336c563d
5
.changeset/tidy-maps-cheer.md
Normal file
5
.changeset/tidy-maps-cheer.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"better-auth": patch
|
||||
---
|
||||
|
||||
Fix `organization.setActiveTeam` so it only accepts teams from the current active organization.
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user