From f88e4b8dfbf390d6a1bca6c2949d594e1908ed1b Mon Sep 17 00:00:00 2001 From: Bereket Engida <86073083+Bekacru@users.noreply.github.com> Date: Mon, 4 Aug 2025 13:12:46 -0700 Subject: [PATCH] feat(organization): add option for requiring email verificaiton (#3785) * feat(organization): add option for requiring email verificaiton * add rejection * docs: add docs --- docs/content/docs/plugins/organization.mdx | 25 +++++++++++++++++++ .../src/plugins/organization/error-codes.ts | 2 ++ .../organization/routes/crud-invites.ts | 20 +++++++++++++++ .../src/plugins/organization/types.ts | 6 +++++ 4 files changed, 53 insertions(+) diff --git a/docs/content/docs/plugins/organization.mdx b/docs/content/docs/plugins/organization.mdx index e370cc4342..996f59b66a 100644 --- a/docs/content/docs/plugins/organization.mdx +++ b/docs/content/docs/plugins/organization.mdx @@ -584,6 +584,25 @@ type acceptInvitation = { ``` +#### Email Verification Requirement + +If the `requireEmailVerificationOnInvitation` option is enabled in your organization configuration, users must verify their email address before they can accept invitations. This adds an extra security layer to ensure that only verified users can join your organization. + +```ts title="auth.ts" +import { betterAuth } from "better-auth" +import { organization } from "better-auth/plugins" + +export const auth = betterAuth({ + plugins: [ + organization({ + requireEmailVerificationOnInvitation: true, // [!code highlight] + async sendInvitationEmail(data) { + // ... your email sending logic + } + }) + ] +}) +``` ### Cancel Invitation @@ -617,6 +636,10 @@ type rejectInvitation = { ``` + +Like accepting invitations, rejecting invitations also requires email verification when the `requireEmailVerificationOnInvitation` option is enabled. Users with unverified emails will receive an error when attempting to reject invitations. + + ### Get Invitation To get an invitation you can use the `organization.getInvitation` function provided by the client. You need to provide the invitation id as a query parameter. @@ -1759,3 +1782,5 @@ await client.organization.create({ **cancelPendingInvitationsOnReInvite**: `boolean` - Whether to cancel pending invitations if the user is already invited to the organization. By default, it's `false`. **invitationLimit**: `number` | `((user: User) => Promise | boolean)` - The maximum number of invitations allowed for a user. By default, it's `100`. You can set it to any number you want or a function that returns a boolean. + +**requireEmailVerificationOnInvitation**: `boolean` - Whether to require email verification before accepting or rejecting invitations. By default, it's `false`. When enabled, users must have verified their email address before they can accept or reject organization invitations. diff --git a/packages/better-auth/src/plugins/organization/error-codes.ts b/packages/better-auth/src/plugins/organization/error-codes.ts index ae6b5fa8da..e2c8e371ba 100644 --- a/packages/better-auth/src/plugins/organization/error-codes.ts +++ b/packages/better-auth/src/plugins/organization/error-codes.ts @@ -31,6 +31,8 @@ export const ORGANIZATION_ERROR_CODES = { INVITATION_NOT_FOUND: "Invitation not found", YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION: "You are not the recipient of the invitation", + EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION: + "Email verification required before accepting or rejecting invitation", YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION: "You are not allowed to cancel this invitation", INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION: 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 9f2704c48a..495d6848ce 100644 --- a/packages/better-auth/src/plugins/organization/routes/crud-invites.ts +++ b/packages/better-auth/src/plugins/organization/routes/crud-invites.ts @@ -419,6 +419,16 @@ export const acceptInvitation = (options: O) => }); } + if ( + ctx.context.orgOptions.requireEmailVerificationOnInvitation && + !session.user.emailVerified + ) { + throw new APIError("FORBIDDEN", { + message: + ORGANIZATION_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION, + }); + } + const membershipLimit = ctx.context.orgOptions?.membershipLimit || 100; const membersCount = await adapter.countMembers({ organizationId: invitation.organizationId, @@ -581,6 +591,16 @@ export const rejectInvitation = (options: O) => }); } + if ( + ctx.context.orgOptions.requireEmailVerificationOnInvitation && + !session.user.emailVerified + ) { + throw new APIError("FORBIDDEN", { + message: + ORGANIZATION_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION, + }); + } + const rejectedI = await adapter.updateInvitation({ invitationId: ctx.body.invitationId, status: "rejected", diff --git a/packages/better-auth/src/plugins/organization/types.ts b/packages/better-auth/src/plugins/organization/types.ts index e2620b5ae3..4b70f4081e 100644 --- a/packages/better-auth/src/plugins/organization/types.ts +++ b/packages/better-auth/src/plugins/organization/types.ts @@ -157,6 +157,12 @@ export interface OrganizationOptions { * @default false */ cancelPendingInvitationsOnReInvite?: boolean; + /** + * Require email verification on accepting or rejecting an invitation + * + * @default false + */ + requireEmailVerificationOnInvitation?: boolean; /** * Send an email with the * invitation link to the user.