diff --git a/docs/content/docs/plugins/organization.mdx b/docs/content/docs/plugins/organization.mdx index 8cd4801a..86e3452a 100644 --- a/docs/content/docs/plugins/organization.mdx +++ b/docs/content/docs/plugins/organization.mdx @@ -393,6 +393,21 @@ To get the current member of the organization you can use the `organization.getA const member = await client.organization.getActiveMember() ``` +### Add Member + +If you want to add a member directly to an organization without sending an invitation, you can use the `addMember` function which can only be invoked on the server. + +```ts title="api.ts" +import { auth } from "@/auth"; + +auth.api.addMember({ + body: { + userId: "user-id", + organizationId: "organization-id", + role: "admin" + } +}) +``` ## Access Control diff --git a/packages/better-auth/src/api/routes/session.ts b/packages/better-auth/src/api/routes/session.ts index 97e56340..28110c6c 100644 --- a/packages/better-auth/src/api/routes/session.ts +++ b/packages/better-auth/src/api/routes/session.ts @@ -214,7 +214,7 @@ export const getSessionFromCtx = async < return session as { session: S & Session; user: U & User; - }; + } | null; }; export const sessionMiddleware = createAuthMiddleware(async (ctx) => { diff --git a/packages/better-auth/src/plugins/organization/organization.test.ts b/packages/better-auth/src/plugins/organization/organization.test.ts index 408e8bcf..9342a92b 100644 --- a/packages/better-auth/src/plugins/organization/organization.test.ts +++ b/packages/better-auth/src/plugins/organization/organization.test.ts @@ -302,6 +302,31 @@ describe("organization", async (it) => { expectTypeOf(auth.api.createOrganization).toBeFunction(); expectTypeOf(auth.api.getInvitation).toBeFunction(); }); + + it("should add member on the server directly", async () => { + const newUser = await auth.api.signUpEmail({ + body: { + email: "new-member@email.com", + password: "password", + name: "new member", + }, + }); + const org = await auth.api.createOrganization({ + body: { + name: "test", + slug: "test", + }, + headers, + }); + const member = await auth.api.addMember({ + body: { + organizationId: org?.id, + userId: newUser.user.email, + role: "admin", + }, + }); + expect(member?.role).toBe("admin"); + }); }); 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 172ce554..d6c564b7 100644 --- a/packages/better-auth/src/plugins/organization/organization.ts +++ b/packages/better-auth/src/plugins/organization/organization.ts @@ -29,6 +29,7 @@ import { rejectInvitation, } from "./routes/crud-invites"; import { + addMember, getActiveMember, removeMember, updateMemberRole, @@ -215,6 +216,7 @@ export const organization = (options?: O) => { acceptInvitation, getInvitation, rejectInvitation, + addMember: addMember(), removeMember, updateMemberRole: updateMemberRole(options as O), getActiveMember, 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 e475eb4e..c281405e 100644 --- a/packages/better-auth/src/plugins/organization/routes/crud-members.ts +++ b/packages/better-auth/src/plugins/organization/routes/crud-members.ts @@ -4,7 +4,78 @@ import { getOrgAdapter } from "../adapter"; import { orgMiddleware, orgSessionMiddleware } from "../call"; import type { InferRolesFromOption, Member } from "../schema"; import { APIError } from "better-call"; +import type { User } from "../../../db/schema"; +import { generateId } from "../../../utils"; import type { OrganizationOptions } from "../organization"; +import { getSessionFromCtx } from "../../../api"; + +export const addMember = () => + createAuthEndpoint( + "/organization/add-member", + { + method: "POST", + body: z.object({ + userId: z.string(), + role: z.string() as unknown as InferRolesFromOption, + organizationId: z.string().optional(), + }), + use: [orgMiddleware], + metadata: { + SERVER_ONLY: true, + }, + }, + async (ctx) => { + const session = ctx.body.userId + ? await getSessionFromCtx<{ + session: { + activeOrganizationId?: string; + }; + }>(ctx).catch((e) => null) + : null; + const orgId = + ctx.body.organizationId || 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 user = await ctx.context.internalAdapter.findUserById( + ctx.body.userId, + ); + + if (!user) { + throw new APIError("BAD_REQUEST", { + message: "User not found!", + }); + } + + const alreadyMember = await adapter.findMemberByEmail({ + email: user.email, + organizationId: orgId, + }); + if (alreadyMember) { + throw new APIError("BAD_REQUEST", { + message: "User is already a member of this organization", + }); + } + + const createdMember = await adapter.createMember({ + id: generateId(), + organizationId: orgId, + userId: user.id, + role: ctx.body.role as string, + createdAt: new Date(), + }); + + return ctx.json(createdMember); + }, + ); export const removeMember = createAuthEndpoint( "/organization/remove-member",