diff --git a/docs/content/docs/plugins/organization.mdx b/docs/content/docs/plugins/organization.mdx index b2d43f8936..5c4df8d821 100644 --- a/docs/content/docs/plugins/organization.mdx +++ b/docs/content/docs/plugins/organization.mdx @@ -560,7 +560,7 @@ await authClient.organization.getInvitation({ ### List Invitations -To list all invitations you can use the `listInvitations` function provided by the client. +To list all invitations for a given organization you can use the `listInvitations` function provided by the client. ```ts title="auth-client.ts" const invitations = await authClient.organization.listInvitations({ @@ -570,6 +570,30 @@ const invitations = await authClient.organization.listInvitations({ }) ``` +### List user invitations + +To list all invitations for a given user you can use the `listUserInvitations` function provided by the client. + +```ts title="auth-client.ts" +const invitations = await authClient.organization.listUserInvitations() +``` + +On the server, you can pass the user ID as a query parameter. + +```ts title="api.ts" +const invitations = await auth.api.listUserInvitations({ + query: { + email: "user@example.com" + } +}) +``` + +The `email` query parameter is only available on the server to query for invitations for a specific user. + + + + + ## Members ### Remove Member diff --git a/packages/better-auth/src/plugins/organization/adapter.ts b/packages/better-auth/src/plugins/organization/adapter.ts index 9f26219d7d..2761d36be0 100644 --- a/packages/better-auth/src/plugins/organization/adapter.ts +++ b/packages/better-auth/src/plugins/organization/adapter.ts @@ -561,7 +561,13 @@ export const getOrgAdapter = ( }); return invitations; }, - + listUserInvitations: async (email: string) => { + const invitations = await adapter.findMany({ + model: "invitation", + where: [{ field: "email", value: email }], + }); + return invitations; + }, createInvitation: async ({ invitation, user, diff --git a/packages/better-auth/src/plugins/organization/organization.test.ts b/packages/better-auth/src/plugins/organization/organization.test.ts index a450891c0d..ae921fdfec 100644 --- a/packages/better-auth/src/plugins/organization/organization.test.ts +++ b/packages/better-auth/src/plugins/organization/organization.test.ts @@ -703,10 +703,11 @@ describe("organization", async (it) => { password: userOverLimit.password, name: userOverLimit.name, }); - const { res, headers: headers2 } = await signInWithUser( + const { headers: headers2 } = await signInWithUser( userOverLimit2.email, userOverLimit2.password, ); + await client.signUp.email( { email: userOverLimit2.email, @@ -738,6 +739,92 @@ describe("organization", async (it) => { }); expect(getFullOrganization.data?.members.length).toBe(6); }); + + it("should allow listing invitations for an org", async () => { + const invitations = await client.organization.listInvitations({ + query: { + organizationId: organizationId, + }, + fetchOptions: { + headers: headers, + }, + }); + expect(invitations.data?.length).toBe(4); + }); + + it("should allow listing invitations for a user using authClient", async () => { + const rng = crypto.randomUUID(); + const user = { + email: `${rng}@email.com`, + password: rng, + name: rng, + }; + const rng2 = crypto.randomUUID(); + const orgAdminUser = { + email: `${rng2}@email.com`, + password: rng2, + name: rng2, + }; + await auth.api.signUpEmail({ + body: user, + }); + await auth.api.signUpEmail({ + body: orgAdminUser, + }); + const { headers: headers2, res: session } = await signInWithUser( + user.email, + user.password, + ); + const { headers: adminHeaders, res: adminSession } = await signInWithUser( + orgAdminUser.email, + orgAdminUser.password, + ); + const orgRng = crypto.randomUUID(); + const org = await auth.api.createOrganization({ + body: { + name: orgRng, + slug: orgRng, + }, + headers: adminHeaders, + }); + const invitation = await client.organization.inviteMember({ + organizationId: org?.id, + email: user.email, + role: "member", + fetchOptions: { + headers: adminHeaders, + }, + }); + const userInvitations = await client.organization.listUserInvitations({ + fetchOptions: { + headers: headers2, + }, + }); + expect(userInvitations.data?.[0].id).toBe(invitation.data?.id); + expect(userInvitations.data?.length).toBe(1); + }); + + it("should allow listing invitations for a user using server", async () => { + const orgInvitations = await client.organization.listInvitations({ + fetchOptions: { + headers, + }, + }); + + if (!orgInvitations.data?.[0].email) throw new Error("No email found"); + + const invitations = await auth.api.listUserInvitations({ + query: { + email: orgInvitations.data?.[0].email, + }, + }); + + expect(invitations?.length).toBe( + orgInvitations.data.filter( + (x) => x.email === orgInvitations.data?.[0].email, + ).length, + ); + }); }); describe("access control", async (it) => { diff --git a/packages/better-auth/src/plugins/organization/organization.ts b/packages/better-auth/src/plugins/organization/organization.ts index 4197d9c633..dbbb1f825f 100644 --- a/packages/better-auth/src/plugins/organization/organization.ts +++ b/packages/better-auth/src/plugins/organization/organization.ts @@ -16,6 +16,7 @@ import { getInvitation, listInvitations, rejectInvitation, + listUserInvitations, } from "./routes/crud-invites"; import { addMember, @@ -386,6 +387,7 @@ export const organization = (options?: O) => { acceptInvitation, getInvitation, rejectInvitation, + listUserInvitations, checkOrganizationSlug, addMember: addMember(), removeMember, diff --git a/packages/better-auth/src/plugins/organization/routes/crud-invites.ts b/packages/better-auth/src/plugins/organization/routes/crud-invites.ts index 8f51d4e58f..25697bd2bc 100644 --- a/packages/better-auth/src/plugins/organization/routes/crud-invites.ts +++ b/packages/better-auth/src/plugins/organization/routes/crud-invites.ts @@ -745,3 +745,44 @@ export const listInvitations = createAuthEndpoint( return ctx.json(invitations); }, ); + +/** + * List all invitations recieved for a user + */ +export const listUserInvitations = createAuthEndpoint( + "/organization/list-user-invitations", + { + method: "GET", + use: [orgMiddleware], + query: z + .object({ + email: z + .string({ + description: + "The email of the user to list invitations for. This only works for server side API calls.", + }) + .optional(), + }) + .optional(), + }, + async (ctx) => { + const session = await getSessionFromCtx(ctx); + + if (ctx.request && ctx.query?.email) { + throw new APIError("BAD_REQUEST", { + message: "User email cannot be passed for client side API calls.", + }); + } + + const userEmail = session?.user.email || ctx.query?.email; + if (!userEmail) { + throw new APIError("BAD_REQUEST", { + message: "Missing session headers, or email query parameter.", + }); + } + const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions); + + const invitations = await adapter.listUserInvitations(userEmail); + return ctx.json(invitations); + }, +);