mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-27 01:16:55 -05:00
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:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -83,7 +83,7 @@ export interface OrganizationOptions {
|
||||
/**
|
||||
* The number of memberships a user can have in an organization.
|
||||
*
|
||||
* @default "unlimited"
|
||||
* @default 100
|
||||
*/
|
||||
membershipLimit?: number;
|
||||
/**
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user