feat: get active memebr and refactor (#445)

This commit is contained in:
Bereket Engida
2024-11-07 16:35:04 +03:00
committed by GitHub
parent 3560ada8a5
commit f7271b79a5
9 changed files with 155 additions and 43 deletions

View File

@@ -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<ActiveOrganization | null>(
null,
props.activeOrganization,
);
const [isRevoking, setIsRevoking] = useState<string[]>([]);
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 }) {
<DropdownMenuContent align="start">
<DropdownMenuItem
className=" py-1"
onClick={() => {
organization.setActive(null);
onClick={async () => {
organization.setActive({
orgId: null,
});
setOptimisticOrg(null);
}}
>
@@ -103,16 +103,19 @@ export function OrganizationCard(props: { session: Session | null }) {
<DropdownMenuItem
className=" py-1"
key={org.id}
onClick={() => {
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);
}}
>
<p className="text-sm sm">{org.name}</p>

View File

@@ -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 (
<div className="w-full">
<div className="flex gap-4 flex-col">
@@ -29,7 +33,10 @@ export default async function DashboardPage() {
session={JSON.parse(JSON.stringify(session))}
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>
);

View File

@@ -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
</Tab>
</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
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.

View File

@@ -21,9 +21,9 @@ interface OrganizationClientOptions {
export const organizationClient = <O extends OrganizationClientOptions>(
options?: O,
) => {
const activeOrgId = atom<string | null | undefined>(undefined);
const $listOrg = atom<boolean>(false);
const $activeOrgSignal = atom<boolean>(false);
const $activeMemberSignal = atom<boolean>(false);
type DefaultStatements = typeof defaultStatements;
type Statements = O["ac"] extends AccessControl<infer S>
@@ -44,7 +44,7 @@ export const organizationClient = <O extends OrganizationClientOptions>(
id: string;
name: string;
email: string;
image: string;
image?: string;
};
}
>[];
@@ -56,9 +56,9 @@ export const organizationClient = <O extends OrganizationClientOptions>(
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 = <O extends OrganizationClientOptions>(
}
>
>(
[activeOrgId, $activeOrgSignal],
"/organization/activate",
[$activeOrgSignal],
"/organization/get-full-organization",
$fetch,
() => ({
method: "POST",
body: {
orgId: activeOrgId.get(),
},
method: "GET",
}),
);
const activeMember = useAuthQuery<Member>(
[$activeMemberSignal],
"/organization/get-active-member",
$fetch,
{
method: "GET",
},
);
return {
$listOrg,
$activeOrgSignal,
$activeMemberSignal,
activeOrganization,
listOrganizations,
activeMember,
};
},
atomListeners: [
@@ -135,6 +143,12 @@ export const organizationClient = <O extends OrganizationClientOptions>(
},
signal: "$activeOrgSignal",
},
{
matcher(path) {
return path.includes("/organization/update-member-role");
},
signal: "$activeMemberSignal",
},
],
} satisfies BetterAuthClientPlugin;
};

View File

@@ -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,

View File

@@ -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 = <O extends OrganizationOptions>(options?: O) => {
rejectInvitation,
removeMember,
updateMemberRole,
getActiveMember,
};
const roles = {

View File

@@ -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);
},
);

View File

@@ -249,7 +249,7 @@ export const getFullOrganization = createAuthEndpoint(
);
export const setActiveOrganization = createAuthEndpoint(
"/organization/activate",
"/organization/set-active",
{
method: "POST",
body: z.object({