mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-06 12:27:44 +00:00
feat(organization): organization life cycle hooks (#4049)
Co-authored-by: Maxwell <145994855+ping-maxwell@users.noreply.github.com>
This commit is contained in:
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -24,6 +24,6 @@
|
|||||||
"editor.defaultFormatter": "biomejs.biome"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
},
|
},
|
||||||
"[mdx]": {
|
"[mdx]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "unifiedjs.vscode-mdx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -15,3 +15,4 @@ export type * from "./oauth2/types";
|
|||||||
export { createTelemetry } from "./telemetry";
|
export { createTelemetry } from "./telemetry";
|
||||||
export { getTelemetryAuthConfig } from "./telemetry/detectors/detect-auth-config";
|
export { getTelemetryAuthConfig } from "./telemetry/detectors/detect-auth-config";
|
||||||
export type { TelemetryEvent } from "./telemetry/types";
|
export type { TelemetryEvent } from "./telemetry/types";
|
||||||
|
export { APIError } from "./api";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, expect, expectTypeOf, it, vi } from "vitest";
|
import { describe, expect, expectTypeOf, it } from "vitest";
|
||||||
import { getTestInstance } from "../../test-utils/test-instance";
|
import { getTestInstance } from "../../test-utils/test-instance";
|
||||||
import { organization } from "./organization";
|
import { organization } from "./organization";
|
||||||
import { createAuthClient } from "../../client";
|
import { createAuthClient } from "../../client";
|
||||||
@@ -15,7 +15,6 @@ import { ownerAc } from "./access";
|
|||||||
import { nextCookies } from "../../integrations/next-js";
|
import { nextCookies } from "../../integrations/next-js";
|
||||||
|
|
||||||
describe("organization", async (it) => {
|
describe("organization", async (it) => {
|
||||||
const onInvitationAccepted = vi.fn();
|
|
||||||
const { auth, signInWithTestUser, signInWithUser, cookieSetter } =
|
const { auth, signInWithTestUser, signInWithUser, cookieSetter } =
|
||||||
await getTestInstance({
|
await getTestInstance({
|
||||||
user: {
|
user: {
|
||||||
@@ -37,7 +36,6 @@ describe("organization", async (it) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
invitationLimit: 3,
|
invitationLimit: 3,
|
||||||
onInvitationAccepted,
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
logger: {
|
logger: {
|
||||||
@@ -295,94 +293,7 @@ describe("organization", async (it) => {
|
|||||||
organizationId,
|
organizationId,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it("should call onInvitationAccepted callback when invitation is accepted", async () => {
|
|
||||||
onInvitationAccepted.mockClear();
|
|
||||||
|
|
||||||
const testOrg = await client.organization.create({
|
|
||||||
name: "Test Org for Callback",
|
|
||||||
slug: `test-org-callback-${Math.random().toString(36).substring(7)}`,
|
|
||||||
metadata: {
|
|
||||||
test: "test",
|
|
||||||
},
|
|
||||||
fetchOptions: {
|
|
||||||
headers,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!testOrg.data) {
|
|
||||||
throw new Error("Failed to create test organization");
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueId = Math.random().toString(36).substring(7);
|
|
||||||
const newUser = {
|
|
||||||
email: `test-accept-${uniqueId}@example.com`,
|
|
||||||
password: "password123",
|
|
||||||
name: "Test Accept User",
|
|
||||||
};
|
|
||||||
|
|
||||||
await client.signUp.email({
|
|
||||||
email: newUser.email,
|
|
||||||
password: newUser.password,
|
|
||||||
name: newUser.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { headers: newUserHeaders } = await signInWithUser(
|
|
||||||
newUser.email,
|
|
||||||
newUser.password,
|
|
||||||
);
|
|
||||||
|
|
||||||
const invite = await client.organization.inviteMember({
|
|
||||||
organizationId: testOrg.data.id,
|
|
||||||
email: newUser.email,
|
|
||||||
role: "member",
|
|
||||||
fetchOptions: {
|
|
||||||
headers,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!invite.data) {
|
|
||||||
console.error("Invitation creation failed:", invite);
|
|
||||||
throw new Error("Invitation not created");
|
|
||||||
}
|
|
||||||
expect(invite.data.role).toBe("member");
|
|
||||||
|
|
||||||
const accept = await client.organization.acceptInvitation({
|
|
||||||
invitationId: invite.data.id,
|
|
||||||
fetchOptions: {
|
|
||||||
headers: newUserHeaders,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(accept.data?.invitation.status).toBe("accepted");
|
|
||||||
|
|
||||||
expect(onInvitationAccepted).toHaveBeenCalledTimes(1);
|
|
||||||
expect(onInvitationAccepted).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
id: invite.data.id,
|
|
||||||
role: "member",
|
|
||||||
organization: expect.objectContaining({
|
|
||||||
id: testOrg.data.id,
|
|
||||||
name: "Test Org for Callback",
|
|
||||||
}),
|
|
||||||
invitation: expect.objectContaining({
|
|
||||||
id: invite.data.id,
|
|
||||||
status: expect.any(String),
|
|
||||||
}),
|
|
||||||
inviter: expect.objectContaining({
|
|
||||||
user: expect.objectContaining({
|
|
||||||
email: expect.any(String),
|
|
||||||
name: expect.any(String),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
acceptedUser: expect.objectContaining({
|
|
||||||
id: expect.any(String),
|
|
||||||
email: newUser.email,
|
|
||||||
name: newUser.name,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
expect.any(Object),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it("should create invitation with multiple roles", async () => {
|
it("should create invitation with multiple roles", async () => {
|
||||||
const invite = await client.organization.inviteMember({
|
const invite = await client.organization.inviteMember({
|
||||||
organizationId: organizationId,
|
organizationId: organizationId,
|
||||||
@@ -1996,3 +1907,79 @@ describe("Additional Fields", async () => {
|
|||||||
expect(row.teamRequiredField).toBe("hey4");
|
expect(row.teamRequiredField).toBe("hey4");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("organization hooks", async (it) => {
|
||||||
|
let hooksCalled: string[] = [];
|
||||||
|
|
||||||
|
const { auth, signInWithTestUser } = await getTestInstance({
|
||||||
|
plugins: [
|
||||||
|
organization({
|
||||||
|
organizationHooks: {
|
||||||
|
beforeCreateOrganization: async (data) => {
|
||||||
|
hooksCalled.push("beforeCreateOrganization");
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
...data.organization,
|
||||||
|
metadata: { hookCalled: true },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
afterCreateOrganization: async (data) => {
|
||||||
|
hooksCalled.push("afterCreateOrganization");
|
||||||
|
},
|
||||||
|
beforeCreateInvitation: async (data) => {
|
||||||
|
hooksCalled.push("beforeCreateInvitation");
|
||||||
|
},
|
||||||
|
afterCreateInvitation: async (data) => {
|
||||||
|
hooksCalled.push("afterCreateInvitation");
|
||||||
|
},
|
||||||
|
beforeAddMember: async (data) => {
|
||||||
|
hooksCalled.push("beforeAddMember");
|
||||||
|
},
|
||||||
|
afterAddMember: async (data) => {
|
||||||
|
hooksCalled.push("afterAddMember");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async sendInvitationEmail() {},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = createAuthClient({
|
||||||
|
plugins: [organizationClient()],
|
||||||
|
baseURL: "http://localhost:3000/api/auth",
|
||||||
|
fetchOptions: {
|
||||||
|
customFetchImpl: async (url, init) => {
|
||||||
|
return auth.handler(new Request(url, init));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { headers } = await signInWithTestUser();
|
||||||
|
|
||||||
|
it("should call organization creation hooks", async () => {
|
||||||
|
hooksCalled = [];
|
||||||
|
const organization = await client.organization.create({
|
||||||
|
name: "Test Org with Hooks",
|
||||||
|
slug: "test-org-hooks",
|
||||||
|
fetchOptions: { headers },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(hooksCalled).toContain("beforeCreateOrganization");
|
||||||
|
expect(hooksCalled).toContain("afterCreateOrganization");
|
||||||
|
expect(organization.data?.metadata).toEqual({ hookCalled: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call invitation hooks", async () => {
|
||||||
|
hooksCalled = [];
|
||||||
|
|
||||||
|
await client.organization.inviteMember({
|
||||||
|
email: "test@example.com",
|
||||||
|
role: "member",
|
||||||
|
fetchOptions: { headers },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(hooksCalled).toContain("beforeCreateInvitation");
|
||||||
|
expect(hooksCalled).toContain("afterCreateInvitation");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -378,14 +378,37 @@ export const createInvitation = <O extends OrganizationOptions>(option: O) => {
|
|||||||
...additionalFields
|
...additionalFields
|
||||||
} = ctx.body;
|
} = ctx.body;
|
||||||
|
|
||||||
|
let invitationData = {
|
||||||
|
role: roles,
|
||||||
|
email: ctx.body.email.toLowerCase(),
|
||||||
|
organizationId: organizationId,
|
||||||
|
teamIds,
|
||||||
|
...(additionalFields ? additionalFields : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run beforeCreateInvitation hook
|
||||||
|
if (option?.organizationHooks?.beforeCreateInvitation) {
|
||||||
|
const response = await option?.organizationHooks.beforeCreateInvitation(
|
||||||
|
{
|
||||||
|
invitation: {
|
||||||
|
...invitationData,
|
||||||
|
inviterId: session.user.id,
|
||||||
|
teamId: teamIds.length > 0 ? teamIds[0] : undefined,
|
||||||
|
},
|
||||||
|
inviter: session.user,
|
||||||
|
organization,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (response && typeof response === "object" && "data" in response) {
|
||||||
|
invitationData = {
|
||||||
|
...invitationData,
|
||||||
|
...response.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const invitation = await adapter.createInvitation({
|
const invitation = await adapter.createInvitation({
|
||||||
invitation: {
|
invitation: invitationData,
|
||||||
role: roles,
|
|
||||||
email: ctx.body.email.toLowerCase(),
|
|
||||||
organizationId: organizationId,
|
|
||||||
teamIds,
|
|
||||||
...(additionalFields ? additionalFields : {}),
|
|
||||||
},
|
|
||||||
user: session.user,
|
user: session.user,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -405,6 +428,15 @@ export const createInvitation = <O extends OrganizationOptions>(option: O) => {
|
|||||||
ctx.request,
|
ctx.request,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Run afterCreateInvitation hook
|
||||||
|
if (option?.organizationHooks?.afterCreateInvitation) {
|
||||||
|
await option?.organizationHooks.afterCreateInvitation({
|
||||||
|
invitation: invitation as unknown as Invitation,
|
||||||
|
inviter: session.user,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return ctx.json(invitation);
|
return ctx.json(invitation);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -492,6 +524,25 @@ export const acceptInvitation = <O extends OrganizationOptions>(options: O) =>
|
|||||||
ORGANIZATION_ERROR_CODES.ORGANIZATION_MEMBERSHIP_LIMIT_REACHED,
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run beforeAcceptInvitation hook
|
||||||
|
if (options?.organizationHooks?.beforeAcceptInvitation) {
|
||||||
|
await options?.organizationHooks.beforeAcceptInvitation({
|
||||||
|
invitation: invitation as unknown as Invitation,
|
||||||
|
user: session.user,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const acceptedI = await adapter.updateInvitation({
|
const acceptedI = await adapter.updateInvitation({
|
||||||
invitationId: ctx.body.invitationId,
|
invitationId: ctx.body.invitationId,
|
||||||
status: "accepted",
|
status: "accepted",
|
||||||
@@ -573,35 +624,13 @@ export const acceptInvitation = <O extends OrganizationOptions>(options: O) =>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (ctx.context.orgOptions.onInvitationAccepted) {
|
if (options?.organizationHooks?.afterAcceptInvitation) {
|
||||||
const organization = await adapter.findOrganizationById(
|
await options?.organizationHooks.afterAcceptInvitation({
|
||||||
invitation.organizationId,
|
invitation: acceptedI as unknown as Invitation,
|
||||||
);
|
member,
|
||||||
|
user: session.user,
|
||||||
const inviterMember = await adapter.findMemberByOrgId({
|
organization,
|
||||||
userId: invitation.inviterId,
|
|
||||||
organizationId: invitation.organizationId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const inviterUser = await ctx.context.internalAdapter.findUserById(
|
|
||||||
invitation.inviterId,
|
|
||||||
);
|
|
||||||
if (organization && inviterMember && inviterUser) {
|
|
||||||
await ctx.context.orgOptions.onInvitationAccepted(
|
|
||||||
{
|
|
||||||
id: invitation.id,
|
|
||||||
role: invitation.role as string,
|
|
||||||
organization: organization,
|
|
||||||
invitation: invitation as unknown as Invitation,
|
|
||||||
inviter: {
|
|
||||||
...inviterMember,
|
|
||||||
user: inviterUser,
|
|
||||||
},
|
|
||||||
acceptedUser: session.user,
|
|
||||||
},
|
|
||||||
ctx.request,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ctx.json({
|
return ctx.json({
|
||||||
invitation: acceptedI,
|
invitation: acceptedI,
|
||||||
@@ -679,11 +708,38 @@ export const rejectInvitation = <O extends OrganizationOptions>(options: O) =>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const organization = await adapter.findOrganizationById(
|
||||||
|
invitation.organizationId,
|
||||||
|
);
|
||||||
|
if (!organization) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run beforeRejectInvitation hook
|
||||||
|
if (options?.organizationHooks?.beforeRejectInvitation) {
|
||||||
|
await options?.organizationHooks.beforeRejectInvitation({
|
||||||
|
invitation: invitation as unknown as Invitation,
|
||||||
|
user: session.user,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const rejectedI = await adapter.updateInvitation({
|
const rejectedI = await adapter.updateInvitation({
|
||||||
invitationId: ctx.body.invitationId,
|
invitationId: ctx.body.invitationId,
|
||||||
status: "rejected",
|
status: "rejected",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Run afterRejectInvitation hook
|
||||||
|
if (options?.organizationHooks?.afterRejectInvitation) {
|
||||||
|
await options?.organizationHooks.afterRejectInvitation({
|
||||||
|
invitation: rejectedI || (invitation as unknown as Invitation),
|
||||||
|
user: session.user,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return ctx.json({
|
return ctx.json({
|
||||||
invitation: rejectedI,
|
invitation: rejectedI,
|
||||||
member: null,
|
member: null,
|
||||||
@@ -756,10 +812,39 @@ export const cancelInvitation = <O extends OrganizationOptions>(options: O) =>
|
|||||||
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION,
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run beforeCancelInvitation hook
|
||||||
|
if (options?.organizationHooks?.beforeCancelInvitation) {
|
||||||
|
await options?.organizationHooks.beforeCancelInvitation({
|
||||||
|
invitation: invitation as unknown as Invitation,
|
||||||
|
cancelledBy: session.user,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const canceledI = await adapter.updateInvitation({
|
const canceledI = await adapter.updateInvitation({
|
||||||
invitationId: ctx.body.invitationId,
|
invitationId: ctx.body.invitationId,
|
||||||
status: "canceled",
|
status: "canceled",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Run afterCancelInvitation hook
|
||||||
|
if (options?.organizationHooks?.afterCancelInvitation) {
|
||||||
|
await options?.organizationHooks.afterCancelInvitation({
|
||||||
|
invitation: (canceledI as unknown as Invitation) || invitation,
|
||||||
|
cancelledBy: session.user,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return ctx.json(canceledI);
|
return ctx.json(canceledI);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -147,13 +147,42 @@ export const addMember = <O extends OrganizationOptions>(option: O) => {
|
|||||||
...additionalFields
|
...additionalFields
|
||||||
} = ctx.body;
|
} = ctx.body;
|
||||||
|
|
||||||
const createdMember = await adapter.createMember({
|
const organization = await adapter.findOrganizationById(orgId);
|
||||||
|
if (!organization) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let memberData = {
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
role: parseRoles(ctx.body.role as string | string[]),
|
role: parseRoles(ctx.body.role as string | string[]),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
...(additionalFields ? additionalFields : {}),
|
...(additionalFields ? additionalFields : {}),
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Run beforeAddMember hook
|
||||||
|
if (option?.organizationHooks?.beforeAddMember) {
|
||||||
|
const response = await option?.organizationHooks.beforeAddMember({
|
||||||
|
member: {
|
||||||
|
userId: user.id,
|
||||||
|
organizationId: orgId,
|
||||||
|
role: parseRoles(ctx.body.role as string | string[]),
|
||||||
|
...additionalFields,
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
if (response && typeof response === "object" && "data" in response) {
|
||||||
|
memberData = {
|
||||||
|
...memberData,
|
||||||
|
...response.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdMember = await adapter.createMember(memberData);
|
||||||
|
|
||||||
if (teamId) {
|
if (teamId) {
|
||||||
await adapter.findOrCreateTeamMember({
|
await adapter.findOrCreateTeamMember({
|
||||||
@@ -162,6 +191,15 @@ export const addMember = <O extends OrganizationOptions>(option: O) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run afterAddMember hook
|
||||||
|
if (option?.organizationHooks?.afterAddMember) {
|
||||||
|
await option?.organizationHooks.afterAddMember({
|
||||||
|
member: createdMember,
|
||||||
|
user,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return ctx.json(createdMember);
|
return ctx.json(createdMember);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -308,6 +346,32 @@ export const removeMember = <O extends OrganizationOptions>(options: O) =>
|
|||||||
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
|
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const organization = await adapter.findOrganizationById(organizationId);
|
||||||
|
if (!organization) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userBeingRemoved = await ctx.context.internalAdapter.findUserById(
|
||||||
|
toBeRemovedMember.userId,
|
||||||
|
);
|
||||||
|
if (!userBeingRemoved) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: "User not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run beforeRemoveMember hook
|
||||||
|
if (options?.organizationHooks?.beforeRemoveMember) {
|
||||||
|
await options?.organizationHooks.beforeRemoveMember({
|
||||||
|
member: toBeRemovedMember,
|
||||||
|
user: userBeingRemoved,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await adapter.deleteMember(toBeRemovedMember.id);
|
await adapter.deleteMember(toBeRemovedMember.id);
|
||||||
if (
|
if (
|
||||||
session.user.id === toBeRemovedMember.userId &&
|
session.user.id === toBeRemovedMember.userId &&
|
||||||
@@ -316,6 +380,16 @@ export const removeMember = <O extends OrganizationOptions>(options: O) =>
|
|||||||
) {
|
) {
|
||||||
await adapter.setActiveOrganization(session.session.token, null);
|
await adapter.setActiveOrganization(session.session.token, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run afterRemoveMember hook
|
||||||
|
if (options?.organizationHooks?.afterRemoveMember) {
|
||||||
|
await options?.organizationHooks.afterRemoveMember({
|
||||||
|
member: toBeRemovedMember,
|
||||||
|
user: userBeingRemoved,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return ctx.json({
|
return ctx.json({
|
||||||
member: toBeRemovedMember,
|
member: toBeRemovedMember,
|
||||||
});
|
});
|
||||||
@@ -486,15 +560,82 @@ export const updateMemberRole = <O extends OrganizationOptions>(option: O) =>
|
|||||||
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER,
|
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const organization = await adapter.findOrganizationById(organizationId);
|
||||||
|
if (!organization) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userBeingUpdated = await ctx.context.internalAdapter.findUserById(
|
||||||
|
toBeUpdatedMember.userId,
|
||||||
|
);
|
||||||
|
if (!userBeingUpdated) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: "User not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousRole = toBeUpdatedMember.role;
|
||||||
|
const newRole = parseRoles(ctx.body.role as string | string[]);
|
||||||
|
|
||||||
|
// Run beforeUpdateMemberRole hook
|
||||||
|
if (option?.organizationHooks?.beforeUpdateMemberRole) {
|
||||||
|
const response = await option?.organizationHooks.beforeUpdateMemberRole(
|
||||||
|
{
|
||||||
|
member: toBeUpdatedMember,
|
||||||
|
newRole,
|
||||||
|
user: userBeingUpdated,
|
||||||
|
organization,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (response && typeof response === "object" && "data" in response) {
|
||||||
|
// Allow the hook to modify the role
|
||||||
|
const updatedMember = await adapter.updateMember(
|
||||||
|
ctx.body.memberId,
|
||||||
|
response.data.role || newRole,
|
||||||
|
);
|
||||||
|
if (!updatedMember) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run afterUpdateMemberRole hook
|
||||||
|
if (option?.organizationHooks?.afterUpdateMemberRole) {
|
||||||
|
await option?.organizationHooks.afterUpdateMemberRole({
|
||||||
|
member: updatedMember,
|
||||||
|
previousRole,
|
||||||
|
user: userBeingUpdated,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.json(updatedMember);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updatedMember = await adapter.updateMember(
|
const updatedMember = await adapter.updateMember(
|
||||||
ctx.body.memberId,
|
ctx.body.memberId,
|
||||||
parseRoles(ctx.body.role as string | string[]),
|
newRole,
|
||||||
);
|
);
|
||||||
if (!updatedMember) {
|
if (!updatedMember) {
|
||||||
throw new APIError("BAD_REQUEST", {
|
throw new APIError("BAD_REQUEST", {
|
||||||
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
|
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run afterUpdateMemberRole hook
|
||||||
|
if (option?.organizationHooks?.afterUpdateMemberRole) {
|
||||||
|
await option?.organizationHooks.afterUpdateMemberRole({
|
||||||
|
member: updatedMember,
|
||||||
|
previousRole,
|
||||||
|
user: userBeingUpdated,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return ctx.json(updatedMember);
|
return ctx.json(updatedMember);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
} from "../../../db";
|
} from "../../../db";
|
||||||
|
|
||||||
export const createOrganization = <O extends OrganizationOptions>(
|
export const createOrganization = <O extends OrganizationOptions>(
|
||||||
options: O,
|
options?: O,
|
||||||
) => {
|
) => {
|
||||||
const additionalFieldsSchema = toZodSchema({
|
const additionalFieldsSchema = toZodSchema({
|
||||||
fields: options?.schema?.organization?.additionalFields || {},
|
fields: options?.schema?.organization?.additionalFields || {},
|
||||||
@@ -162,12 +162,6 @@ export const createOrganization = <O extends OrganizationOptions>(
|
|||||||
...orgData
|
...orgData
|
||||||
} = ctx.body;
|
} = ctx.body;
|
||||||
|
|
||||||
let hookResponse:
|
|
||||||
| {
|
|
||||||
data: Record<string, any>;
|
|
||||||
}
|
|
||||||
| undefined = undefined;
|
|
||||||
|
|
||||||
if (options.organizationCreation?.beforeCreate) {
|
if (options.organizationCreation?.beforeCreate) {
|
||||||
const response = await options.organizationCreation.beforeCreate(
|
const response = await options.organizationCreation.beforeCreate(
|
||||||
{
|
{
|
||||||
@@ -180,7 +174,24 @@ export const createOrganization = <O extends OrganizationOptions>(
|
|||||||
ctx.request,
|
ctx.request,
|
||||||
);
|
);
|
||||||
if (response && typeof response === "object" && "data" in response) {
|
if (response && typeof response === "object" && "data" in response) {
|
||||||
hookResponse = response;
|
ctx.body = {
|
||||||
|
...ctx.body,
|
||||||
|
...response.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.organizationHooks?.beforeCreateOrganization) {
|
||||||
|
const response =
|
||||||
|
await options?.organizationHooks.beforeCreateOrganization({
|
||||||
|
organization: orgData,
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
if (response && typeof response === "object" && "data" in response) {
|
||||||
|
ctx.body = {
|
||||||
|
...ctx.body,
|
||||||
|
...response.data,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +199,6 @@ export const createOrganization = <O extends OrganizationOptions>(
|
|||||||
organization: {
|
organization: {
|
||||||
...orgData,
|
...orgData,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
...(hookResponse?.data || {}),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -241,6 +251,14 @@ export const createOrganization = <O extends OrganizationOptions>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options?.organizationHooks?.afterCreateOrganization) {
|
||||||
|
await options?.organizationHooks.afterCreateOrganization({
|
||||||
|
organization,
|
||||||
|
user,
|
||||||
|
member,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (ctx.context.session && !ctx.body.keepCurrentActiveOrganization) {
|
if (ctx.context.session && !ctx.body.keepCurrentActiveOrganization) {
|
||||||
await adapter.setActiveOrganization(
|
await adapter.setActiveOrganization(
|
||||||
ctx.context.session.session.token,
|
ctx.context.session.session.token,
|
||||||
@@ -297,7 +315,7 @@ export const checkOrganizationSlug = <O extends OrganizationOptions>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const updateOrganization = <O extends OrganizationOptions>(
|
export const updateOrganization = <O extends OrganizationOptions>(
|
||||||
options: O,
|
options?: O,
|
||||||
) => {
|
) => {
|
||||||
const additionalFieldsSchema = toZodSchema({
|
const additionalFieldsSchema = toZodSchema({
|
||||||
fields: options?.schema?.organization?.additionalFields || {},
|
fields: options?.schema?.organization?.additionalFields || {},
|
||||||
@@ -416,10 +434,31 @@ export const updateOrganization = <O extends OrganizationOptions>(
|
|||||||
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION,
|
ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_ORGANIZATION,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (options?.organizationHooks?.beforeUpdateOrganization) {
|
||||||
|
const response =
|
||||||
|
await options.organizationHooks.beforeUpdateOrganization({
|
||||||
|
organization: ctx.body.data,
|
||||||
|
user: session.user,
|
||||||
|
member,
|
||||||
|
});
|
||||||
|
if (response && typeof response === "object" && "data" in response) {
|
||||||
|
ctx.body.data = {
|
||||||
|
...ctx.body.data,
|
||||||
|
...response.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
const updatedOrg = await adapter.updateOrganization(
|
const updatedOrg = await adapter.updateOrganization(
|
||||||
organizationId,
|
organizationId,
|
||||||
ctx.body.data,
|
ctx.body.data,
|
||||||
);
|
);
|
||||||
|
if (options?.organizationHooks?.afterUpdateOrganization) {
|
||||||
|
await options.organizationHooks.afterUpdateOrganization({
|
||||||
|
organization: updatedOrg,
|
||||||
|
user: session.user,
|
||||||
|
member,
|
||||||
|
});
|
||||||
|
}
|
||||||
return ctx.json(updatedOrg);
|
return ctx.json(updatedOrg);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -459,6 +498,19 @@ export const deleteOrganization = <O extends OrganizationOptions>(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (ctx) => {
|
async (ctx) => {
|
||||||
|
const disableOrganizationDeletion =
|
||||||
|
ctx.context.orgOptions.organizationDeletion?.disabled ||
|
||||||
|
ctx.context.orgOptions.disableOrganizationDeletion;
|
||||||
|
if (disableOrganizationDeletion) {
|
||||||
|
if (ctx.context.orgOptions.organizationDeletion?.disabled) {
|
||||||
|
ctx.context.logger.info(
|
||||||
|
"`organizationDeletion.disabled` is deprecated. Use `disableOrganizationDeletion` instead",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new APIError("NOT_FOUND", {
|
||||||
|
message: "Organization deletion is disabled",
|
||||||
|
});
|
||||||
|
}
|
||||||
const session = await ctx.context.getSession(ctx);
|
const session = await ctx.context.getSession(ctx);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new APIError("UNAUTHORIZED", { status: 401 });
|
throw new APIError("UNAUTHORIZED", { status: 401 });
|
||||||
@@ -503,23 +555,20 @@ export const deleteOrganization = <O extends OrganizationOptions>(
|
|||||||
*/
|
*/
|
||||||
await adapter.setActiveOrganization(session.session.token, null);
|
await adapter.setActiveOrganization(session.session.token, null);
|
||||||
}
|
}
|
||||||
const option = ctx.context.orgOptions.organizationDeletion;
|
|
||||||
if (option?.disabled) {
|
|
||||||
throw new APIError("FORBIDDEN");
|
|
||||||
}
|
|
||||||
const org = await adapter.findOrganizationById(organizationId);
|
const org = await adapter.findOrganizationById(organizationId);
|
||||||
if (!org) {
|
if (!org) {
|
||||||
throw new APIError("BAD_REQUEST");
|
throw new APIError("BAD_REQUEST");
|
||||||
}
|
}
|
||||||
if (option?.beforeDelete) {
|
if (options?.organizationHooks?.beforeDeleteOrganization) {
|
||||||
await option.beforeDelete({
|
await options.organizationHooks.beforeDeleteOrganization({
|
||||||
organization: org,
|
organization: org,
|
||||||
user: session.user,
|
user: session.user,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await adapter.deleteOrganization(organizationId);
|
await adapter.deleteOrganization(organizationId);
|
||||||
if (option?.afterDelete) {
|
if (options?.organizationHooks?.afterDeleteOrganization) {
|
||||||
await option.afterDelete({
|
await options.organizationHooks.afterDeleteOrganization({
|
||||||
organization: org,
|
organization: org,
|
||||||
user: session.user,
|
user: session.user,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -155,13 +155,52 @@ export const createTeam = <O extends OrganizationOptions>(options: O) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { name, organizationId: _, ...additionalFields } = ctx.body;
|
const { name, organizationId: _, ...additionalFields } = ctx.body;
|
||||||
const createdTeam = await adapter.createTeam({
|
|
||||||
|
const organization = await adapter.findOrganizationById(organizationId);
|
||||||
|
if (!organization) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let teamData = {
|
||||||
name,
|
name,
|
||||||
organizationId,
|
organizationId,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
...additionalFields,
|
...additionalFields,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Run beforeCreateTeam hook
|
||||||
|
if (options?.organizationHooks?.beforeCreateTeam) {
|
||||||
|
const response = await options?.organizationHooks.beforeCreateTeam({
|
||||||
|
team: {
|
||||||
|
name,
|
||||||
|
organizationId,
|
||||||
|
...additionalFields,
|
||||||
|
},
|
||||||
|
user: session?.user,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
if (response && typeof response === "object" && "data" in response) {
|
||||||
|
teamData = {
|
||||||
|
...teamData,
|
||||||
|
...response.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdTeam = await adapter.createTeam(teamData);
|
||||||
|
|
||||||
|
// Run afterCreateTeam hook
|
||||||
|
if (options?.organizationHooks?.afterCreateTeam) {
|
||||||
|
await options?.organizationHooks.afterCreateTeam({
|
||||||
|
team: createdTeam,
|
||||||
|
user: session?.user,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return ctx.json(createdTeam);
|
return ctx.json(createdTeam);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -273,7 +312,33 @@ export const removeTeam = <O extends OrganizationOptions>(options: O) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const organization = await adapter.findOrganizationById(organizationId);
|
||||||
|
if (!organization) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run beforeDeleteTeam hook
|
||||||
|
if (options?.organizationHooks?.beforeDeleteTeam) {
|
||||||
|
await options?.organizationHooks.beforeDeleteTeam({
|
||||||
|
team,
|
||||||
|
user: session?.user,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await adapter.deleteTeam(team.id);
|
await adapter.deleteTeam(team.id);
|
||||||
|
|
||||||
|
// Run afterDeleteTeam hook
|
||||||
|
if (options?.organizationHooks?.afterDeleteTeam) {
|
||||||
|
await options?.organizationHooks.afterDeleteTeam({
|
||||||
|
team,
|
||||||
|
user: session?.user,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return ctx.json({ message: "Team removed successfully." });
|
return ctx.json({ message: "Team removed successfully." });
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -414,10 +479,57 @@ export const updateTeam = <O extends OrganizationOptions>(options: O) => {
|
|||||||
|
|
||||||
const { name, organizationId: __, ...additionalFields } = ctx.body.data;
|
const { name, organizationId: __, ...additionalFields } = ctx.body.data;
|
||||||
|
|
||||||
const updatedTeam = await adapter.updateTeam(team.id, {
|
const organization = await adapter.findOrganizationById(organizationId);
|
||||||
|
if (!organization) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates = {
|
||||||
name,
|
name,
|
||||||
...additionalFields,
|
...additionalFields,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Run beforeUpdateTeam hook
|
||||||
|
if (options?.organizationHooks?.beforeUpdateTeam) {
|
||||||
|
const response = await options?.organizationHooks.beforeUpdateTeam({
|
||||||
|
team,
|
||||||
|
updates,
|
||||||
|
user: session.user,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
if (response && typeof response === "object" && "data" in response) {
|
||||||
|
// Allow the hook to modify the updates
|
||||||
|
const modifiedUpdates = response.data;
|
||||||
|
const updatedTeam = await adapter.updateTeam(
|
||||||
|
team.id,
|
||||||
|
modifiedUpdates,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Run afterUpdateTeam hook
|
||||||
|
if (options?.organizationHooks?.afterUpdateTeam) {
|
||||||
|
await options?.organizationHooks.afterUpdateTeam({
|
||||||
|
team: updatedTeam,
|
||||||
|
user: session.user,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.json(updatedTeam);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedTeam = await adapter.updateTeam(team.id, updates);
|
||||||
|
|
||||||
|
// Run afterUpdateTeam hook
|
||||||
|
if (options?.organizationHooks?.afterUpdateTeam) {
|
||||||
|
await options?.organizationHooks.afterUpdateTeam({
|
||||||
|
team: updatedTeam,
|
||||||
|
user: session.user,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return ctx.json(updatedTeam);
|
return ctx.json(updatedTeam);
|
||||||
},
|
},
|
||||||
@@ -862,11 +974,66 @@ export const addTeamMember = <O extends OrganizationOptions>(options: O) =>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const team = await adapter.findTeamById({
|
||||||
|
teamId: ctx.body.teamId,
|
||||||
|
organizationId: session.session.activeOrganizationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!team) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const organization = await adapter.findOrganizationById(
|
||||||
|
session.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
if (!organization) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userBeingAdded = await ctx.context.internalAdapter.findUserById(
|
||||||
|
ctx.body.userId,
|
||||||
|
);
|
||||||
|
if (!userBeingAdded) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: "User not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run beforeAddTeamMember hook
|
||||||
|
if (options?.organizationHooks?.beforeAddTeamMember) {
|
||||||
|
const response = await options?.organizationHooks.beforeAddTeamMember({
|
||||||
|
teamMember: {
|
||||||
|
teamId: ctx.body.teamId,
|
||||||
|
userId: ctx.body.userId,
|
||||||
|
},
|
||||||
|
team,
|
||||||
|
user: userBeingAdded,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
if (response && typeof response === "object" && "data" in response) {
|
||||||
|
// Allow the hook to modify the data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const teamMember = await adapter.findOrCreateTeamMember({
|
const teamMember = await adapter.findOrCreateTeamMember({
|
||||||
teamId: ctx.body.teamId,
|
teamId: ctx.body.teamId,
|
||||||
userId: ctx.body.userId,
|
userId: ctx.body.userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Run afterAddTeamMember hook
|
||||||
|
if (options?.organizationHooks?.afterAddTeamMember) {
|
||||||
|
await options?.organizationHooks.afterAddTeamMember({
|
||||||
|
teamMember,
|
||||||
|
team,
|
||||||
|
user: userBeingAdded,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return ctx.json(teamMember);
|
return ctx.json(teamMember);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -962,11 +1129,71 @@ export const removeTeamMember = <O extends OrganizationOptions>(options: O) =>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const team = await adapter.findTeamById({
|
||||||
|
teamId: ctx.body.teamId,
|
||||||
|
organizationId: session.session.activeOrganizationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!team) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const organization = await adapter.findOrganizationById(
|
||||||
|
session.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
if (!organization) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userBeingRemoved = await ctx.context.internalAdapter.findUserById(
|
||||||
|
ctx.body.userId,
|
||||||
|
);
|
||||||
|
if (!userBeingRemoved) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: "User not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const teamMember = await adapter.findTeamMember({
|
||||||
|
teamId: ctx.body.teamId,
|
||||||
|
userId: ctx.body.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!teamMember) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_TEAM,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run beforeRemoveTeamMember hook
|
||||||
|
if (options?.organizationHooks?.beforeRemoveTeamMember) {
|
||||||
|
await options?.organizationHooks.beforeRemoveTeamMember({
|
||||||
|
teamMember,
|
||||||
|
team,
|
||||||
|
user: userBeingRemoved,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await adapter.removeTeamMember({
|
await adapter.removeTeamMember({
|
||||||
teamId: ctx.body.teamId,
|
teamId: ctx.body.teamId,
|
||||||
userId: ctx.body.userId,
|
userId: ctx.body.userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Run afterRemoveTeamMember hook
|
||||||
|
if (options?.organizationHooks?.afterRemoveTeamMember) {
|
||||||
|
await options?.organizationHooks.afterRemoveTeamMember({
|
||||||
|
teamMember,
|
||||||
|
team,
|
||||||
|
user: userBeingRemoved,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return ctx.json({ message: "Team member removed successfully." });
|
return ctx.json({ message: "Team member removed successfully." });
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -221,42 +221,6 @@ export interface OrganizationOptions {
|
|||||||
*/
|
*/
|
||||||
request?: Request,
|
request?: Request,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
|
||||||
onInvitationAccepted?: (
|
|
||||||
data: {
|
|
||||||
/**
|
|
||||||
* the invitation id
|
|
||||||
*/
|
|
||||||
id: string;
|
|
||||||
/**
|
|
||||||
* the role of the user
|
|
||||||
*/
|
|
||||||
role: string;
|
|
||||||
/**
|
|
||||||
* the organization the user joined
|
|
||||||
*/
|
|
||||||
organization: Organization;
|
|
||||||
/**
|
|
||||||
* the invitation object
|
|
||||||
*/
|
|
||||||
invitation: Invitation;
|
|
||||||
/**
|
|
||||||
* the member who sent the invitation
|
|
||||||
*/
|
|
||||||
inviter: Member & {
|
|
||||||
user: User;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* the user who accepted the invitation
|
|
||||||
*/
|
|
||||||
acceptedUser: User;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* The request object
|
|
||||||
*/
|
|
||||||
request?: Request,
|
|
||||||
) => Promise<void>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The schema for the organization plugin.
|
* The schema for the organization plugin.
|
||||||
*/
|
*/
|
||||||
@@ -310,18 +274,29 @@ export interface OrganizationOptions {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Disable organization deletion
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
disableOrganizationDeletion?: boolean;
|
||||||
/**
|
/**
|
||||||
* Configure how organization deletion is handled
|
* Configure how organization deletion is handled
|
||||||
|
*
|
||||||
|
* @deprecated Use `organizationHooks` instead
|
||||||
*/
|
*/
|
||||||
organizationDeletion?: {
|
organizationDeletion?: {
|
||||||
/**
|
/**
|
||||||
* disable deleting organization
|
* disable deleting organization
|
||||||
|
*
|
||||||
|
* @deprecated Use `disableOrganizationDeletion` instead
|
||||||
*/
|
*/
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
/**
|
/**
|
||||||
* A callback that runs before the organization is
|
* A callback that runs before the organization is
|
||||||
* deleted
|
* deleted
|
||||||
*
|
*
|
||||||
|
* @deprecated Use `organizationHooks` instead
|
||||||
* @param data - organization and user object
|
* @param data - organization and user object
|
||||||
* @param request - the request object
|
* @param request - the request object
|
||||||
* @returns
|
* @returns
|
||||||
@@ -337,6 +312,7 @@ export interface OrganizationOptions {
|
|||||||
* A callback that runs after the organization is
|
* A callback that runs after the organization is
|
||||||
* deleted
|
* deleted
|
||||||
*
|
*
|
||||||
|
* @deprecated Use `organizationHooks` instead
|
||||||
* @param data - organization and user object
|
* @param data - organization and user object
|
||||||
* @param request - the request object
|
* @param request - the request object
|
||||||
* @returns
|
* @returns
|
||||||
@@ -349,12 +325,15 @@ export interface OrganizationOptions {
|
|||||||
request?: Request,
|
request?: Request,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* @deprecated Use `organizationHooks` instead
|
||||||
|
*/
|
||||||
organizationCreation?: {
|
organizationCreation?: {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
beforeCreate?: (
|
beforeCreate?: (
|
||||||
data: {
|
data: {
|
||||||
organization: Omit<Organization, "id"> & Record<string, any>;
|
organization: Omit<Organization, "id"> & Record<string, any>;
|
||||||
user: User;
|
user: User & Record<string, any>;
|
||||||
},
|
},
|
||||||
request?: Request,
|
request?: Request,
|
||||||
) => Promise<void | {
|
) => Promise<void | {
|
||||||
@@ -364,11 +343,433 @@ export interface OrganizationOptions {
|
|||||||
data: {
|
data: {
|
||||||
organization: Organization & Record<string, any>;
|
organization: Organization & Record<string, any>;
|
||||||
member: Member & Record<string, any>;
|
member: Member & Record<string, any>;
|
||||||
user: User;
|
user: User & Record<string, any>;
|
||||||
},
|
},
|
||||||
request?: Request,
|
request?: Request,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Hooks for organization
|
||||||
|
*/
|
||||||
|
organizationHooks?: {
|
||||||
|
/**
|
||||||
|
* A callback that runs before the organization is created
|
||||||
|
*
|
||||||
|
* You can return a `data` object to override the default data.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* beforeCreateOrganization: async (data) => {
|
||||||
|
* return {
|
||||||
|
* data: {
|
||||||
|
* ...data.organization,
|
||||||
|
* },
|
||||||
|
* };
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* You can also throw `new APIError` to stop the organization creation.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* beforeCreateOrganization: async (data) => {
|
||||||
|
* throw new APIError("BAD_REQUEST", {
|
||||||
|
* message: "Organization creation is disabled",
|
||||||
|
* });
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
beforeCreateOrganization?: (data: {
|
||||||
|
organization: {
|
||||||
|
name?: string;
|
||||||
|
slug?: string;
|
||||||
|
logo?: string;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
user: User & Record<string, any>;
|
||||||
|
}) => Promise<void | {
|
||||||
|
data: Record<string, any>;
|
||||||
|
}>;
|
||||||
|
/**
|
||||||
|
* A callback that runs after the organization is created
|
||||||
|
*/
|
||||||
|
afterCreateOrganization?: (data: {
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
member: Member & Record<string, any>;
|
||||||
|
user: User & Record<string, any>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
/**
|
||||||
|
* A callback that runs before the organization is updated
|
||||||
|
*
|
||||||
|
* You can return a `data` object to override the default data.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* beforeUpdateOrganization: async (data) => {
|
||||||
|
* return { data: { ...data.organization } };
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
beforeUpdateOrganization?: (data: {
|
||||||
|
organization: {
|
||||||
|
name?: string;
|
||||||
|
slug?: string;
|
||||||
|
logo?: string;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
user: User & Record<string, any>;
|
||||||
|
member: Member & Record<string, any>;
|
||||||
|
}) => Promise<void | {
|
||||||
|
data: {
|
||||||
|
name?: string;
|
||||||
|
slug?: string;
|
||||||
|
logo?: string;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
/**
|
||||||
|
* A callback that runs after the organization is updated
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* afterUpdateOrganization: async (data) => {
|
||||||
|
* console.log(data.organization);
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
afterUpdateOrganization?: (data: {
|
||||||
|
/**
|
||||||
|
* Updated organization object
|
||||||
|
*
|
||||||
|
* This could be `null` if an adapter doesn't return updated organization.
|
||||||
|
*/
|
||||||
|
organization: (Organization & Record<string, any>) | null;
|
||||||
|
user: User & Record<string, any>;
|
||||||
|
member: Member & Record<string, any>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
/**
|
||||||
|
* A callback that runs before the organization is deleted
|
||||||
|
*/
|
||||||
|
beforeDeleteOrganization?: (data: {
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
user: User & Record<string, any>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
/**
|
||||||
|
* A callback that runs after the organization is deleted
|
||||||
|
*/
|
||||||
|
afterDeleteOrganization?: (data: {
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
user: User & Record<string, any>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
/**
|
||||||
|
* Member hooks
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs before a member is added to an organization
|
||||||
|
*
|
||||||
|
* You can return a `data` object to override the default data.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* beforeAddMember: async (data) => {
|
||||||
|
* return {
|
||||||
|
* data: {
|
||||||
|
* ...data.member,
|
||||||
|
* role: "custom-role"
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
beforeAddMember?: (data: {
|
||||||
|
member: {
|
||||||
|
userId: string;
|
||||||
|
organizationId: string;
|
||||||
|
role: string;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
user: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void | {
|
||||||
|
data: Record<string, any>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs after a member is added to an organization
|
||||||
|
*/
|
||||||
|
afterAddMember?: (data: {
|
||||||
|
member: Member & Record<string, any>;
|
||||||
|
user: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs before a member is removed from an organization
|
||||||
|
*/
|
||||||
|
beforeRemoveMember?: (data: {
|
||||||
|
member: Member & Record<string, any>;
|
||||||
|
user: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs after a member is removed from an organization
|
||||||
|
*/
|
||||||
|
afterRemoveMember?: (data: {
|
||||||
|
member: Member & Record<string, any>;
|
||||||
|
user: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs before a member's role is updated
|
||||||
|
*
|
||||||
|
* You can return a `data` object to override the default data.
|
||||||
|
*/
|
||||||
|
beforeUpdateMemberRole?: (data: {
|
||||||
|
member: Member & Record<string, any>;
|
||||||
|
newRole: string;
|
||||||
|
user: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void | {
|
||||||
|
data: {
|
||||||
|
role: string;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs after a member's role is updated
|
||||||
|
*/
|
||||||
|
afterUpdateMemberRole?: (data: {
|
||||||
|
member: Member & Record<string, any>;
|
||||||
|
previousRole: string;
|
||||||
|
user: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invitation hooks
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs before an invitation is created
|
||||||
|
*
|
||||||
|
* You can return a `data` object to override the default data.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* beforeCreateInvitation: async (data) => {
|
||||||
|
* return {
|
||||||
|
* data: {
|
||||||
|
* ...data.invitation,
|
||||||
|
* expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7) // 7 days
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
beforeCreateInvitation?: (data: {
|
||||||
|
invitation: {
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
organizationId: string;
|
||||||
|
inviterId: string;
|
||||||
|
teamId?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
inviter: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void | {
|
||||||
|
data: Record<string, any>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs after an invitation is created
|
||||||
|
*/
|
||||||
|
afterCreateInvitation?: (data: {
|
||||||
|
invitation: Invitation & Record<string, any>;
|
||||||
|
inviter: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs before an invitation is accepted
|
||||||
|
*/
|
||||||
|
beforeAcceptInvitation?: (data: {
|
||||||
|
invitation: Invitation & Record<string, any>;
|
||||||
|
user: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs after an invitation is accepted
|
||||||
|
*/
|
||||||
|
afterAcceptInvitation?: (data: {
|
||||||
|
invitation: Invitation & Record<string, any>;
|
||||||
|
member: Member & Record<string, any>;
|
||||||
|
user: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs before an invitation is rejected
|
||||||
|
*/
|
||||||
|
beforeRejectInvitation?: (data: {
|
||||||
|
invitation: Invitation & Record<string, any>;
|
||||||
|
user: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs after an invitation is rejected
|
||||||
|
*/
|
||||||
|
afterRejectInvitation?: (data: {
|
||||||
|
invitation: Invitation & Record<string, any>;
|
||||||
|
user: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs before an invitation is cancelled
|
||||||
|
*/
|
||||||
|
beforeCancelInvitation?: (data: {
|
||||||
|
invitation: Invitation & Record<string, any>;
|
||||||
|
cancelledBy: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs after an invitation is cancelled
|
||||||
|
*/
|
||||||
|
afterCancelInvitation?: (data: {
|
||||||
|
invitation: Invitation & Record<string, any>;
|
||||||
|
cancelledBy: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Team hooks (when teams are enabled)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs before a team is created
|
||||||
|
*
|
||||||
|
* You can return a `data` object to override the default data.
|
||||||
|
*/
|
||||||
|
beforeCreateTeam?: (data: {
|
||||||
|
team: {
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
user?: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void | {
|
||||||
|
data: Record<string, any>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs after a team is created
|
||||||
|
*/
|
||||||
|
afterCreateTeam?: (data: {
|
||||||
|
team: Team & Record<string, any>;
|
||||||
|
user?: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs before a team is updated
|
||||||
|
*
|
||||||
|
* You can return a `data` object to override the default data.
|
||||||
|
*/
|
||||||
|
beforeUpdateTeam?: (data: {
|
||||||
|
team: Team & Record<string, any>;
|
||||||
|
updates: {
|
||||||
|
name?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
user: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void | {
|
||||||
|
data: Record<string, any>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs after a team is updated
|
||||||
|
*/
|
||||||
|
afterUpdateTeam?: (data: {
|
||||||
|
team: (Team & Record<string, any>) | null;
|
||||||
|
user: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs before a team is deleted
|
||||||
|
*/
|
||||||
|
beforeDeleteTeam?: (data: {
|
||||||
|
team: Team & Record<string, any>;
|
||||||
|
user?: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs after a team is deleted
|
||||||
|
*/
|
||||||
|
afterDeleteTeam?: (data: {
|
||||||
|
team: Team & Record<string, any>;
|
||||||
|
user?: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs before a member is added to a team
|
||||||
|
*/
|
||||||
|
beforeAddTeamMember?: (data: {
|
||||||
|
teamMember: {
|
||||||
|
teamId: string;
|
||||||
|
userId: string;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
team: Team & Record<string, any>;
|
||||||
|
user: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void | {
|
||||||
|
data: Record<string, any>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs after a member is added to a team
|
||||||
|
*/
|
||||||
|
afterAddTeamMember?: (data: {
|
||||||
|
teamMember: TeamMember & Record<string, any>;
|
||||||
|
team: Team & Record<string, any>;
|
||||||
|
user: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs before a member is removed from a team
|
||||||
|
*/
|
||||||
|
beforeRemoveTeamMember?: (data: {
|
||||||
|
teamMember: TeamMember & Record<string, any>;
|
||||||
|
team: Team & Record<string, any>;
|
||||||
|
user: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback that runs after a member is removed from a team
|
||||||
|
*/
|
||||||
|
afterRemoveTeamMember?: (data: {
|
||||||
|
teamMember: TeamMember & Record<string, any>;
|
||||||
|
team: Team & Record<string, any>;
|
||||||
|
user: User & Record<string, any>;
|
||||||
|
organization: Organization & Record<string, any>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* Automatically create an organization for the user on sign up.
|
* Automatically create an organization for the user on sign up.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user