diff --git a/demo/nextjs/app/dashboard/organization-card.tsx b/demo/nextjs/app/dashboard/organization-card.tsx index 33cadeac..5b441848 100644 --- a/demo/nextjs/app/dashboard/organization-card.tsx +++ b/demo/nextjs/app/dashboard/organization-card.tsx @@ -49,17 +49,15 @@ import { AnimatePresence, motion } from "framer-motion"; import CopyButton from "@/components/ui/copy-button"; import Image from "next/image"; -export function OrganizationCard(props: { session: Session | null }) { +export function OrganizationCard(props: { + session: Session | null; + activeOrganization: ActiveOrganization | null; +}) { const organizations = useListOrganizations(); - const activeOrg = useActiveOrganization(); const [optimisticOrg, setOptimisticOrg] = useState( - null, + props.activeOrganization, ); const [isRevoking, setIsRevoking] = useState([]); - useEffect(() => { - setOptimisticOrg(activeOrg.data); - }, [activeOrg.data]); - const inviteVariants = { hidden: { opacity: 0, height: 0 }, visible: { opacity: 1, height: "auto" }, @@ -92,8 +90,10 @@ export function OrganizationCard(props: { session: Session | null }) { { - organization.setActive(null); + onClick={async () => { + organization.setActive({ + orgId: null, + }); setOptimisticOrg(null); }} > @@ -103,16 +103,19 @@ export function OrganizationCard(props: { session: Session | null }) { { + onClick={async () => { if (org.id === optimisticOrg?.id) { return; } - organization.setActive(org.id); setOptimisticOrg({ members: [], invitations: [], ...org, }); + const { data } = await organization.setActive({ + orgId: org.id, + }); + setOptimisticOrg(data); }} >

{org.name}

diff --git a/demo/nextjs/app/dashboard/page.tsx b/demo/nextjs/app/dashboard/page.tsx index d97959ae..b79c72e1 100644 --- a/demo/nextjs/app/dashboard/page.tsx +++ b/demo/nextjs/app/dashboard/page.tsx @@ -3,22 +3,26 @@ import { headers } from "next/headers"; import { redirect } from "next/navigation"; import UserCard from "./user-card"; import { OrganizationCard } from "./organization-card"; -import AccountSwitcher from "@/components/account-swtich"; +import AccountSwitcher from "@/components/account-switch"; export default async function DashboardPage() { - const [session, activeSessions, deviceSessions] = await Promise.all([ - auth.api.getSession({ - headers: await headers(), - }), - auth.api.listSessions({ - headers: await headers(), - }), - auth.api.listDeviceSessions({ - headers: await headers(), - }), - ]).catch((e) => { - throw redirect("/sign-in"); - }); + const [session, activeSessions, deviceSessions, organization] = + await Promise.all([ + auth.api.getSession({ + headers: await headers(), + }), + auth.api.listSessions({ + headers: await headers(), + }), + auth.api.listDeviceSessions({ + headers: await headers(), + }), + auth.api.getFullOrganization({ + headers: await headers(), + }), + ]).catch((e) => { + throw redirect("/sign-in"); + }); return (
@@ -29,7 +33,10 @@ export default async function DashboardPage() { session={JSON.parse(JSON.stringify(session))} activeSessions={JSON.parse(JSON.stringify(activeSessions))} /> - +
); diff --git a/demo/nextjs/components/account-swtich.tsx b/demo/nextjs/components/account-switch.tsx similarity index 100% rename from demo/nextjs/components/account-swtich.tsx rename to demo/nextjs/components/account-switch.tsx diff --git a/docs/content/docs/plugins/organization.mdx b/docs/content/docs/plugins/organization.mdx index 616e89b2..98591c6c 100644 --- a/docs/content/docs/plugins/organization.mdx +++ b/docs/content/docs/plugins/organization.mdx @@ -181,10 +181,12 @@ Active organization is the workspace the user is currently working on. By defaul #### Set Active Organization -You can set the active organization by calling the `organization.setActive` function. It'll set the active organization for the user both on the client state and the session on the server. +You can set the active organization by calling the `organization.setActive` function. It'll set the active organization for the user session. ```ts title="client.ts" -client.organization.setActive("organization-id") +client.organization.setActive({ + orgId: "organization-id" +}) ``` #### Use Active Organization @@ -249,6 +251,27 @@ To retrieve the active organization for the user, you can call the `useActiveOrg +### Get Full Organization + +To get the full details of an organization, you can use the `getFull` function provided by the client. The `getFull` function takes an object with the following properties: + +```ts title="client.ts" +const organization = await client.organization.getFull({ + orgId: "organization-id" // optional, by default it will use the active organization +}) +``` + +To get the full organization on the server + +```ts title="api.ts" +import { auth } from "@/auth"; +import { headers } from "next/headers"; + +auth.api.getFullOrganization({ + headers: await headers() +}) +``` + ## Invitations To add a member to an organization, we first need to send an invitation to the user. The user will receive an email/sms with the invitation link. Once the user accepts the invitation, they will be added to the organization. @@ -360,6 +383,14 @@ await client.organization.updateMemberRole({ }) ``` +### Get Active Member + +To get the current member of the organization you can use the `organization.getCurrentMember` function. This function will return the current member of the organization. + +```ts title="client.ts" +const member = await client.organization.getActiveMember() +``` + ## 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. diff --git a/packages/better-auth/src/plugins/organization/client.ts b/packages/better-auth/src/plugins/organization/client.ts index 7213e2c6..80828a9e 100644 --- a/packages/better-auth/src/plugins/organization/client.ts +++ b/packages/better-auth/src/plugins/organization/client.ts @@ -21,9 +21,9 @@ interface OrganizationClientOptions { export const organizationClient = ( options?: O, ) => { - const activeOrgId = atom(undefined); const $listOrg = atom(false); const $activeOrgSignal = atom(false); + const $activeMemberSignal = atom(false); type DefaultStatements = typeof defaultStatements; type Statements = O["ac"] extends AccessControl @@ -44,7 +44,7 @@ export const organizationClient = ( id: string; name: string; email: string; - image: string; + image?: string; }; } >[]; @@ -56,9 +56,9 @@ export const organizationClient = ( Member: {} as Member, }, organization: { - setActive(orgId: string | null) { - activeOrgId.set(orgId); - }, + // setActive(orgId: string | null) { + // activeOrgId.set(orgId); + // }, hasPermission: async (data: { permission: Partial<{ //@ts-expect-error fix this later @@ -102,22 +102,30 @@ export const organizationClient = ( } > >( - [activeOrgId, $activeOrgSignal], - "/organization/activate", + [$activeOrgSignal], + "/organization/get-full-organization", $fetch, () => ({ - method: "POST", - body: { - orgId: activeOrgId.get(), - }, + method: "GET", }), ); + const activeMember = useAuthQuery( + [$activeMemberSignal], + "/organization/get-active-member", + $fetch, + { + method: "GET", + }, + ); + return { $listOrg, $activeOrgSignal, + $activeMemberSignal, activeOrganization, listOrganizations, + activeMember, }; }, atomListeners: [ @@ -135,6 +143,12 @@ export const organizationClient = ( }, signal: "$activeOrgSignal", }, + { + matcher(path) { + return path.includes("/organization/update-member-role"); + }, + signal: "$activeMemberSignal", + }, ], } satisfies BetterAuthClientPlugin; }; diff --git a/packages/better-auth/src/plugins/organization/organization.test.ts b/packages/better-auth/src/plugins/organization/organization.test.ts index 730f42a2..37933026 100644 --- a/packages/better-auth/src/plugins/organization/organization.test.ts +++ b/packages/better-auth/src/plugins/organization/organization.test.ts @@ -70,7 +70,7 @@ describe("organization", async (it) => { }); it("should allow activating organization and set session", async () => { - const organization = await client.organization.activate({ + const organization = await client.organization.setActive({ orgId, fetchOptions: { headers, @@ -147,6 +147,24 @@ describe("organization", async (it) => { ); }); + it("should allow getting a member", async () => { + const { headers } = await signInWithTestUser(); + await client.organization.setActive({ + orgId, + fetchOptions: { + headers, + }, + }); + const member = await client.organization.getActiveMember({ + fetchOptions: { + headers, + }, + }); + expect(member.data).toMatchObject({ + role: "owner", + }); + }); + it("should allow updating member", async () => { const { headers } = await signInWithTestUser(); const org = await client.organization.getFull({ @@ -225,7 +243,7 @@ describe("organization", async (it) => { }); it("should validate permissions", async () => { - await client.organization.activate({ + await client.organization.setActive({ orgId, fetchOptions: { headers, diff --git a/packages/better-auth/src/plugins/organization/organization.ts b/packages/better-auth/src/plugins/organization/organization.ts index 6b466d45..2afd81f4 100644 --- a/packages/better-auth/src/plugins/organization/organization.ts +++ b/packages/better-auth/src/plugins/organization/organization.ts @@ -27,7 +27,11 @@ import { getInvitation, rejectInvitation, } from "./routes/crud-invites"; -import { removeMember, updateMemberRole } from "./routes/crud-members"; +import { + getActiveMember, + removeMember, + updateMemberRole, +} from "./routes/crud-members"; import { createOrganization, deleteOrganization, @@ -176,6 +180,7 @@ export const organization = (options?: O) => { rejectInvitation, removeMember, updateMemberRole, + getActiveMember, }; const roles = { diff --git a/packages/better-auth/src/plugins/organization/routes/crud-members.ts b/packages/better-auth/src/plugins/organization/routes/crud-members.ts index d346f6b1..0dec901c 100644 --- a/packages/better-auth/src/plugins/organization/routes/crud-members.ts +++ b/packages/better-auth/src/plugins/organization/routes/crud-members.ts @@ -176,3 +176,37 @@ export const updateMemberRole = createAuthEndpoint( return ctx.json(updatedMember); }, ); + +export const getActiveMember = createAuthEndpoint( + "/organization/get-active-member", + { + method: "GET", + use: [orgMiddleware, orgSessionMiddleware], + }, + async (ctx) => { + const session = ctx.context.session; + const orgId = session.session.activeOrganizationId; + if (!orgId) { + return ctx.json(null, { + status: 400, + body: { + message: "No active organization found!", + }, + }); + } + const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions); + const member = await adapter.findMemberByOrgId({ + userId: session.user.id, + organizationId: orgId, + }); + if (!member) { + return ctx.json(null, { + status: 400, + body: { + message: "Member not found!", + }, + }); + } + return ctx.json(member); + }, +); diff --git a/packages/better-auth/src/plugins/organization/routes/crud-org.ts b/packages/better-auth/src/plugins/organization/routes/crud-org.ts index a2999be6..057f2e9d 100644 --- a/packages/better-auth/src/plugins/organization/routes/crud-org.ts +++ b/packages/better-auth/src/plugins/organization/routes/crud-org.ts @@ -249,7 +249,7 @@ export const getFullOrganization = createAuthEndpoint( ); export const setActiveOrganization = createAuthEndpoint( - "/organization/activate", + "/organization/set-active", { method: "POST", body: z.object({