mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-09 12:27:43 +00:00
feat: get active memebr and refactor (#445)
This commit is contained in:
@@ -49,17 +49,15 @@ import { AnimatePresence, motion } from "framer-motion";
|
|||||||
import CopyButton from "@/components/ui/copy-button";
|
import CopyButton from "@/components/ui/copy-button";
|
||||||
import Image from "next/image";
|
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 organizations = useListOrganizations();
|
||||||
const activeOrg = useActiveOrganization();
|
|
||||||
const [optimisticOrg, setOptimisticOrg] = useState<ActiveOrganization | null>(
|
const [optimisticOrg, setOptimisticOrg] = useState<ActiveOrganization | null>(
|
||||||
null,
|
props.activeOrganization,
|
||||||
);
|
);
|
||||||
const [isRevoking, setIsRevoking] = useState<string[]>([]);
|
const [isRevoking, setIsRevoking] = useState<string[]>([]);
|
||||||
useEffect(() => {
|
|
||||||
setOptimisticOrg(activeOrg.data);
|
|
||||||
}, [activeOrg.data]);
|
|
||||||
|
|
||||||
const inviteVariants = {
|
const inviteVariants = {
|
||||||
hidden: { opacity: 0, height: 0 },
|
hidden: { opacity: 0, height: 0 },
|
||||||
visible: { opacity: 1, height: "auto" },
|
visible: { opacity: 1, height: "auto" },
|
||||||
@@ -92,8 +90,10 @@ export function OrganizationCard(props: { session: Session | null }) {
|
|||||||
<DropdownMenuContent align="start">
|
<DropdownMenuContent align="start">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className=" py-1"
|
className=" py-1"
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
organization.setActive(null);
|
organization.setActive({
|
||||||
|
orgId: null,
|
||||||
|
});
|
||||||
setOptimisticOrg(null);
|
setOptimisticOrg(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -103,16 +103,19 @@ export function OrganizationCard(props: { session: Session | null }) {
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className=" py-1"
|
className=" py-1"
|
||||||
key={org.id}
|
key={org.id}
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
if (org.id === optimisticOrg?.id) {
|
if (org.id === optimisticOrg?.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
organization.setActive(org.id);
|
|
||||||
setOptimisticOrg({
|
setOptimisticOrg({
|
||||||
members: [],
|
members: [],
|
||||||
invitations: [],
|
invitations: [],
|
||||||
...org,
|
...org,
|
||||||
});
|
});
|
||||||
|
const { data } = await organization.setActive({
|
||||||
|
orgId: org.id,
|
||||||
|
});
|
||||||
|
setOptimisticOrg(data);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p className="text-sm sm">{org.name}</p>
|
<p className="text-sm sm">{org.name}</p>
|
||||||
|
|||||||
@@ -3,22 +3,26 @@ import { headers } from "next/headers";
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import UserCard from "./user-card";
|
import UserCard from "./user-card";
|
||||||
import { OrganizationCard } from "./organization-card";
|
import { OrganizationCard } from "./organization-card";
|
||||||
import AccountSwitcher from "@/components/account-swtich";
|
import AccountSwitcher from "@/components/account-switch";
|
||||||
|
|
||||||
export default async function DashboardPage() {
|
export default async function DashboardPage() {
|
||||||
const [session, activeSessions, deviceSessions] = await Promise.all([
|
const [session, activeSessions, deviceSessions, organization] =
|
||||||
auth.api.getSession({
|
await Promise.all([
|
||||||
headers: await headers(),
|
auth.api.getSession({
|
||||||
}),
|
headers: await headers(),
|
||||||
auth.api.listSessions({
|
}),
|
||||||
headers: await headers(),
|
auth.api.listSessions({
|
||||||
}),
|
headers: await headers(),
|
||||||
auth.api.listDeviceSessions({
|
}),
|
||||||
headers: await headers(),
|
auth.api.listDeviceSessions({
|
||||||
}),
|
headers: await headers(),
|
||||||
]).catch((e) => {
|
}),
|
||||||
throw redirect("/sign-in");
|
auth.api.getFullOrganization({
|
||||||
});
|
headers: await headers(),
|
||||||
|
}),
|
||||||
|
]).catch((e) => {
|
||||||
|
throw redirect("/sign-in");
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex gap-4 flex-col">
|
<div className="flex gap-4 flex-col">
|
||||||
@@ -29,7 +33,10 @@ export default async function DashboardPage() {
|
|||||||
session={JSON.parse(JSON.stringify(session))}
|
session={JSON.parse(JSON.stringify(session))}
|
||||||
activeSessions={JSON.parse(JSON.stringify(activeSessions))}
|
activeSessions={JSON.parse(JSON.stringify(activeSessions))}
|
||||||
/>
|
/>
|
||||||
<OrganizationCard session={JSON.parse(JSON.stringify(session))} />
|
<OrganizationCard
|
||||||
|
session={JSON.parse(JSON.stringify(session))}
|
||||||
|
activeOrganization={JSON.parse(JSON.stringify(organization))}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -181,10 +181,12 @@ Active organization is the workspace the user is currently working on. By defaul
|
|||||||
|
|
||||||
#### Set Active Organization
|
#### 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"
|
```ts title="client.ts"
|
||||||
client.organization.setActive("organization-id")
|
client.organization.setActive({
|
||||||
|
orgId: "organization-id"
|
||||||
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Use Active Organization
|
#### Use Active Organization
|
||||||
@@ -249,6 +251,27 @@ To retrieve the active organization for the user, you can call the `useActiveOrg
|
|||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
### 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
|
## 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.
|
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
|
## 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.
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ interface OrganizationClientOptions {
|
|||||||
export const organizationClient = <O extends OrganizationClientOptions>(
|
export const organizationClient = <O extends OrganizationClientOptions>(
|
||||||
options?: O,
|
options?: O,
|
||||||
) => {
|
) => {
|
||||||
const activeOrgId = atom<string | null | undefined>(undefined);
|
|
||||||
const $listOrg = atom<boolean>(false);
|
const $listOrg = atom<boolean>(false);
|
||||||
const $activeOrgSignal = atom<boolean>(false);
|
const $activeOrgSignal = atom<boolean>(false);
|
||||||
|
const $activeMemberSignal = atom<boolean>(false);
|
||||||
|
|
||||||
type DefaultStatements = typeof defaultStatements;
|
type DefaultStatements = typeof defaultStatements;
|
||||||
type Statements = O["ac"] extends AccessControl<infer S>
|
type Statements = O["ac"] extends AccessControl<infer S>
|
||||||
@@ -44,7 +44,7 @@ export const organizationClient = <O extends OrganizationClientOptions>(
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
image: string;
|
image?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
>[];
|
>[];
|
||||||
@@ -56,9 +56,9 @@ export const organizationClient = <O extends OrganizationClientOptions>(
|
|||||||
Member: {} as Member,
|
Member: {} as Member,
|
||||||
},
|
},
|
||||||
organization: {
|
organization: {
|
||||||
setActive(orgId: string | null) {
|
// setActive(orgId: string | null) {
|
||||||
activeOrgId.set(orgId);
|
// activeOrgId.set(orgId);
|
||||||
},
|
// },
|
||||||
hasPermission: async (data: {
|
hasPermission: async (data: {
|
||||||
permission: Partial<{
|
permission: Partial<{
|
||||||
//@ts-expect-error fix this later
|
//@ts-expect-error fix this later
|
||||||
@@ -102,22 +102,30 @@ export const organizationClient = <O extends OrganizationClientOptions>(
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
>(
|
>(
|
||||||
[activeOrgId, $activeOrgSignal],
|
[$activeOrgSignal],
|
||||||
"/organization/activate",
|
"/organization/get-full-organization",
|
||||||
$fetch,
|
$fetch,
|
||||||
() => ({
|
() => ({
|
||||||
method: "POST",
|
method: "GET",
|
||||||
body: {
|
|
||||||
orgId: activeOrgId.get(),
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const activeMember = useAuthQuery<Member>(
|
||||||
|
[$activeMemberSignal],
|
||||||
|
"/organization/get-active-member",
|
||||||
|
$fetch,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
$listOrg,
|
$listOrg,
|
||||||
$activeOrgSignal,
|
$activeOrgSignal,
|
||||||
|
$activeMemberSignal,
|
||||||
activeOrganization,
|
activeOrganization,
|
||||||
listOrganizations,
|
listOrganizations,
|
||||||
|
activeMember,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
atomListeners: [
|
atomListeners: [
|
||||||
@@ -135,6 +143,12 @@ export const organizationClient = <O extends OrganizationClientOptions>(
|
|||||||
},
|
},
|
||||||
signal: "$activeOrgSignal",
|
signal: "$activeOrgSignal",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
matcher(path) {
|
||||||
|
return path.includes("/organization/update-member-role");
|
||||||
|
},
|
||||||
|
signal: "$activeMemberSignal",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
} satisfies BetterAuthClientPlugin;
|
} satisfies BetterAuthClientPlugin;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ describe("organization", async (it) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should allow activating organization and set session", async () => {
|
it("should allow activating organization and set session", async () => {
|
||||||
const organization = await client.organization.activate({
|
const organization = await client.organization.setActive({
|
||||||
orgId,
|
orgId,
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
headers,
|
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 () => {
|
it("should allow updating member", async () => {
|
||||||
const { headers } = await signInWithTestUser();
|
const { headers } = await signInWithTestUser();
|
||||||
const org = await client.organization.getFull({
|
const org = await client.organization.getFull({
|
||||||
@@ -225,7 +243,7 @@ describe("organization", async (it) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should validate permissions", async () => {
|
it("should validate permissions", async () => {
|
||||||
await client.organization.activate({
|
await client.organization.setActive({
|
||||||
orgId,
|
orgId,
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
headers,
|
headers,
|
||||||
|
|||||||
@@ -27,7 +27,11 @@ import {
|
|||||||
getInvitation,
|
getInvitation,
|
||||||
rejectInvitation,
|
rejectInvitation,
|
||||||
} from "./routes/crud-invites";
|
} from "./routes/crud-invites";
|
||||||
import { removeMember, updateMemberRole } from "./routes/crud-members";
|
import {
|
||||||
|
getActiveMember,
|
||||||
|
removeMember,
|
||||||
|
updateMemberRole,
|
||||||
|
} from "./routes/crud-members";
|
||||||
import {
|
import {
|
||||||
createOrganization,
|
createOrganization,
|
||||||
deleteOrganization,
|
deleteOrganization,
|
||||||
@@ -176,6 +180,7 @@ export const organization = <O extends OrganizationOptions>(options?: O) => {
|
|||||||
rejectInvitation,
|
rejectInvitation,
|
||||||
removeMember,
|
removeMember,
|
||||||
updateMemberRole,
|
updateMemberRole,
|
||||||
|
getActiveMember,
|
||||||
};
|
};
|
||||||
|
|
||||||
const roles = {
|
const roles = {
|
||||||
|
|||||||
@@ -176,3 +176,37 @@ export const updateMemberRole = createAuthEndpoint(
|
|||||||
return ctx.json(updatedMember);
|
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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ export const getFullOrganization = createAuthEndpoint(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const setActiveOrganization = createAuthEndpoint(
|
export const setActiveOrganization = createAuthEndpoint(
|
||||||
"/organization/activate",
|
"/organization/set-active",
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: z.object({
|
body: z.object({
|
||||||
|
|||||||
Reference in New Issue
Block a user