UNPKG

better-auth

Version:

The most comprehensive authentication framework for TypeScript.

602 lines (601 loc) • 26.6 kB
import { getDate } from "../../../utils/date.mjs"; import { setSessionCookie } from "../../../cookies/index.mjs"; import { toZodSchema } from "../../../db/to-zod.mjs"; import { getSessionFromCtx } from "../../../api/routes/session.mjs"; import { defaultRoles } from "../access/statement.mjs"; import { ORGANIZATION_ERROR_CODES } from "../error-codes.mjs"; import { getOrgAdapter, resolveMaximumMembersPerTeam } from "../adapter.mjs"; import { orgMiddleware, orgSessionMiddleware } from "../call.mjs"; import { hasPermission } from "../has-permission.mjs"; import { parseRoles } from "../organization.mjs"; import { runWithTransaction } from "@better-auth/core/context"; import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error"; import { createAuthEndpoint } from "@better-auth/core/api"; import * as z from "zod"; //#region src/plugins/organization/routes/crud-invites.ts const baseInvitationSchema = 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`, owner. 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()]) }); const getAdvancedGenerateId = (advancedOptions) => { if (typeof advancedOptions !== "object" || advancedOptions === null) return; const generateId = advancedOptions.generateId; if (typeof generateId !== "function") return; return generateId; }; const hasBuiltInOpaqueInvitationIdGeneration = ({ advancedGenerateId, databaseGenerateId }) => advancedGenerateId === void 0 && (databaseGenerateId === void 0 || databaseGenerateId === "uuid"); const shouldRequireVerifiedEmailForInvitationIdAction = ({ organizationOptions, advancedGenerateId, databaseGenerateId }) => { if (organizationOptions.requireEmailVerificationOnInvitation !== void 0) return organizationOptions.requireEmailVerificationOnInvitation; return !hasBuiltInOpaqueInvitationIdGeneration({ advancedGenerateId, databaseGenerateId }); }; const createInvitation = (option) => { const additionalFieldsSchema = toZodSchema({ fields: option?.schema?.invitation?.additionalFields || {}, isClientSide: true }); return createAuthEndpoint("/organization/invite-member", { method: "POST", requireHeaders: true, use: [orgMiddleware, orgSessionMiddleware], body: z.object({ ...baseInvitationSchema.shape, ...additionalFieldsSchema.shape }), metadata: { $Infer: { body: {} }, openapi: { operationId: "createOrganizationInvitation", description: "Create an invitation 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" }, createdAt: { type: "string" } }, required: [ "id", "email", "role", "organizationId", "inviterId", "status", "expiresAt", "createdAt" ] } } } } } } } }, async (ctx) => { const session = ctx.context.session; const organizationId = ctx.body.organizationId || session.session.activeOrganizationId; if (!organizationId) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND); const email = ctx.body.email.toLowerCase(); if (!z.email().safeParse(email).success) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.INVALID_EMAIL); const adapter = getOrgAdapter(ctx.context, option); const member = await adapter.findMemberByOrgId({ userId: session.user.id, organizationId }); if (!member) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND); if (!await hasPermission({ role: member.role, options: ctx.context.orgOptions, permissions: { invitation: ["create"] }, organizationId }, ctx)) throw APIError.from("FORBIDDEN", 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); const rolesArray = roles.split(",").map((r) => r.trim()).filter(Boolean); const defaults = Object.keys(defaultRoles); const customRoles = Object.keys(ctx.context.orgOptions.roles || {}); const validStaticRoles = /* @__PURE__ */ new Set([...defaults, ...customRoles]); const unknownRoles = rolesArray.filter((role) => !validStaticRoles.has(role)); if (unknownRoles.length > 0) if (ctx.context.orgOptions.dynamicAccessControl?.enabled) { const foundRoleNames = (await ctx.context.adapter.findMany({ model: "organizationRole", where: [{ field: "organizationId", value: organizationId }, { field: "role", value: unknownRoles, operator: "in" }] })).map((r) => r.role); const stillInvalid = unknownRoles.filter((r) => !foundRoleNames.includes(r)); if (stillInvalid.length > 0) throw new APIError("BAD_REQUEST", { message: `${ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND}: ${stillInvalid.join(", ")}` }); } else throw new APIError("BAD_REQUEST", { message: `${ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND}: ${unknownRoles.join(", ")}` }); if (!member.role.split(",").map((r) => r.trim()).includes(creatorRole) && roles.split(",").includes(creatorRole)) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE); if (await adapter.findMemberByEmail({ email, organizationId })) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION); const alreadyInvited = await adapter.findPendingInvitation({ email, organizationId }); if (alreadyInvited.length && !ctx.body.resend && !ctx.context.orgOptions.cancelPendingInvitationsOnReInvite) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION); const organization = await adapter.findOrganizationById(organizationId); if (!organization) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND); if (alreadyInvited.length && ctx.body.resend) { const existingInvitation = alreadyInvited[0]; const newExpiresAt = getDate(ctx.context.orgOptions.invitationExpiresIn || 3600 * 48, "sec"); await ctx.context.adapter.update({ model: "invitation", where: [{ field: "id", value: existingInvitation.id }], update: { expiresAt: newExpiresAt } }); const updatedInvitation = { ...existingInvitation, expiresAt: newExpiresAt }; if (ctx.context.orgOptions.sendInvitationEmail) await ctx.context.runInBackgroundOrAwait(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; if ((await adapter.findPendingInvitations({ organizationId })).length >= invitationLimit) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.INVITATION_LIMIT_REACHED); if (ctx.context.orgOptions.teams?.enabled && "teamId" in ctx.body && ctx.body.teamId) { const requestedTeamIds = typeof ctx.body.teamId === "string" ? [ctx.body.teamId] : ctx.body.teamId; if (requestedTeamIds.some((id) => id.includes(","))) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.INVALID_TEAM_ID); for (const teamId of requestedTeamIds) if (!await adapter.findTeamById({ teamId, organizationId })) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND); } 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 teamIds = typeof ctx.body.teamId === "string" ? [ctx.body.teamId] : ctx.body.teamId; for (const teamId of teamIds) { const team = await adapter.findTeamById({ teamId, organizationId, includeTeamMembers: true }); if (!team) throw APIError.from("BAD_REQUEST", 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 APIError.from("FORBIDDEN", 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, 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 }); if (ctx.context.orgOptions.sendInvitationEmail) await ctx.context.runInBackgroundOrAwait(ctx.context.orgOptions.sendInvitationEmail({ id: invitation.id, role: invitation.role, email: invitation.email.toLowerCase(), organization, inviter: { ...member, user: session.user }, invitation }, ctx.request)); if (option?.organizationHooks?.afterCreateInvitation) await option?.organizationHooks.afterCreateInvitation({ invitation, inviter: session.user, organization }); return ctx.json(invitation); }); }; const acceptInvitationBodySchema = z.object({ invitationId: z.string().meta({ description: "The ID of the invitation to accept" }) }); const acceptInvitation = (options) => createAuthEndpoint("/organization/accept-invitation", { method: "POST", body: acceptInvitationBodySchema, requireHeaders: true, 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 APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND); if (invitation.email.toLowerCase() !== session.user.email.toLowerCase()) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION); if (shouldRequireVerifiedEmailForInvitationIdAction({ organizationOptions: ctx.context.orgOptions, advancedGenerateId: getAdvancedGenerateId(ctx.context.options.advanced), databaseGenerateId: ctx.context.options.advanced?.database?.generateId }) && !session.user.emailVerified) throw APIError.from("FORBIDDEN", 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 }); const organization = await adapter.findOrganizationById(invitation.organizationId); if (!organization) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND); if (membersCount >= (typeof membershipLimit === "number" ? membershipLimit : await membershipLimit(session.user, organization))) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.ORGANIZATION_MEMBERSHIP_LIMIT_REACHED); if (options?.organizationHooks?.beforeAcceptInvitation) await options?.organizationHooks.beforeAcceptInvitation({ invitation, user: session.user, organization }); const acceptedI = await adapter.updateInvitation({ invitationId: ctx.body.invitationId, status: "accepted", fromStatus: "pending" }); if (!acceptedI) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND); const member = await runWithTransaction(ctx.context.adapter, async () => { 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) { if (!await adapter.findTeamById({ teamId, organizationId: acceptedI.organizationId })) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND); const maximumMembersPerTeam = await resolveMaximumMembersPerTeam(ctx.context.orgOptions.teams, { teamId, organizationId: acceptedI.organizationId, session }); if (maximumMembersPerTeam !== void 0) { if ((await adapter.addTeamMemberWithLimit({ teamId, userId: session.user.id, maximumMembersPerTeam })).status === "limitReached") throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.TEAM_MEMBER_LIMIT_REACHED); } else await adapter.findOrCreateTeamMember({ teamId, userId: session.user.id }); } if (onlyOne) { const teamId = teamIds[0]; await setSessionCookie(ctx, { session: await adapter.setActiveTeam(session.session.token, teamId, ctx), user: session.user }); } } const createdMember = await adapter.createMember({ organizationId: acceptedI.organizationId, userId: session.user.id, role: acceptedI.role, createdAt: /* @__PURE__ */ new Date() }); await adapter.setActiveOrganization(session.session.token, acceptedI.organizationId, ctx); return createdMember; }).catch(async (error) => { await adapter.updateInvitation({ invitationId: ctx.body.invitationId, status: "pending" }); throw error; }); if (options?.organizationHooks?.afterAcceptInvitation) await options?.organizationHooks.afterAcceptInvitation({ invitation: acceptedI, member, user: session.user, organization }); return ctx.json({ invitation: acceptedI, member }); }); const rejectInvitationBodySchema = z.object({ invitationId: z.string().meta({ description: "The ID of the invitation to reject" }) }); const rejectInvitation = (options) => createAuthEndpoint("/organization/reject-invitation", { method: "POST", body: rejectInvitationBodySchema, requireHeaders: true, 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: "object", nullable: true } } } } } } } } } }, 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.status !== "pending") throw APIError.from("BAD_REQUEST", { message: "Invitation not found!", code: "INVITATION_NOT_FOUND" }); if (invitation.email.toLowerCase() !== session.user.email.toLowerCase()) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION); if (shouldRequireVerifiedEmailForInvitationIdAction({ organizationOptions: ctx.context.orgOptions, advancedGenerateId: getAdvancedGenerateId(ctx.context.options.advanced), databaseGenerateId: ctx.context.options.advanced?.database?.generateId }) && !session.user.emailVerified) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION); const organization = await adapter.findOrganizationById(invitation.organizationId); if (!organization) throw APIError.from("BAD_REQUEST", 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 cancelInvitationBodySchema = z.object({ invitationId: z.string().meta({ description: "The ID of the invitation to cancel" }) }); const cancelInvitation = (options) => createAuthEndpoint("/organization/cancel-invitation", { method: "POST", body: cancelInvitationBodySchema, requireHeaders: true, use: [orgMiddleware, orgSessionMiddleware], openapi: { operationId: "cancelOrganizationInvitation", 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 APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND); const member = await adapter.findMemberByOrgId({ userId: session.user.id, organizationId: invitation.organizationId }); if (!member) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND); if (!await hasPermission({ role: member.role, options: ctx.context.orgOptions, permissions: { invitation: ["cancel"] }, organizationId: invitation.organizationId }, ctx)) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION); const organization = await adapter.findOrganizationById(invitation.organizationId); if (!organization) throw APIError.from("BAD_REQUEST", 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 getInvitationQuerySchema = z.object({ id: z.string().meta({ description: "The ID of the invitation to get" }) }); const getInvitation = (options) => createAuthEndpoint("/organization/get-invitation", { method: "GET", use: [orgMiddleware], requireHeaders: true, query: getInvitationQuerySchema, 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 APIError.fromStatus("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 APIError.fromStatus("BAD_REQUEST", { message: "Invitation not found!" }); if (invitation.email.toLowerCase() !== session.user.email.toLowerCase()) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION); if (shouldRequireVerifiedEmailForInvitationIdAction({ organizationOptions: ctx.context.orgOptions, advancedGenerateId: getAdvancedGenerateId(ctx.context.options.advanced), databaseGenerateId: ctx.context.options.advanced?.database?.generateId }) && !session.user.emailVerified) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED_FOR_INVITATION); const organization = await adapter.findOrganizationById(invitation.organizationId); if (!organization) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND); const member = await adapter.findMemberByOrgId({ userId: invitation.inviterId, organizationId: invitation.organizationId }); if (!member) throw APIError.from("BAD_REQUEST", 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 listInvitationQuerySchema = z.object({ organizationId: z.string().meta({ description: "The ID of the organization to list invitations for" }).optional() }).optional(); const listInvitations = (options) => createAuthEndpoint("/organization/list-invitations", { method: "GET", requireHeaders: true, use: [orgMiddleware, orgSessionMiddleware], query: listInvitationQuerySchema }, async (ctx) => { const session = await getSessionFromCtx(ctx); if (!session) throw APIError.fromStatus("UNAUTHORIZED", { message: "Not authenticated" }); const orgId = ctx.query?.organizationId || session.session.activeOrganizationId; if (!orgId) throw APIError.fromStatus("BAD_REQUEST", { message: "Organization ID is required" }); const adapter = getOrgAdapter(ctx.context, options); if (!await adapter.findMemberByOrgId({ userId: session.user.id, organizationId: orgId })) throw APIError.fromStatus("FORBIDDEN", { message: "You are not a member of this organization" }); const invitations = await adapter.listInvitations({ organizationId: orgId }); return ctx.json(invitations); }); /** * List all invitations a user has received */ const listUserInvitations = (options) => createAuthEndpoint("/organization/list-user-invitations", { method: "GET", use: [orgMiddleware], query: z.object({ email: z.string().meta({ description: "The email of the user to list invitations for. This only works for server side API calls." }).optional() }).optional(), metadata: { openapi: { description: "List all invitations a user has received", responses: { "200": { description: "Success", content: { "application/json": { schema: { type: "array", items: { type: "object", properties: { id: { type: "string" }, email: { type: "string" }, role: { type: "string" }, organizationId: { type: "string" }, organizationName: { type: "string" }, inviterId: { type: "string", description: "The ID of the user who created the invitation" }, teamId: { type: "string", description: "The ID of the team associated with the invitation", nullable: true }, status: { type: "string" }, expiresAt: { type: "string" }, createdAt: { type: "string" } }, required: [ "id", "email", "role", "organizationId", "organizationName", "inviterId", "status", "expiresAt", "createdAt" ] } } } } } } } } }, async (ctx) => { const session = await getSessionFromCtx(ctx); if (ctx.request && ctx.query?.email) throw APIError.fromStatus("BAD_REQUEST", { message: "User email cannot be passed for client side API calls." }); if (session && !session.user.emailVerified) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED_FOR_INVITATION); const userEmail = session?.user.email || ctx.query?.email; if (!userEmail) throw APIError.fromStatus("BAD_REQUEST", { message: "Missing session headers, or email query parameter." }); const pendingInvitations = (await getOrgAdapter(ctx.context, options).listUserInvitations(userEmail)).filter((inv) => inv.status === "pending"); return ctx.json(pendingInvitations); }); //#endregion export { acceptInvitation, cancelInvitation, createInvitation, getInvitation, listInvitations, listUserInvitations, rejectInvitation };