mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-07 20:37:44 +00:00
feat(organization): leave organization (#1239)
This commit is contained in:
@@ -46,3 +46,5 @@ export const {
|
|||||||
useListOrganizations,
|
useListOrganizations,
|
||||||
useActiveOrganization,
|
useActiveOrganization,
|
||||||
} = client;
|
} = client;
|
||||||
|
|
||||||
|
client.$store.listen("$sessionSignal", async () => {});
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user