UNPKG

better-auth

Version:

The most comprehensive authentication library for TypeScript.

1,687 lines (1,682 loc) 181 kB
import { APIError } from 'better-call'; import * as z from 'zod/v4'; import { a as createAuthEndpoint, g as getSessionFromCtx, s as sessionMiddleware, B as BASE_ERROR_CODES, i as requestOnlySessionMiddleware } from './better-auth.DV5EHeYG.mjs'; import './better-auth.CewjboYP.mjs'; import './better-auth.CMQ3rA-I.mjs'; import '@better-auth/utils/base64'; import '@better-auth/utils/hmac'; import './better-auth.BjBlybv-.mjs'; import '@better-auth/utils/binary'; import './better-auth.Dcv8PS7T.mjs'; import { g as getDate } from './better-auth.CW6D9eSx.mjs'; import { B as BetterAuthError } from './better-auth.DdzSJf-n.mjs'; import { p as parseJSON } from './better-auth.ffWeg50w.mjs'; import { o as orgMiddleware, a as orgSessionMiddleware, t as teamSchema } from './better-auth.B1aLAOp3.mjs'; import { s as setSessionCookie } from './better-auth.UfVWArIB.mjs'; import { h as hasPermission } from './better-auth.Cxcr0S4x.mjs'; import { t as toZodSchema } from './better-auth.DrJWSFx6.mjs'; import '@better-auth/utils/random'; import '@better-auth/utils/hash'; import '@noble/ciphers/chacha.js'; import '@noble/ciphers/utils.js'; import 'jose'; import '@noble/hashes/scrypt.js'; import '@better-auth/utils'; import '@better-auth/utils/hex'; import '@noble/hashes/utils.js'; import './better-auth.B4Qoxdgc.mjs'; import 'kysely'; import { defaultRoles } from '../plugins/organization/access/index.mjs'; const shimContext = (originalObject, newContext) => { const shimmedObj = {}; for (const [key, value] of Object.entries(originalObject)) { shimmedObj[key] = (ctx) => { return value({ ...ctx, context: { ...newContext, ...ctx.context } }); }; shimmedObj[key].path = value.path; shimmedObj[key].method = value.method; shimmedObj[key].options = value.options; shimmedObj[key].headers = value.headers; } return shimmedObj; }; const getOrgAdapter = (context, options) => { const adapter = context.adapter; return { findOrganizationBySlug: async (slug) => { const organization = await adapter.findOne({ model: "organization", where: [ { field: "slug", value: slug } ] }); return organization; }, createOrganization: async (data) => { const organization = await adapter.create({ model: "organization", data: { ...data.organization, metadata: data.organization.metadata ? JSON.stringify(data.organization.metadata) : void 0 } }); return { ...organization, metadata: organization.metadata && typeof organization.metadata === "string" ? JSON.parse(organization.metadata) : void 0 }; }, findMemberByEmail: async (data) => { const user = await adapter.findOne({ model: "user", where: [ { field: "email", value: data.email.toLowerCase() } ] }); if (!user) { return null; } const member = await adapter.findOne({ model: "member", where: [ { field: "organizationId", value: data.organizationId }, { field: "userId", value: user.id } ] }); if (!member) { return null; } return { ...member, user: { id: user.id, name: user.name, email: user.email, image: user.image } }; }, listMembers: async (data) => { const members = await Promise.all([ adapter.findMany({ model: "member", where: [ { field: "organizationId", value: data.organizationId }, ...data.filter?.field ? [ { field: data.filter?.field, value: data.filter?.value } ] : [] ], limit: data.limit || options?.membershipLimit || 100, offset: data.offset || 0, sortBy: data.sortBy ? { field: data.sortBy, direction: data.sortOrder || "asc" } : void 0 }), adapter.count({ model: "member", where: [ { field: "organizationId", value: data.organizationId }, ...data.filter?.field ? [ { field: data.filter?.field, value: data.filter?.value } ] : [] ] }) ]); const users = await adapter.findMany({ model: "user", where: [ { field: "id", value: members[0].map((member) => member.userId), operator: "in" } ] }); return { members: members[0].map((member) => { const user = users.find((user2) => user2.id === member.userId); if (!user) { throw new BetterAuthError( "Unexpected error: User not found for member" ); } return { ...member, user: { id: user.id, name: user.name, email: user.email, image: user.image } }; }), total: members[1] }; }, findMemberByOrgId: async (data) => { const [member, user] = await Promise.all([ await adapter.findOne({ model: "member", where: [ { field: "userId", value: data.userId }, { field: "organizationId", value: data.organizationId } ] }), await adapter.findOne({ model: "user", where: [ { field: "id", value: data.userId } ] }) ]); if (!user || !member) { return null; } return { ...member, user: { id: user.id, name: user.name, email: user.email, image: user.image } }; }, findMemberById: async (memberId) => { const member = await adapter.findOne({ model: "member", where: [ { field: "id", value: memberId } ] }); if (!member) { return null; } const user = await adapter.findOne({ model: "user", where: [ { field: "id", value: member.userId } ] }); if (!user) { return null; } return { ...member, user: { id: user.id, name: user.name, email: user.email, image: user.image } }; }, createMember: async (data) => { const member = await adapter.create({ model: "member", data: { ...data, createdAt: /* @__PURE__ */ new Date() } }); return member; }, updateMember: async (memberId, role) => { const member = await adapter.update({ model: "member", where: [ { field: "id", value: memberId } ], update: { role } }); return member; }, deleteMember: async (memberId) => { const member = await adapter.delete({ model: "member", where: [ { field: "id", value: memberId } ] }); return member; }, updateOrganization: async (organizationId, data) => { const organization = await adapter.update({ model: "organization", where: [ { field: "id", value: organizationId } ], update: { ...data, metadata: typeof data.metadata === "object" ? JSON.stringify(data.metadata) : data.metadata } }); if (!organization) { return null; } return { ...organization, metadata: organization.metadata ? parseJSON(organization.metadata) : void 0 }; }, deleteOrganization: async (organizationId) => { await adapter.delete({ model: "member", where: [ { field: "organizationId", value: organizationId } ] }); await adapter.delete({ model: "invitation", where: [ { field: "organizationId", value: organizationId } ] }); await adapter.delete({ model: "organization", where: [ { field: "id", value: organizationId } ] }); return organizationId; }, setActiveOrganization: async (sessionToken, organizationId) => { const session = await context.internalAdapter.updateSession( sessionToken, { activeOrganizationId: organizationId } ); return session; }, findOrganizationById: async (organizationId) => { const organization = await adapter.findOne({ model: "organization", where: [ { field: "id", value: organizationId } ] }); return organization; }, checkMembership: async ({ userId, organizationId }) => { const member = await adapter.findOne({ model: "member", where: [ { field: "userId", value: userId }, { field: "organizationId", value: organizationId } ] }); return member; }, /** * @requires db */ findFullOrganization: async ({ organizationId, isSlug, includeTeams, membersLimit }) => { const org = await adapter.findOne({ model: "organization", where: [{ field: isSlug ? "slug" : "id", value: organizationId }] }); if (!org) { return null; } const [invitations, members, teams] = await Promise.all([ adapter.findMany({ model: "invitation", where: [{ field: "organizationId", value: org.id }] }), adapter.findMany({ model: "member", where: [{ field: "organizationId", value: org.id }], limit: membersLimit ?? options?.membershipLimit ?? 100 }), includeTeams ? adapter.findMany({ model: "team", where: [{ field: "organizationId", value: org.id }] }) : null ]); if (!org) return null; const userIds = members.map((member) => member.userId); const users = userIds.length > 0 ? await adapter.findMany({ model: "user", where: [{ field: "id", value: userIds, operator: "in" }], limit: options?.membershipLimit || 100 }) : []; const userMap = new Map(users.map((user) => [user.id, user])); const membersWithUsers = members.map((member) => { const user = userMap.get(member.userId); if (!user) { throw new BetterAuthError( "Unexpected error: User not found for member" ); } return { ...member, user: { id: user.id, name: user.name, email: user.email, image: user.image } }; }); return { ...org, invitations, members: membersWithUsers, teams }; }, listOrganizations: async (userId) => { const members = await adapter.findMany({ model: "member", where: [ { field: "userId", value: userId } ] }); if (!members || members.length === 0) { return []; } const organizationIds = members.map((member) => member.organizationId); const organizations = await adapter.findMany({ model: "organization", where: [ { field: "id", value: organizationIds, operator: "in" } ] }); return organizations; }, createTeam: async (data) => { const team = await adapter.create({ model: "team", data }); return team; }, findTeamById: async ({ teamId, organizationId, includeTeamMembers }) => { const team = await adapter.findOne({ model: "team", where: [ { field: "id", value: teamId }, ...organizationId ? [ { field: "organizationId", value: organizationId } ] : [] ] }); if (!team) { return null; } let members = []; if (includeTeamMembers) { members = await adapter.findMany({ model: "teamMember", where: [ { field: "teamId", value: teamId } ], limit: options?.membershipLimit || 100 }); return { ...team, members }; } return team; }, updateTeam: async (teamId, data) => { if ("id" in data) data.id = void 0; const team = await adapter.update({ model: "team", where: [ { field: "id", value: teamId } ], update: { ...data } }); return team; }, deleteTeam: async (teamId) => { await adapter.deleteMany({ model: "teamMember", where: [ { field: "teamId", value: teamId } ] }); const team = await adapter.delete({ model: "team", where: [ { field: "id", value: teamId } ] }); return team; }, listTeams: async (organizationId) => { const teams = await adapter.findMany({ model: "team", where: [ { field: "organizationId", value: organizationId } ] }); return teams; }, createTeamInvitation: async ({ email, role, teamId, organizationId, inviterId, expiresIn = 1e3 * 60 * 60 * 48 // Default expiration: 48 hours }) => { const expiresAt = getDate(expiresIn); const invitation = await adapter.create({ model: "invitation", data: { email, role, organizationId, teamId, inviterId, status: "pending", expiresAt } }); return invitation; }, setActiveTeam: async (sessionToken, teamId) => { const session = await context.internalAdapter.updateSession( sessionToken, { activeTeamId: teamId } ); return session; }, listTeamMembers: async (data) => { const members = await adapter.findMany({ model: "teamMember", where: [ { field: "teamId", value: data.teamId } ] }); return members; }, countTeamMembers: async (data) => { const count = await adapter.count({ model: "teamMember", where: [{ field: "teamId", value: data.teamId }] }); return count; }, countMembers: async (data) => { const count = await adapter.count({ model: "member", where: [{ field: "organizationId", value: data.organizationId }] }); return count; }, listTeamsByUser: async (data) => { const members = await adapter.findMany({ model: "teamMember", where: [ { field: "userId", value: data.userId } ] }); const teams = await adapter.findMany({ model: "team", where: [ { field: "id", operator: "in", value: members.map((m) => m.teamId) } ] }); return teams; }, findTeamMember: async (data) => { const member = await adapter.findOne({ model: "teamMember", where: [ { field: "teamId", value: data.teamId }, { field: "userId", value: data.userId } ] }); return member; }, findOrCreateTeamMember: async (data) => { const member = await adapter.findOne({ model: "teamMember", where: [ { field: "teamId", value: data.teamId }, { field: "userId", value: data.userId } ] }); if (member) return member; return await adapter.create({ model: "teamMember", data: { teamId: data.teamId, userId: data.userId, createdAt: /* @__PURE__ */ new Date() } }); }, removeTeamMember: async (data) => { await adapter.delete({ model: "teamMember", where: [ { field: "teamId", value: data.teamId }, { field: "userId", value: data.userId } ] }); }, findInvitationsByTeamId: async (teamId) => { const invitations = await adapter.findMany({ model: "invitation", where: [ { field: "teamId", value: teamId } ] }); return invitations; }, listUserInvitations: async (email) => { const invitations = await adapter.findMany({ model: "invitation", where: [{ field: "email", value: email.toLowerCase() }] }); return invitations; }, createInvitation: async ({ invitation, user }) => { const defaultExpiration = 60 * 60 * 48; const expiresAt = getDate( options?.invitationExpiresIn || defaultExpiration, "sec" ); const invite = await adapter.create({ model: "invitation", data: { status: "pending", expiresAt, inviterId: user.id, ...invitation, teamId: invitation.teamIds.length > 0 ? invitation.teamIds.join(",") : null } }); return invite; }, findInvitationById: async (id) => { const invitation = await adapter.findOne({ model: "invitation", where: [ { field: "id", value: id } ] }); return invitation; }, findPendingInvitation: async (data) => { const invitation = await adapter.findMany({ model: "invitation", where: [ { field: "email", value: data.email.toLowerCase() }, { field: "organizationId", value: data.organizationId }, { field: "status", value: "pending" } ] }); return invitation.filter( (invite) => new Date(invite.expiresAt) > /* @__PURE__ */ new Date() ); }, findPendingInvitations: async (data) => { const invitations = await adapter.findMany({ model: "invitation", where: [ { field: "organizationId", value: data.organizationId }, { field: "status", value: "pending" } ] }); return invitations.filter( (invite) => new Date(invite.expiresAt) > /* @__PURE__ */ new Date() ); }, listInvitations: async (data) => { const invitations = await adapter.findMany({ model: "invitation", where: [ { field: "organizationId", value: data.organizationId } ] }); return invitations; }, updateInvitation: async (data) => { const invitation = await adapter.update({ model: "invitation", where: [ { field: "id", value: data.invitationId } ], update: { status: data.status } }); return invitation; } }; }; const ORGANIZATION_ERROR_CODES = { YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_ORGANIZATION: "You are not allowed to create a new organization", YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_ORGANIZATIONS: "You have reached the maximum number of organizations", ORGANIZATION_ALREADY_EXISTS: "Organization already exists", ORGANIZATION_NOT_FOUND: "Organization not found", USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION: "User is not a member of the organization", YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION: "You are not allowed to update this organization", YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_ORGANIZATION: "You are not allowed to delete this organization", NO_ACTIVE_ORGANIZATION: "No active organization", USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION: "User is already a member of this organization", MEMBER_NOT_FOUND: "Member not found", ROLE_NOT_FOUND: "Role not found", YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM: "You are not allowed to create a new team", TEAM_ALREADY_EXISTS: "Team already exists", TEAM_NOT_FOUND: "Team not found", YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER: "You cannot leave the organization as the only owner", YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER: "You cannot leave the organization without an owner", YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER: "You are not allowed to delete this member", YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION: "You are not allowed to invite users to this organization", USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION: "User is already invited to this organization", 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: "Inviter is no longer a member of the organization", YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE: "You are not allowed to invite a user with this role", FAILED_TO_RETRIEVE_INVITATION: "Failed to retrieve invitation", YOU_HAVE_REACHED_THE_MAXIMUM_NUMBER_OF_TEAMS: "You have reached the maximum number of teams", UNABLE_TO_REMOVE_LAST_TEAM: "Unable to remove last team", YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER: "You are not allowed to update this member", ORGANIZATION_MEMBERSHIP_LIMIT_REACHED: "Organization membership limit reached", YOU_ARE_NOT_ALLOWED_TO_CREATE_TEAMS_IN_THIS_ORGANIZATION: "You are not allowed to create teams in this organization", YOU_ARE_NOT_ALLOWED_TO_DELETE_TEAMS_IN_THIS_ORGANIZATION: "You are not allowed to delete teams in this organization", YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_TEAM: "You are not allowed to update this team", YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_TEAM: "You are not allowed to delete this team", INVITATION_LIMIT_REACHED: "Invitation limit reached", TEAM_MEMBER_LIMIT_REACHED: "Team member limit reached", USER_IS_NOT_A_MEMBER_OF_THE_TEAM: "User is not a member of the team", YOU_CAN_NOT_ACCESS_THE_MEMBERS_OF_THIS_TEAM: "You are not allowed to list the members of this team", YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM: "You do not have an active team", YOU_ARE_NOT_ALLOWED_TO_CREATE_A_NEW_TEAM_MEMBER: "You are not allowed to create a new member", YOU_ARE_NOT_ALLOWED_TO_REMOVE_A_TEAM_MEMBER: "You are not allowed to remove a team member", YOU_ARE_NOT_ALLOWED_TO_ACCESS_THIS_ORGANIZATION: "You are not allowed to access this organization as an owner", YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION: "You are not a member of this organization" }; const createInvitation = (option) => { const additionalFieldsSchema = toZodSchema({ fields: option?.schema?.invitation?.additionalFields || {}, isClientSide: true }); const baseSchema = z.object({ email: z.string().meta({ description: "The email address of the user to invite" }), role: z.union([ z.string().meta({ description: "The role to assign to the user" }), z.array( z.string().meta({ description: "The roles to assign to the user" }) ) ]).meta({ description: 'The role(s) to assign to the user. It can be `admin`, `member`, or `guest`. Eg: "member"' }), organizationId: z.string().meta({ description: "The organization ID to invite the user to" }).optional(), resend: z.boolean().meta({ description: "Resend the invitation email, if the user is already invited. Eg: true" }).optional(), teamId: z.union([ z.string().meta({ description: "The team ID to invite the user to" }).optional(), z.array(z.string()).meta({ description: "The team IDs to invite the user to" }).optional() ]) }); return createAuthEndpoint( "/organization/invite-member", { method: "POST", use: [orgMiddleware, orgSessionMiddleware], body: z.object({ ...baseSchema.shape, ...additionalFieldsSchema.shape }), metadata: { $Infer: { body: {} }, openapi: { description: "Invite a user to an organization", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { id: { type: "string" }, email: { type: "string" }, role: { type: "string" }, organizationId: { type: "string" }, inviterId: { type: "string" }, status: { type: "string" }, expiresAt: { type: "string" } }, required: [ "id", "email", "role", "organizationId", "inviterId", "status", "expiresAt" ] } } } } } } } }, async (ctx) => { const session = ctx.context.session; const organizationId = ctx.body.organizationId || session.session.activeOrganizationId; if (!organizationId) { throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND }); } const adapter = getOrgAdapter(ctx.context, option); const member = await adapter.findMemberByOrgId({ userId: session.user.id, organizationId }); if (!member) { throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND }); } const canInvite = await hasPermission( { role: member.role, options: ctx.context.orgOptions, permissions: { invitation: ["create"] }, organizationId }, ctx ); if (!canInvite) { throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION }); } const creatorRole = ctx.context.orgOptions.creatorRole || "owner"; const roles = parseRoles(ctx.body.role); if (member.role !== creatorRole && roles.split(",").includes(creatorRole)) { throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE }); } const alreadyMember = await adapter.findMemberByEmail({ email: ctx.body.email, organizationId }); if (alreadyMember) { throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION }); } const alreadyInvited = await adapter.findPendingInvitation({ email: ctx.body.email, organizationId }); if (alreadyInvited.length && !ctx.body.resend) { throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION }); } const organization = await adapter.findOrganizationById(organizationId); if (!organization) { throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND }); } if (alreadyInvited.length && ctx.body.resend) { const existingInvitation = alreadyInvited[0]; const defaultExpiration = 60 * 60 * 48; const newExpiresAt = getDate( ctx.context.orgOptions.invitationExpiresIn || defaultExpiration, "sec" ); await ctx.context.adapter.update({ model: "invitation", where: [ { field: "id", value: existingInvitation.id } ], update: { expiresAt: newExpiresAt } }); const updatedInvitation = { ...existingInvitation, expiresAt: newExpiresAt }; await ctx.context.orgOptions.sendInvitationEmail?.( { id: updatedInvitation.id, role: updatedInvitation.role, email: updatedInvitation.email.toLowerCase(), organization, inviter: { ...member, user: session.user }, invitation: updatedInvitation }, ctx.request ); return ctx.json(updatedInvitation); } if (alreadyInvited.length && ctx.context.orgOptions.cancelPendingInvitationsOnReInvite) { await adapter.updateInvitation({ invitationId: alreadyInvited[0].id, status: "canceled" }); } const invitationLimit = typeof ctx.context.orgOptions.invitationLimit === "function" ? await ctx.context.orgOptions.invitationLimit( { user: session.user, organization, member }, ctx.context ) : ctx.context.orgOptions.invitationLimit ?? 100; const pendingInvitations = await adapter.findPendingInvitations({ organizationId }); if (pendingInvitations.length >= invitationLimit) { throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.INVITATION_LIMIT_REACHED }); } if (ctx.context.orgOptions.teams && ctx.context.orgOptions.teams.enabled && typeof ctx.context.orgOptions.teams.maximumMembersPerTeam !== "undefined" && "teamId" in ctx.body && ctx.body.teamId) { const teamIds2 = typeof ctx.body.teamId === "string" ? [ctx.body.teamId] : ctx.body.teamId; for (const teamId of teamIds2) { const team = await adapter.findTeamById({ teamId, organizationId, includeTeamMembers: true }); if (!team) { throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND }); } const maximumMembersPerTeam = typeof ctx.context.orgOptions.teams.maximumMembersPerTeam === "function" ? await ctx.context.orgOptions.teams.maximumMembersPerTeam({ teamId, session, organizationId }) : ctx.context.orgOptions.teams.maximumMembersPerTeam; if (team.members.length >= maximumMembersPerTeam) { throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.TEAM_MEMBER_LIMIT_REACHED }); } } } const teamIds = "teamId" in ctx.body ? typeof ctx.body.teamId === "string" ? [ctx.body.teamId] : ctx.body.teamId ?? [] : []; const { email: _, role: __, organizationId: ___, resend: ____, ...additionalFields } = ctx.body; let invitationData = { role: roles, email: ctx.body.email.toLowerCase(), organizationId, teamIds, ...additionalFields ? additionalFields : {} }; if (option?.organizationHooks?.beforeCreateInvitation) { const response = await option?.organizationHooks.beforeCreateInvitation( { invitation: { ...invitationData, inviterId: session.user.id, teamId: teamIds.length > 0 ? teamIds[0] : void 0 }, inviter: session.user, organization } ); if (response && typeof response === "object" && "data" in response) { invitationData = { ...invitationData, ...response.data }; } } const invitation = await adapter.createInvitation({ invitation: invitationData, user: session.user }); await ctx.context.orgOptions.sendInvitationEmail?.( { id: invitation.id, role: invitation.role, email: invitation.email.toLowerCase(), organization, inviter: { ...member, user: session.user }, //@ts-expect-error invitation }, ctx.request ); if (option?.organizationHooks?.afterCreateInvitation) { await option?.organizationHooks.afterCreateInvitation({ invitation, inviter: session.user, organization }); } return ctx.json(invitation); } ); }; const acceptInvitation = (options) => createAuthEndpoint( "/organization/accept-invitation", { method: "POST", body: z.object({ invitationId: z.string().meta({ description: "The ID of the invitation to accept" }) }), use: [orgMiddleware, orgSessionMiddleware], metadata: { openapi: { description: "Accept an invitation to an organization", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { invitation: { type: "object" }, member: { type: "object" } } } } } } } } } }, async (ctx) => { const session = ctx.context.session; const adapter = getOrgAdapter(ctx.context, options); const invitation = await adapter.findInvitationById( ctx.body.invitationId ); if (!invitation || invitation.expiresAt < /* @__PURE__ */ new Date() || invitation.status !== "pending") { throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND }); } if (invitation.email.toLowerCase() !== session.user.email.toLowerCase()) { throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION }); } 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 }); if (membersCount >= membershipLimit) { throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.ORGANIZATION_MEMBERSHIP_LIMIT_REACHED }); } const organization = await adapter.findOrganizationById( invitation.organizationId ); if (!organization) { throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND }); } if (options?.organizationHooks?.beforeAcceptInvitation) { await options?.organizationHooks.beforeAcceptInvitation({ invitation, user: session.user, organization }); } const acceptedI = await adapter.updateInvitation({ invitationId: ctx.body.invitationId, status: "accepted" }); if (!acceptedI) { throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.FAILED_TO_RETRIEVE_INVITATION }); } if (ctx.context.orgOptions.teams && ctx.context.orgOptions.teams.enabled && "teamId" in acceptedI && acceptedI.teamId) { const teamIds = acceptedI.teamId.split(","); const onlyOne = teamIds.length === 1; for (const teamId of teamIds) { await adapter.findOrCreateTeamMember({ teamId, userId: session.user.id }); if (typeof ctx.context.orgOptions.teams.maximumMembersPerTeam !== "undefined") { const members = await adapter.countTeamMembers({ teamId }); const maximumMembersPerTeam = typeof ctx.context.orgOptions.teams.maximumMembersPerTeam === "function" ? await ctx.context.orgOptions.teams.maximumMembersPerTeam({ teamId, session, organizationId: invitation.organizationId }) : ctx.context.orgOptions.teams.maximumMembersPerTeam; if (members >= maximumMembersPerTeam) { throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.TEAM_MEMBER_LIMIT_REACHED }); } } } if (onlyOne) { const teamId = teamIds[0]; const updatedSession = await adapter.setActiveTeam( session.session.token, teamId ); await setSessionCookie(ctx, { session: updatedSession, user: session.user }); } } const member = await adapter.createMember({ organizationId: invitation.organizationId, userId: session.user.id, role: invitation.role, createdAt: /* @__PURE__ */ new Date() }); await adapter.setActiveOrganization( session.session.token, invitation.organizationId ); if (!acceptedI) { return ctx.json(null, { status: 400, body: { message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND } }); } if (options?.organizationHooks?.afterAcceptInvitation) { await options?.organizationHooks.afterAcceptInvitation({ invitation: acceptedI, member, user: session.user, organization }); } return ctx.json({ invitation: acceptedI, member }); } ); const rejectInvitation = (options) => createAuthEndpoint( "/organization/reject-invitation", { method: "POST", body: z.object({ invitationId: z.string().meta({ description: "The ID of the invitation to reject" }) }), use: [orgMiddleware, orgSessionMiddleware], metadata: { openapi: { description: "Reject an invitation to an organization", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { invitation: { type: "object" }, member: { type: "null" } } } } } } } } } }, async (ctx) => { const session = ctx.context.session; const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions); const invitation = await adapter.findInvitationById( ctx.body.invitationId ); if (!invitation || invitation.expiresAt < /* @__PURE__ */ new Date() || invitation.status !== "pending") { throw new APIError("BAD_REQUEST", { message: "Invitation not found!" }); } if (invitation.email.toLowerCase() !== session.user.email.toLowerCase()) { throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION }); } 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 organization = await adapter.findOrganizationById( invitation.organizationId ); if (!organization) { throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND }); } if (options?.organizationHooks?.beforeRejectInvitation) { await options?.organizationHooks.beforeRejectInvitation({ invitation, user: session.user, organization }); } const rejectedI = await adapter.updateInvitation({ invitationId: ctx.body.invitationId, status: "rejected" }); if (options?.organizationHooks?.afterRejectInvitation) { await options?.organizationHooks.afterRejectInvitation({ invitation: rejectedI || invitation, user: session.user, organization }); } return ctx.json({ invitation: rejectedI, member: null }); } ); const cancelInvitation = (options) => createAuthEndpoint( "/organization/cancel-invitation", { method: "POST", body: z.object({ invitationId: z.string().meta({ description: "The ID of the invitation to cancel" }) }), use: [orgMiddleware, orgSessionMiddleware], openapi: { description: "Cancel an invitation to an organization", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { invitation: { type: "object" } } } } } } } } }, async (ctx) => { const session = ctx.context.session; const adapter = getOrgAdapter(ctx.context, options); const invitation = await adapter.findInvitationById( ctx.body.invitationId ); if (!invitation) { throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND }); } const member = await adapter.findMemberByOrgId({ userId: session.user.id, organizationId: invitation.organizationId }); if (!member) { throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND }); } const canCancel = await hasPermission( { role: member.role, options: ctx.context.orgOptions, permissions: { invitation: ["cancel"] }, organizationId: invitation.organizationId }, ctx ); if (!canCancel) { throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION }); } const organization = await adapter.findOrganizationById( invitation.organizationId ); if (!organization) { throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND }); } if (options?.organizationHooks?.beforeCancelInvitation) { await options?.organizationHooks.beforeCancelInvitation({ invitation, cancelledBy: session.user, organization }); } const canceledI = await adapter.updateInvitation({ invitationId: ctx.body.invitationId, status: "canceled" }); if (options?.organizationHooks?.afterCancelInvitation) { await options?.organizationHooks.afterCancelInvitation({ invitation: canceledI || invitation, cancelledBy: session.user, organization }); } return ctx.json(canceledI); } ); const getInvitation = (options) => createAuthEndpoint( "/organization/get-invitation", { method: "GET", use: [orgMiddleware], requireHeaders: true, query: z.object({ id: z.string().meta({ description: "The ID of the invitation to get" }) }), metadata: { openapi: { description: "Get an invitation by ID", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "object", properties: { id: { type: "string" }, email: { type: "string" }, role: { type: "string" }, organizationId: { type: "string" }, inviterId: { type: "string" }, status: { type: "string" }, expiresAt: { type: "string" }, organizationName: { type: "string" }, organizationSlug: { type: "string" }, inviterEmail: { type: "string" } }, required: [ "id", "email", "role", "organizationId", "inviterId", "status", "expiresAt", "organizationName", "organizationSlug", "inviterEmail" ] } } } } } } } }, async (ctx) => { const session = await getSessionFromCtx(ctx); if (!session) { throw new APIError("UNAUTHORIZED", { message: "Not authenticated" }); } const adapter = getOrgAdapter(ctx.context, options); const invitation = await adapter.findInvitationById(ctx.query.id); if (!invitation || invitation.status !== "pending" || invitation.expiresAt < /* @__PURE__ */ new Date()) { throw new APIError("BAD_REQUEST", { message: "Invitation not found!" }); } if (invitation.email.toLowerCase() !== session.user.email.toLowerCase()) { throw new APIError("FORBIDDEN", { message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION }); } const organization = await adapter.findOrganizationById( invitation.organizationId ); if (!organization) { throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND }); } const member = await adapter.findMemberByOrgId({ userId: invitation.inviterId, organizationId: invitation.organizationId }); if (!member) { throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION }); } return ctx.json({ ...invitation, organizationName: organization.name, organizationSlug: organization.slug, inviterEmail: member.user.email }); } ); const listInvitations = (options) => createAuthEndpoint( "/organization/list-invitations", { method: "GET", use: [orgMiddleware, orgSessionMiddleware], query: z.object({ organizationId: z.string().meta({ description: "The ID of the organization to list invitations for" }).optional() }).optional() }, async (ctx) => { const session = await getSessionFromCtx(ctx); if (!session) { throw new APIError("UNAUTHORIZED", { message: "Not authenticated" }); } const orgId = ctx.query?.organizationId || session.session.activeOrganizationId; if (!orgId) { throw new APIError("BAD_REQUEST", { message: "Organization ID is required" }); } const adapter = getOrgAdapter(ctx.context, options); const isMember = await adapter.findMemberByOrgId({ userId: session.user.id, organizationId: orgId }); if (!isMember) { throw new APIError("FORBIDDEN", { message: "You are not a member of this organization" }); } const invitations = await adapter.listInvitations({ organizationId: orgId }); return ctx.json(invitations); } ); const listUserInvitations = (options) => createAuthEndpoint( "/organization/list-user-invitations", { method: "GET",