feat(organization): leave organization (#1239)

This commit is contained in:
Bereket Engida
2025-02-14 13:12:15 +03:00
committed by GitHub
parent b8d20c5e7b
commit b26f8334b3
5 changed files with 118 additions and 21 deletions

View File

@@ -46,3 +46,5 @@ export const {
useListOrganizations, useListOrganizations,
useActiveOrganization, useActiveOrganization,
} = client; } = client;
client.$store.listen("$sessionSignal", async () => {});

View File

@@ -533,6 +533,16 @@ auth.api.addMember({
}) })
``` ```
### Leave Organization
To leave organization you can use `organization.leave` function. This function will remove the current user from the organization.
```ts title="auth-client.ts"
await authClient.organization.leave({
organizationId: "organization-id"
})
```
## Access Control ## Access Control
The organization plugin providers a very flexible access control system. You can control the access of the user based on the role they have in the organization. You can define your own set of permissions based on the role of the user. The organization plugin providers a very flexible access control system. You can control the access of the user based on the role they have in the organization. You can define your own set of permissions based on the role of the user.

View File

@@ -8,7 +8,8 @@ import { ORGANIZATION_ERROR_CODES } from "./error-codes";
import { BetterAuthError } from "../../error"; import { BetterAuthError } from "../../error";
describe("organization", async (it) => { describe("organization", async (it) => {
const { auth, signInWithTestUser, signInWithUser } = await getTestInstance({ const { auth, signInWithTestUser, signInWithUser, cookieSetter } =
await getTestInstance({
user: { user: {
modelName: "users", modelName: "users",
}, },
@@ -312,6 +313,36 @@ describe("organization", async (it) => {
); );
}); });
it("should allow leaving organization", async () => {
const newUser = {
email: "leave@org.com",
name: "leaving member",
password: "password",
};
const headers = new Headers();
const res = await client.signUp.email(newUser, {
onSuccess: cookieSetter(headers),
});
const member = await auth.api.addMember({
body: {
organizationId,
userId: res.data?.user.id!,
role: "admin",
},
});
const leaveRes = await client.organization.leave(
{
organizationId,
},
{
headers,
},
);
expect(leaveRes.data).toMatchObject({
userId: res.data?.user.id!,
});
});
it("should allow removing member from organization", async () => { it("should allow removing member from organization", async () => {
const { headers } = await signInWithTestUser(); const { headers } = await signInWithTestUser();
const orgBefore = await client.organization.getFullOrganization({ const orgBefore = await client.organization.getFullOrganization({

View File

@@ -31,6 +31,7 @@ import {
import { import {
addMember, addMember,
getActiveMember, getActiveMember,
leaveOrganization,
removeMember, removeMember,
updateMemberRole, updateMemberRole,
} from "./routes/crud-members"; } from "./routes/crud-members";
@@ -253,6 +254,7 @@ export const organization = <O extends OrganizationOptions>(options?: O) => {
removeMember, removeMember,
updateMemberRole: updateMemberRole(options as O), updateMemberRole: updateMemberRole(options as O),
getActiveMember, getActiveMember,
leaveOrganization,
}; };
const roles = { const roles = {

View File

@@ -6,7 +6,7 @@ import type { InferRolesFromOption, Member } from "../schema";
import { APIError } from "better-call"; import { APIError } from "better-call";
import { generateId } from "../../../utils"; import { generateId } from "../../../utils";
import type { OrganizationOptions } from "../organization"; import type { OrganizationOptions } from "../organization";
import { getSessionFromCtx } from "../../../api"; import { getSessionFromCtx, sessionMiddleware } from "../../../api";
import { ORGANIZATION_ERROR_CODES } from "../error-codes"; import { ORGANIZATION_ERROR_CODES } from "../error-codes";
import { BASE_ERROR_CODES } from "../../../error/codes"; import { BASE_ERROR_CODES } from "../../../error/codes";
@@ -402,3 +402,55 @@ export const getActiveMember = createAuthEndpoint(
return ctx.json(member); return ctx.json(member);
}, },
); );
export const leaveOrganization = createAuthEndpoint(
"/organization/leave",
{
method: "POST",
body: z.object({
organizationId: z.string(),
}),
use: [sessionMiddleware, orgMiddleware],
},
async (ctx) => {
const session = ctx.context.session;
const adapter = getOrgAdapter(ctx.context);
const member = await adapter.findMemberByOrgId({
userId: session.user.id,
organizationId: ctx.body.organizationId,
});
if (!member) {
throw new APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
});
}
const isOwnerLeaving =
member.role === (ctx.context.orgOptions?.creatorRole || "owner");
if (isOwnerLeaving) {
const members = await ctx.context.adapter.findMany<Member>({
model: "member",
where: [
{
field: "organizationId",
value: ctx.body.organizationId,
},
],
});
const owners = members.filter(
(member) =>
member.role === (ctx.context.orgOptions?.creatorRole || "owner"),
);
if (owners.length <= 1) {
throw new APIError("BAD_REQUEST", {
message:
ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER,
});
}
}
await adapter.deleteMember(member.id);
if (session.session.activeOrganizationId === ctx.body.organizationId) {
await adapter.setActiveOrganization(session.session.token, null);
}
return ctx.json(member);
},
);