better-auth
Version:
The most comprehensive authentication framework for TypeScript.
502 lines (501 loc) • 23.5 kB
JavaScript
import { toZodSchema } from "../../../db/to-zod.mjs";
import { getSessionFromCtx, sessionMiddleware } 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 { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
import { whereOperators } from "@better-auth/core/db/adapter";
import { createAuthEndpoint } from "@better-auth/core/api";
import * as z from "zod";
//#region src/plugins/organization/routes/crud-members.ts
const baseMemberSchema = z.object({
userId: z.coerce.string().meta({ description: "The user Id which represents the user to be added as a member. If `null` is provided, then it's expected to provide session headers. Eg: \"user-id\"" }),
role: z.union([z.string(), z.array(z.string())]).meta({ description: "The role(s) to assign to the new member. Eg: [\"admin\", \"sale\"]" }),
organizationId: z.string().meta({ description: "An optional organization ID to pass. If not provided, will default to the user's active organization. Eg: \"org-id\"" }).optional(),
teamId: z.string().meta({ description: "An optional team ID to add the member to. Eg: \"team-id\"" }).optional()
});
const addMember = (option) => {
const additionalFieldsSchema = toZodSchema({
fields: option?.schema?.member?.additionalFields || {},
isClientSide: true
});
return createAuthEndpoint.serverOnly({
method: "POST",
body: z.object({
...baseMemberSchema.shape,
...additionalFieldsSchema.shape
}),
use: [orgMiddleware],
metadata: {
$Infer: { body: {} },
openapi: {
operationId: "addOrganizationMember",
description: "Add a member to an organization"
}
}
}, async (ctx) => {
const session = ctx.body.userId ? await getSessionFromCtx(ctx).catch((e) => null) : null;
const orgId = ctx.body.organizationId || session?.session.activeOrganizationId;
if (!orgId) throw APIError.from("BAD_REQUEST", 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 APIError.fromStatus("BAD_REQUEST", { message: "Teams are not enabled" });
}
const adapter = getOrgAdapter(ctx.context, option);
const user = await ctx.context.internalAdapter.findUserById(ctx.body.userId);
if (!user) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.USER_NOT_FOUND);
if (await adapter.findMemberByEmail({
email: user.email,
organizationId: orgId
})) throw APIError.from("BAD_REQUEST", 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 APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND);
}
const membershipLimit = ctx.context.orgOptions?.membershipLimit || 100;
const count = await adapter.countMembers({ organizationId: orgId });
const organization = await adapter.findOrganizationById(orgId);
if (!organization) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND);
if (count >= (typeof membershipLimit === "number" ? membershipLimit : await membershipLimit(user, organization))) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.ORGANIZATION_MEMBERSHIP_LIMIT_REACHED);
const { role: _, userId: __, organizationId: ___, ...additionalFields } = ctx.body;
let memberData = {
organizationId: orgId,
userId: user.id,
role: parseRoles(ctx.body.role),
createdAt: /* @__PURE__ */ new Date(),
...additionalFields ? additionalFields : {}
};
if (option?.organizationHooks?.beforeAddMember) {
const response = await option?.organizationHooks.beforeAddMember({
member: {
userId: user.id,
organizationId: orgId,
role: parseRoles(ctx.body.role),
...additionalFields
},
user,
organization
});
if (response && typeof response === "object" && "data" in response) memberData = {
...memberData,
...response.data
};
}
const maximumMembersPerTeam = teamId ? await resolveMaximumMembersPerTeam(ctx.context.orgOptions.teams, {
teamId,
organizationId: orgId,
session
}) : void 0;
if (teamId) if (maximumMembersPerTeam !== void 0) {
if ((await adapter.addTeamMemberWithLimit({
teamId,
userId: user.id,
maximumMembersPerTeam
})).status === "limitReached") throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.TEAM_MEMBER_LIMIT_REACHED);
} else await adapter.findOrCreateTeamMember({
userId: user.id,
teamId
});
const createdMember = await adapter.createMember(memberData);
if (option?.organizationHooks?.afterAddMember) await option?.organizationHooks.afterAddMember({
member: createdMember,
user,
organization
});
return ctx.json(createdMember);
});
};
const removeMemberBodySchema = z.object({
memberIdOrEmail: z.string().meta({ description: "The ID or email of the member to remove" }),
/**
* If not provided, the active organization will be used
*/
organizationId: z.string().meta({ description: "The ID of the organization to remove the member from. If not provided, the active organization will be used. Eg: \"org-id\"" }).optional()
});
const removeMember = (options) => createAuthEndpoint("/organization/remove-member", {
method: "POST",
body: removeMemberBodySchema,
requireHeaders: true,
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) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION);
const adapter = getOrgAdapter(ctx.context, options);
const member = await adapter.findMemberByOrgId({
userId: session.user.id,
organizationId
});
if (!member) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND);
let toBeRemovedMember = null;
if (ctx.body.memberIdOrEmail.includes("@")) toBeRemovedMember = await adapter.findMemberByEmail({
email: ctx.body.memberIdOrEmail,
organizationId
});
else {
const result = await adapter.findMemberById(ctx.body.memberIdOrEmail);
if (!result) toBeRemovedMember = null;
else {
const { user: _user, ...member } = result;
toBeRemovedMember = member;
}
}
if (!toBeRemovedMember) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND);
const roles = toBeRemovedMember.role.split(",");
const creatorRole = ctx.context.orgOptions?.creatorRole || "owner";
if (roles.includes(creatorRole)) {
if (!member.role.split(",").map((r) => r.trim()).includes(creatorRole)) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER);
const { members } = await adapter.listMembers({ organizationId });
if (members.filter((member) => {
return member.role.split(",").includes(creatorRole);
}).length <= 1) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER);
}
if (!await hasPermission({
role: member.role,
options: ctx.context.orgOptions,
permissions: { member: ["delete"] },
organizationId
}, ctx)) throw APIError.from("UNAUTHORIZED", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_DELETE_THIS_MEMBER);
if (toBeRemovedMember?.organizationId !== organizationId) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND);
const organization = await adapter.findOrganizationById(organizationId);
if (!organization) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND);
const userBeingRemoved = await ctx.context.internalAdapter.findUserById(toBeRemovedMember.userId);
if (!userBeingRemoved) throw APIError.fromStatus("BAD_REQUEST", { message: "User not found" });
if (options?.organizationHooks?.beforeRemoveMember) await options?.organizationHooks.beforeRemoveMember({
member: toBeRemovedMember,
user: userBeingRemoved,
organization
});
await adapter.deleteMember({
memberId: toBeRemovedMember.id,
organizationId,
userId: toBeRemovedMember.userId
});
if (session.user.id === toBeRemovedMember.userId && session.session.activeOrganizationId === toBeRemovedMember.organizationId) await adapter.setActiveOrganization(session.session.token, null, ctx);
if (options?.organizationHooks?.afterRemoveMember) await options?.organizationHooks.afterRemoveMember({
member: toBeRemovedMember,
user: userBeingRemoved,
organization
});
return ctx.json({ member: toBeRemovedMember });
});
const updateMemberRoleBodySchema = z.object({
role: z.union([z.string(), z.array(z.string())]).meta({ description: "The new role to be applied. This can be a string or array of strings representing the roles. Eg: [\"admin\", \"sale\"]" }),
memberId: z.string().meta({ description: "The member id to apply the role update to. Eg: \"member-id\"" }),
organizationId: z.string().meta({ description: "An optional organization ID which the member is a part of to apply the role update. If not provided, you must provide session headers to get the active organization. Eg: \"organization-id\"" }).optional()
});
const updateMemberRole = (option) => createAuthEndpoint("/organization/update-member-role", {
method: "POST",
body: updateMemberRoleBodySchema,
use: [orgMiddleware, orgSessionMiddleware],
requireHeaders: true,
metadata: {
$Infer: { body: {} },
openapi: {
operationId: "updateOrganizationMemberRole",
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 APIError.fromStatus("BAD_REQUEST");
const organizationId = ctx.body.organizationId || session.session.activeOrganizationId;
if (!organizationId) throw APIError.from("BAD_REQUEST", 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]).flatMap((role) => role.split(",")).map((role) => role.trim()).filter(Boolean);
if (roleToSet.length === 0) throw APIError.fromStatus("BAD_REQUEST");
const validStaticRoles = new Set([...Object.keys(defaultRoles), ...Object.keys(ctx.context.orgOptions.roles || {})]);
const unknownRoles = roleToSet.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", {
code: ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND.code,
message: `${ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND.code}: ${stillInvalid.join(", ")}`
});
} else throw new APIError("BAD_REQUEST", {
code: ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND.code,
message: `${ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND.code}: ${unknownRoles.join(", ")}`
});
const member = await adapter.findMemberByOrgId({
userId: session.user.id,
organizationId
});
if (!member) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND);
const toBeUpdatedMember = member.id !== ctx.body.memberId ? await adapter.findMemberById(ctx.body.memberId) : member;
if (!toBeUpdatedMember) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND);
if (!(toBeUpdatedMember.organizationId === organizationId)) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER);
const creatorRole = ctx.context.orgOptions?.creatorRole || "owner";
const updatingMemberRoles = member.role.split(",");
const isUpdatingCreator = toBeUpdatedMember.role.split(",").includes(creatorRole);
const updaterIsCreator = updatingMemberRoles.includes(creatorRole);
const isSettingCreatorRole = roleToSet.includes(creatorRole);
const memberIsUpdatingThemselves = member.id === toBeUpdatedMember.id;
if (isUpdatingCreator && !updaterIsCreator || isSettingCreatorRole && !updaterIsCreator) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER);
if (updaterIsCreator && memberIsUpdatingThemselves) {
if ((await ctx.context.adapter.findMany({
model: "member",
where: [{
field: "organizationId",
value: organizationId
}]
})).filter((member) => {
return member.role.split(",").includes(creatorRole);
}).length <= 1 && !isSettingCreatorRole) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER);
}
if (!await hasPermission({
role: member.role,
options: ctx.context.orgOptions,
permissions: { member: ["update"] },
allowCreatorAllPermissions: true,
organizationId
}, ctx)) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER);
const organization = await adapter.findOrganizationById(organizationId);
if (!organization) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND);
const userBeingUpdated = await ctx.context.internalAdapter.findUserById(toBeUpdatedMember.userId);
if (!userBeingUpdated) throw APIError.fromStatus("BAD_REQUEST", { message: "User not found" });
const previousRole = toBeUpdatedMember.role;
const newRole = parseRoles(roleToSet);
if (option?.organizationHooks?.beforeUpdateMemberRole) {
const response = await option?.organizationHooks.beforeUpdateMemberRole({
member: toBeUpdatedMember,
newRole,
user: userBeingUpdated,
organization
});
if (response && typeof response === "object" && "data" in response) {
const updatedMember = await adapter.updateMember(ctx.body.memberId, response.data.role || newRole);
if (!updatedMember) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND);
if (option?.organizationHooks?.afterUpdateMemberRole) await option?.organizationHooks.afterUpdateMemberRole({
member: updatedMember,
previousRole,
user: userBeingUpdated,
organization
});
return ctx.json(updatedMember);
}
}
const updatedMember = await adapter.updateMember(ctx.body.memberId, newRole);
if (!updatedMember) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND);
if (option?.organizationHooks?.afterUpdateMemberRole) await option?.organizationHooks.afterUpdateMemberRole({
member: updatedMember,
previousRole,
user: userBeingUpdated,
organization
});
return ctx.json(updatedMember);
});
const getActiveMember = (options) => createAuthEndpoint("/organization/get-active-member", {
method: "GET",
use: [orgMiddleware, orgSessionMiddleware],
requireHeaders: true,
metadata: { openapi: {
description: "Get the member details of the active 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) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION);
const member = await getOrgAdapter(ctx.context, options).findMemberByOrgId({
userId: session.user.id,
organizationId
});
if (!member) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND);
return ctx.json(member);
});
const leaveOrganizationBodySchema = z.object({ organizationId: z.string().meta({ description: "The organization Id for the member to leave. Eg: \"organization-id\"" }) });
const leaveOrganization = (options) => createAuthEndpoint("/organization/leave", {
method: "POST",
body: leaveOrganizationBodySchema,
requireHeaders: true,
use: [sessionMiddleware, orgMiddleware]
}, async (ctx) => {
const session = ctx.context.session;
const adapter = getOrgAdapter(ctx.context, options);
const member = await adapter.findMemberByOrgId({
userId: session.user.id,
organizationId: ctx.body.organizationId
});
if (!member) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND);
const creatorRole = ctx.context.orgOptions?.creatorRole || "owner";
if (member.role.split(",").includes(creatorRole)) {
if ((await ctx.context.adapter.findMany({
model: "member",
where: [{
field: "organizationId",
value: ctx.body.organizationId
}]
})).filter((member) => member.role.split(",").includes(creatorRole)).length <= 1) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER);
}
await adapter.deleteMember({
memberId: member.id,
organizationId: ctx.body.organizationId,
userId: session.user.id
});
if (session.session.activeOrganizationId === ctx.body.organizationId) await adapter.setActiveOrganization(session.session.token, null, ctx);
return ctx.json(member);
});
const listMembers = (options) => createAuthEndpoint("/organization/list-members", {
method: "GET",
query: z.object({
limit: z.string().meta({ description: "The number of users to return" }).or(z.number()).optional(),
offset: z.string().meta({ description: "The offset to start from" }).or(z.number()).optional(),
sortBy: z.string().meta({ description: "The field to sort by" }).optional(),
sortDirection: z.enum(["asc", "desc"]).meta({ description: "The direction to sort by" }).optional(),
filterField: z.string().meta({ description: "The field to filter by" }).optional(),
filterValue: z.string().meta({ description: "The value to filter by" }).or(z.number()).or(z.boolean()).or(z.array(z.string())).or(z.array(z.number())).optional(),
filterOperator: z.enum(whereOperators).meta({ description: "The operator to use for the filter" }).optional(),
organizationId: z.string().meta({ description: "The organization ID to list members for. If not provided, will default to the user's active organization. Eg: \"organization-id\"" }).optional(),
organizationSlug: z.string().meta({ description: "The organization slug to list members for. If not provided, will default to the user's active organization. Eg: \"organization-slug\"" }).optional()
}).optional(),
requireHeaders: true,
use: [orgMiddleware, orgSessionMiddleware]
}, async (ctx) => {
const session = ctx.context.session;
let organizationId = ctx.query?.organizationId || session.session.activeOrganizationId;
const adapter = getOrgAdapter(ctx.context, options);
if (ctx.query?.organizationSlug) {
const organization = await adapter.findOrganizationBySlug(ctx.query?.organizationSlug);
if (!organization) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND);
organizationId = organization.id;
}
if (!organizationId) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION);
if (!await adapter.findMemberByOrgId({
userId: session.user.id,
organizationId
})) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION);
const { members, total } = await adapter.listMembers({
organizationId,
limit: ctx.query?.limit ? Number(ctx.query.limit) : void 0,
offset: ctx.query?.offset ? Number(ctx.query.offset) : void 0,
sortBy: ctx.query?.sortBy,
sortOrder: ctx.query?.sortDirection,
filter: ctx.query?.filterField ? {
field: ctx.query?.filterField,
operator: ctx.query.filterOperator,
value: ctx.query.filterValue
} : void 0
});
return ctx.json({
members,
total
});
});
const getActiveMemberRoleQuerySchema = z.object({
userId: z.string().meta({ description: "The user ID to get the role for. If not provided, will default to the current user's" }).optional(),
organizationId: z.string().meta({ description: "The organization ID to list members for. If not provided, will default to the user's active organization. Eg: \"organization-id\"" }).optional(),
organizationSlug: z.string().meta({ description: "The organization slug to list members for. If not provided, will default to the user's active organization. Eg: \"organization-slug\"" }).optional()
}).optional();
const getActiveMemberRole = (options) => createAuthEndpoint("/organization/get-active-member-role", {
method: "GET",
query: getActiveMemberRoleQuerySchema,
requireHeaders: true,
use: [orgMiddleware, orgSessionMiddleware]
}, async (ctx) => {
const session = ctx.context.session;
let organizationId = ctx.query?.organizationId || session.session.activeOrganizationId;
const adapter = getOrgAdapter(ctx.context, options);
if (ctx.query?.organizationSlug) {
const organization = await adapter.findOrganizationBySlug(ctx.query?.organizationSlug);
if (!organization) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND);
organizationId = organization.id;
}
if (!organizationId) throw APIError.from("BAD_REQUEST", ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION);
const isMember = await adapter.findMemberByOrgId({
userId: session.user.id,
organizationId
});
if (!isMember) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION);
if (!ctx.query?.userId) return ctx.json({ role: isMember.role });
const userIdToGetRole = ctx.query?.userId;
const member = await adapter.findMemberByOrgId({
userId: userIdToGetRole,
organizationId
});
if (!member) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION);
return ctx.json({ role: member?.role });
});
//#endregion
export { addMember, getActiveMember, getActiveMemberRole, leaveOrganization, listMembers, removeMember, updateMemberRole };