better-auth
Version:
The most comprehensive authentication library for TypeScript.
1,687 lines (1,682 loc) • 181 kB
JavaScript
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",