fix(organization): respect membershipLimit (#1600)

* added membershipLimit

* fix: failing test and revert unlimited

* feat: add default membership limit of 100 to organization plugin

---------

Co-authored-by: Bereket Engida <bekacru@gmail.com>
This commit is contained in:
Igor Pawelec
2025-02-28 23:23:06 +01:00
committed by GitHub
parent 53d090af36
commit bdf69994d1
6 changed files with 143 additions and 1 deletions

View File

@@ -129,6 +129,7 @@ export const getOrgAdapter = (
value: data.organizationId,
},
],
limit: options?.membershipLimit || 100,
});
return members;
},
@@ -356,6 +357,7 @@ export const getOrgAdapter = (
adapter.findMany<Member>({
model: "member",
where: [{ field: "organizationId", value: org.id }],
limit: options?.membershipLimit || 100,
}),
includeTeams
? adapter.findMany<Team>({
@@ -408,6 +410,7 @@ export const getOrgAdapter = (
value: userId,
},
],
limit: options?.membershipLimit || 100,
});
if (!members || members.length === 0) {

View File

@@ -43,4 +43,6 @@ export const ORGANIZATION_ERROR_CODES = {
UNABLE_TO_REMOVE_LAST_TEAM: "Unable to remove last team",
YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER:
"You are not allowed to update this member",
ORGANIZATION_MEMBERSHIP_LIMIT_REACHED:
"Organization membership limit reached",
} as const;

View File

@@ -6,6 +6,7 @@ import { organizationClient } from "./client";
import { createAccessControl } from "../access";
import { ORGANIZATION_ERROR_CODES } from "./error-codes";
import { BetterAuthError } from "../../error";
import { APIError } from "better-call";
describe("organization", async (it) => {
const { auth, signInWithTestUser, signInWithUser, cookieSetter } =
@@ -15,6 +16,7 @@ describe("organization", async (it) => {
},
plugins: [
organization({
membershipLimit: 6,
async sendInvitationEmail(data, request) {},
schema: {
organization: {
@@ -525,6 +527,120 @@ describe("organization", async (it) => {
});
expect(member?.role).toBe("admin");
});
it("should respect membershipLimit when adding members to organization", async () => {
const org = await auth.api.createOrganization({
body: {
name: "test-5-membership-limit",
slug: "test-5-membership-limit",
},
headers,
});
const users = [
"user1@emial.com",
"user2@email.com",
"user3@email.com",
"user4@email.com",
];
for (const user of users) {
const newUser = await auth.api.signUpEmail({
body: {
email: user,
password: "password",
name: user,
},
});
const session = await auth.api.getSession({
headers: new Headers({
Authorization: `Bearer ${newUser?.token}`,
}),
});
await auth.api.addMember({
body: {
organizationId: org?.id,
userId: session?.user.id!,
role: "admin",
},
});
}
const userOverLimit = {
email: "shouldthrowerror@email.com",
password: "password",
name: "name",
};
// test api method
const newUser = await auth.api.signUpEmail({
body: {
email: userOverLimit.email,
password: userOverLimit.password,
name: userOverLimit.name,
},
});
const session = await auth.api.getSession({
headers: new Headers({
Authorization: `Bearer ${newUser?.token}`,
}),
});
await auth.api
.addMember({
body: {
organizationId: org?.id,
userId: session?.user.id!,
role: "admin",
},
})
.catch((e: APIError) => {
expect(e).not.toBeNull();
expect(e).toBeInstanceOf(APIError);
expect(e.message).toBe(
ORGANIZATION_ERROR_CODES.ORGANIZATION_MEMBERSHIP_LIMIT_REACHED,
);
});
// test client method
const invite = await client.organization.inviteMember({
organizationId: org?.id,
email: userOverLimit.email,
role: "member",
fetchOptions: {
headers,
},
});
if (!invite.data) throw new Error("Invitation not created");
await client.signUp.email({
email: userOverLimit.email,
password: userOverLimit.password,
name: userOverLimit.name,
});
const { res, headers: headers2 } = await signInWithUser(
userOverLimit.email,
userOverLimit.password,
);
const invitation = await client.organization.acceptInvitation({
invitationId: invite.data.id,
fetchOptions: {
headers: headers2,
},
});
expect(invitation.error?.message).toBe(
ORGANIZATION_ERROR_CODES.ORGANIZATION_MEMBERSHIP_LIMIT_REACHED,
);
const getFullOrganization = await client.organization.getFullOrganization({
query: {
organizationId: org?.id,
},
fetchOptions: {
headers,
},
});
expect(getFullOrganization.data?.members.length).toBe(6);
});
});
describe("access control", async (it) => {

View File

@@ -83,7 +83,7 @@ export interface OrganizationOptions {
/**
* The number of memberships a user can have in an organization.
*
* @default "unlimited"
* @default 100
*/
membershipLimit?: number;
/**

View File

@@ -290,6 +290,16 @@ export const acceptInvitation = createAuthEndpoint(
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION,
});
}
const membershipLimit = ctx.context.orgOptions?.membershipLimit || 100;
const members = await adapter.listMembers({
organizationId: invitation.organizationId,
});
if (members.length >= membershipLimit) {
throw new APIError("FORBIDDEN", {
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_MEMBERSHIP_LIMIT_REACHED,
});
}
const acceptedI = await adapter.updateInvitation({
invitationId: ctx.body.invitationId,
status: "accepted",

View File

@@ -64,6 +64,7 @@ export const addMember = <O extends OrganizationOptions>() =>
email: user.email,
organizationId: orgId,
});
if (alreadyMember) {
throw new APIError("BAD_REQUEST", {
message:
@@ -71,6 +72,16 @@ export const addMember = <O extends OrganizationOptions>() =>
});
}
const membershipLimit = ctx.context.orgOptions?.membershipLimit || 100;
const members = await adapter.listMembers({ organizationId: orgId });
if (members.length >= membershipLimit) {
throw new APIError("FORBIDDEN", {
message:
ORGANIZATION_ERROR_CODES.ORGANIZATION_MEMBERSHIP_LIMIT_REACHED,
});
}
const createdMember = await adapter.createMember({
id: generateId(),
organizationId: orgId,