better-auth
Version:
The most comprehensive authentication library for TypeScript.
1,694 lines (1,687 loc) • 91.2 kB
JavaScript
'use strict';
const betterCall = require('better-call');
const zod = require('zod');
const account = require('./better-auth.CM7smaHY.cjs');
require('./better-auth.DiSjtgs9.cjs');
require('@better-auth/utils/base64');
require('@better-auth/utils/hmac');
require('@better-auth/utils/binary');
require('./better-auth.DcWKCjjf.cjs');
const date = require('./better-auth.C1hdVENX.cjs');
const index = require('./better-auth.ANpbi45u.cjs');
const parser = require('./better-auth.DhsGZ30Q.cjs');
require('./better-auth.GpOOav9x.cjs');
require('defu');
const hasPermission = require('./better-auth.DSVbLSt7.cjs');
const cookies_index = require('../cookies/index.cjs');
const id = require('./better-auth.Bg6iw3ig.cjs');
require('@better-auth/utils/hash');
require('@noble/ciphers/chacha');
require('@noble/ciphers/utils');
require('@noble/ciphers/webcrypto');
require('jose');
require('@noble/hashes/scrypt');
require('@better-auth/utils');
require('@better-auth/utils/hex');
require('@noble/hashes/utils');
require('./better-auth.CYeOI8C-.cjs');
const plugins_organization_access_index = require('../plugins/organization/access/index.cjs');
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 ? JSON.parse(organization.metadata) : void 0
};
},
findMemberByEmail: async (data) => {
const user = await adapter.findOne({
model: "user",
where: [
{
field: "email",
value: data.email
}
]
});
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 adapter.findMany({
model: "member",
where: [
{
field: "organizationId",
value: data.organizationId
}
],
limit: options?.membershipLimit || 100
});
return members;
},
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 ? parser.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;
},
/**
* @requires db
*/
findFullOrganization: async ({
organizationId,
isSlug,
includeTeams
}) => {
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: 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 = 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 index.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: "member",
where: [
{
field: "teamId",
value: teamId
}
],
limit: options?.membershipLimit || 100
});
return {
...team,
members
};
}
return team;
},
updateTeam: async (teamId, data) => {
const team = await adapter.update({
model: "team",
where: [
{
field: "id",
value: teamId
}
],
update: {
...data
}
});
return team;
},
deleteTeam: async (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 = date.getDate(expiresIn);
const invitation = await adapter.create({
model: "invitation",
data: {
email,
role,
organizationId,
teamId,
inviterId,
status: "pending",
expiresAt
}
});
return invitation;
},
findInvitationsByTeamId: async (teamId) => {
const invitations = await adapter.findMany({
model: "invitation",
where: [
{
field: "teamId",
value: teamId
}
]
});
return invitations;
},
createInvitation: async ({
invitation,
user
}) => {
const defaultExpiration = 60 * 60 * 48;
const expiresAt = date.getDate(
options?.invitationExpiresIn || defaultExpiration,
"sec"
);
const invite = await adapter.create({
model: "invitation",
data: {
status: "pending",
expiresAt,
inviterId: user.id,
...invitation
}
});
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
},
{
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 orgMiddleware = account.createAuthMiddleware(async (ctx) => {
return {};
});
const orgSessionMiddleware = account.createAuthMiddleware(
{
use: [account.sessionMiddleware]
},
async (ctx) => {
const session = ctx.context.session;
return {
session
};
}
);
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_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",
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 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"
};
const createInvitation = (option) => account.createAuthEndpoint(
"/organization/invite-member",
{
method: "POST",
use: [orgMiddleware, orgSessionMiddleware],
body: zod.z.object({
email: zod.z.string({
description: "The email address of the user to invite"
}),
role: zod.z.union([
zod.z.string({
description: "The role to assign to the user"
}),
zod.z.array(
zod.z.string({
description: "The roles to assign to the user"
})
)
]),
organizationId: zod.z.string({
description: "The organization ID to invite the user to"
}).optional(),
resend: zod.z.boolean({
description: "Resend the invitation email, if the user is already invited"
}).optional(),
teamId: zod.z.string({
description: "The team ID to invite the user to"
}).optional()
}),
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 betterCall.APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND
});
}
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
const member = await adapter.findMemberByOrgId({
userId: session.user.id,
organizationId
});
if (!member) {
throw new betterCall.APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND
});
}
const canInvite = hasPermission.hasPermission({
role: member.role,
options: ctx.context.orgOptions,
permissions: {
invitation: ["create"]
}
});
if (!canInvite) {
throw new betterCall.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 betterCall.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 betterCall.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 betterCall.APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION
});
}
if (alreadyInvited.length && ctx.context.orgOptions.cancelPendingInvitationsOnReInvite) {
await adapter.updateInvitation({
invitationId: alreadyInvited[0].id,
status: "canceled"
});
}
const organization = await adapter.findOrganizationById(organizationId);
if (!organization) {
throw new betterCall.APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND
});
}
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 betterCall.APIError("FORBIDDEN", {
message: ORGANIZATION_ERROR_CODES.INVITATION_LIMIT_REACHED
});
}
const invitation = await adapter.createInvitation({
invitation: {
role: roles,
email: ctx.body.email.toLowerCase(),
organizationId,
..."teamId" in ctx.body ? {
teamId: ctx.body.teamId
} : {}
},
user: session.user
});
await ctx.context.orgOptions.sendInvitationEmail?.(
{
id: invitation.id,
role: invitation.role,
email: invitation.email.toLowerCase(),
organization,
inviter: {
...member,
user: session.user
},
invitation
},
ctx.request
);
return ctx.json(invitation);
}
);
const acceptInvitation = account.createAuthEndpoint(
"/organization/accept-invitation",
{
method: "POST",
body: zod.z.object({
invitationId: zod.z.string({
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, ctx.context.orgOptions);
const invitation = await adapter.findInvitationById(ctx.body.invitationId);
if (!invitation || invitation.expiresAt < /* @__PURE__ */ new Date() || invitation.status !== "pending") {
throw new betterCall.APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND
});
}
if (invitation.email.toLowerCase() !== session.user.email.toLowerCase()) {
throw new betterCall.APIError("FORBIDDEN", {
message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION
});
}
const membershipLimit = ctx.context.orgOptions?.membershipLimit || 100;
const members = await adapter.listMembers({
organizationId: invitation.organizationId
});
if (members.length >= membershipLimit) {
throw new betterCall.APIError("FORBIDDEN", {
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_MEMBERSHIP_LIMIT_REACHED
});
}
const acceptedI = await adapter.updateInvitation({
invitationId: ctx.body.invitationId,
status: "accepted"
});
if (!acceptedI) {
throw new betterCall.APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.FAILED_TO_RETRIEVE_INVITATION
});
}
const member = await adapter.createMember({
organizationId: invitation.organizationId,
userId: session.user.id,
role: invitation.role,
createdAt: /* @__PURE__ */ new Date(),
..."teamId" in acceptedI ? {
teamId: acceptedI.teamId
} : {}
});
await adapter.setActiveOrganization(
session.session.token,
invitation.organizationId
);
if (!acceptedI) {
return ctx.json(null, {
status: 400,
body: {
message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND
}
});
}
return ctx.json({
invitation: acceptedI,
member
});
}
);
const rejectInvitation = account.createAuthEndpoint(
"/organization/reject-invitation",
{
method: "POST",
body: zod.z.object({
invitationId: zod.z.string({
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 betterCall.APIError("BAD_REQUEST", {
message: "Invitation not found!"
});
}
if (invitation.email.toLowerCase() !== session.user.email.toLowerCase()) {
throw new betterCall.APIError("FORBIDDEN", {
message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION
});
}
const rejectedI = await adapter.updateInvitation({
invitationId: ctx.body.invitationId,
status: "rejected"
});
return ctx.json({
invitation: rejectedI,
member: null
});
}
);
const cancelInvitation = account.createAuthEndpoint(
"/organization/cancel-invitation",
{
method: "POST",
body: zod.z.object({
invitationId: zod.z.string({
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, ctx.context.orgOptions);
const invitation = await adapter.findInvitationById(ctx.body.invitationId);
if (!invitation) {
throw new betterCall.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 betterCall.APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND
});
}
const canCancel = hasPermission.hasPermission({
role: member.role,
options: ctx.context.orgOptions,
permissions: {
invitation: ["cancel"]
}
});
if (!canCancel) {
throw new betterCall.APIError("FORBIDDEN", {
message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION
});
}
const canceledI = await adapter.updateInvitation({
invitationId: ctx.body.invitationId,
status: "canceled"
});
return ctx.json(canceledI);
}
);
const getInvitation = account.createAuthEndpoint(
"/organization/get-invitation",
{
method: "GET",
use: [orgMiddleware],
requireHeaders: true,
query: zod.z.object({
id: zod.z.string({
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 account.getSessionFromCtx(ctx);
if (!session) {
throw new betterCall.APIError("UNAUTHORIZED", {
message: "Not authenticated"
});
}
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
const invitation = await adapter.findInvitationById(ctx.query.id);
if (!invitation || invitation.status !== "pending" || invitation.expiresAt < /* @__PURE__ */ new Date()) {
throw new betterCall.APIError("BAD_REQUEST", {
message: "Invitation not found!"
});
}
if (invitation.email.toLowerCase() !== session.user.email.toLowerCase()) {
throw new betterCall.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 betterCall.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 betterCall.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 = account.createAuthEndpoint(
"/organization/list-invitations",
{
method: "GET",
use: [orgMiddleware, orgSessionMiddleware],
query: zod.z.object({
organizationId: zod.z.string({
description: "The ID of the organization to list invitations for"
}).optional()
}).optional()
},
async (ctx) => {
const session = await account.getSessionFromCtx(ctx);
if (!session) {
throw new betterCall.APIError("UNAUTHORIZED", {
message: "Not authenticated"
});
}
const orgId = ctx.query?.organizationId || session.session.activeOrganizationId;
if (!orgId) {
throw new betterCall.APIError("BAD_REQUEST", {
message: "Organization ID is required"
});
}
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
const isMember = await adapter.findMemberByOrgId({
userId: session.user.id,
organizationId: orgId
});
if (!isMember) {
throw new betterCall.APIError("FORBIDDEN", {
message: "You are not a member of this organization"
});
}
const invitations = await adapter.listInvitations({
organizationId: orgId
});
return ctx.json(invitations);
}
);
const addMember = () => account.createAuthEndpoint(
"/organization/add-member",
{
method: "POST",
body: zod.z.object({
userId: zod.z.coerce.string(),
role: zod.z.union([zod.z.string(), zod.z.array(zod.z.string())]),
organizationId: zod.z.string().optional()
}),
use: [orgMiddleware],
metadata: {
SERVER_ONLY: true,
$Infer: {
body: {}
}
}
},
async (ctx) => {
const session = ctx.body.userId ? await account.getSessionFromCtx(ctx).catch((e) => null) : null;
const orgId = ctx.body.organizationId || session?.session.activeOrganizationId;
if (!orgId) {
return ctx.json(null, {
status: 400,
body: {
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION
}
});
}
const teamId = "teamId" in ctx.body ? ctx.body.teamId : void 0;
if (teamId && !ctx.context.orgOptions.teams?.enabled) {
ctx.context.logger.error("Teams are not enabled");
throw new betterCall.APIError("BAD_REQUEST", {
message: "Teams are not enabled"
});
}
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
const user = await ctx.context.internalAdapter.findUserById(
ctx.body.userId
);
if (!user) {
throw new betterCall.APIError("BAD_REQUEST", {
message: account.BASE_ERROR_CODES.USER_NOT_FOUND
});
}
const alreadyMember = await adapter.findMemberByEmail({
email: user.email,
organizationId: orgId
});
if (alreadyMember) {
throw new betterCall.APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION
});
}
if (teamId) {
const team = await adapter.findTeamById({
teamId,
organizationId: orgId
});
if (!team || team.organizationId !== orgId) {
throw new betterCall.APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND
});
}
}
const membershipLimit = ctx.context.orgOptions?.membershipLimit || 100;
const members = await adapter.listMembers({ organizationId: orgId });
if (members.length >= membershipLimit) {
throw new betterCall.APIError("FORBIDDEN", {
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_MEMBERSHIP_LIMIT_REACHED
});
}
const createdMember = await adapter.createMember({
organizationId: orgId,
userId: user.id,
role: parseRoles(ctx.body.role),
createdAt: /* @__PURE__ */ new Date(),
...teamId ? { teamId } : {}
});
return ctx.json(createdMember);
}
);
const removeMember = account.createAuthEndpoint(
"/organization/remove-member",
{
method: "POST",
body: zod.z.object({
memberIdOrEmail: zod.z.string({
description: "The ID or email of the member to remove"
}),
/**
* If not provided, the active organization will be used
*/
organizationId: zod.z.string({
description: "The ID of the organization to remove the member from. If not provided, the active organization will be used"
}).optional()
}),
use: [orgMiddleware, orgSessionMiddleware],
metadata: {
openapi: {
description: "Remove a member from an organization",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
member: {
type: "object",
properties: {
id: {
type: "string"
},
userId: {
type: "string"
},
organizationId: {
type: "string"
},
role: {
type: "string"
}
},
required: ["id", "userId", "organizationId", "role"]
}
},
required: ["member"]
}
}
}
}
}
}
}
},
async (ctx) => {
const session = ctx.context.session;
const organizationId = ctx.body.organizationId || session.session.activeOrganizationId;
if (!organizationId) {
return ctx.json(null, {
status: 400,
body: {
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION
}
});
}
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
const member = await adapter.findMemberByOrgId({
userId: session.user.id,
organizationId
});
if (!member) {
throw new betterCall.APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND
});
}
let toBeRemovedMember = null;
if (ctx.body.memberIdOrEmail.includes("@")) {
toBeRemovedMember = await adapter.findMemberByEmail({
email: ctx.body.memberIdOrEmail,
organizationId
});
} else {
toBeRemovedMember = await adapter.findMemberById(
ctx.body.memberIdOrEmail
);
}
if (!toBeRemovedMember) {
throw new betterCall.APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND
});
}
const roles = toBeRemovedMember.role.split(",");
const creatorRole = ctx.context.orgOptions?.creatorRole || "owner";
const isOwner = roles.includes(creatorRole);
if (isOwner) {
if (member.role !== creatorRole) {
throw new betterCall.APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER
});
}
const members = await adapter.listMembers({
organizationId
});
const owners = members.filter((member2) => {
const roles2 = member2.role.split(",");
return roles2.includes(creatorRole);
});
if (owners.length <= 1) {
throw new betterCall.APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER
});
}
}
const canDeleteMember = hasPermission.hasPermission({
role: member.role,
options: ctx.context.orgOptions,
permissions: {
member: ["delete"]
}
});
if (!canDeleteMember) {
throw new betterCall.APIError("UNAUTHORIZED", {
message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER
});
}
if (toBeRemovedMember?.organizationId !== organizationId) {
throw new betterCall.APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND
});
}
await adapter.deleteMember(toBeRemovedMember.id);
if (session.user.id === toBeRemovedMember.userId && session.session.activeOrganizationId === toBeRemovedMember.organizationId) {
await adapter.setActiveOrganization(session.session.token, null);
}
return ctx.json({
member: toBeRemovedMember
});
}
);
const updateMemberRole = (option) => account.createAuthEndpoint(
"/organization/update-member-role",
{
method: "POST",
body: zod.z.object({
role: zod.z.union([zod.z.string(), zod.z.array(zod.z.string())]),
memberId: zod.z.string(),
organizationId: zod.z.string().optional()
}),
use: [orgMiddleware, orgSessionMiddleware],
metadata: {
$Infer: {
body: {}
},
openapi: {
description: "Update the role of a member in an organization",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
member: {
type: "object",
properties: {
id: {
type: "string"
},
userId: {
type: "string"
},
organizationId: {
type: "string"
},
role: {
type: "string"
}
},
required: ["id", "userId", "organizationId", "role"]
}
},
required: ["member"]
}
}
}
}
}
}
}
},
async (ctx) => {
const session = ctx.context.session;
if (!ctx.body.role) {
throw new betterCall.APIError("BAD_REQUEST");
}
const organizationId = ctx.body.organizationId || session.session.activeOrganizationId;
if (!organizationId) {
throw new betterCall.APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION
});
}
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
const roleToSet = Array.isArray(ctx.body.role) ? ctx.body.role : ctx.body.role ? [ctx.body.role] : [];
const member = await adapter.findMemberByOrgId({
userId: session.user.id,
organizationId
});
if (!member) {
throw new betterCall.APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND
});
}
const toBeUpdatedMember = member.id !== ctx.body.memberId ? await adapter.findMemberById(ctx.body.memberId) : member;
if (!toBeUpdatedMember) {
throw new betterCall.APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND
});
}
const toBeUpdatedMemberRoles = toBeUpdatedMember.role.split(",");
const updatingMemberRoles = member.role.split(",");
const creatorRole = ctx.context.orgOptions?.creatorRole || "owner";
if (toBeUpdatedMemberRoles.includes(creatorRole) && !updatingMemberRoles.includes(creatorRole) || roleToSet.includes(creatorRole) && !updatingMemberRoles.includes(creatorRole)) {
throw new betterCall.APIError("FORBIDDEN", {
message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER
});
}
const canUpdateMember = hasPermission.hasPermission({
role: member.role,
options: ctx.context.orgOptions,
permissions: {
member: ["update"]
}
});
if (!canUpdateMember) {
throw new betterCall.APIError("FORBIDDEN", {
message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER
});
}
const updatedMember = await adapter.updateMember(
ctx.body.memberId,
parseRoles(ctx.body.role)
);
if (!updatedMember) {
throw new betterCall.APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND
});
}
return ctx.json(updatedMember);
}
);
const getActiveMember = account.createAuthEndpoint(
"/organization/get-active-member",
{
method: "GET",
use: [orgMiddleware, orgSessionMiddleware],
metadata: {
openapi: {
description: "Get the active member in the organization",
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: {
type: "object",
properties: {
id: {
type: "string"
},
userId: {
type: "string"
},
organizationId: {
type: "string"
},
role: {
type: "string"
}
},
required: ["id", "userId", "organizationId", "role"]
}
}
}
}
}
}
}
},
async (ctx) => {
const session = ctx.context.session;
const organizationId = session.session.activeOrganizationId;
if (!organizationId) {
return ctx.json(null, {
status: 400,
body: {
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION
}
});
}
const adapter = getOrgAdapter(ctx.context, ctx.context.orgOptions);
const member = await adapter.findMemberByOrgId({
userId: session.user.id,
organizationId
});
if (!member) {
return ctx.json(null, {
status: 400,
body: {
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND
}
});
}
return ctx.json(member);
}
);
const leaveOrganization = account.createAuthEndpoint(
"/organization/leave",
{
method: "POST",
body: zod.z.object({
organizationId: zod.z.string()
}),
use: [account.sessionMiddleware, orgMiddleware]
},
async (ctx) => {
const session = ctx.context.session;
const adapter = getOrgAdapter(ctx.context);
const member = await adapter.findMemberByOrgId({
userId: session.user.id,
organizationId: ctx.body.organizationId
});
if (!member) {
throw new betterCall.APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND
});
}
const isOwnerLeaving = member.role === (ctx.context.orgOptions?.creatorRole || "owner");
if (isOwnerLeaving) {
const members = await ctx.context.adapter.findMany({
model: "member",
where: [
{
field: "organizationId",
value: ctx.body.organizationId
}
]
});
const owners = members.filter(
(member2) => member2.role === (ctx.context.orgOptions?.creatorRole || "owner")
);
if (owners.length <= 1) {
throw new betterCall.APIError("BAD_REQUEST", {
message: ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER
});
}
}
await adapter.deleteMember(member.id);
if (session.session.activeOrganizationId === ctx.body.organizationId) {
await adapter.setActiveOrganization(session.sessio