mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-06 04:19:20 +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"
|
||||
},
|
||||
"[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 { getTelemetryAuthConfig } from "./telemetry/detectors/detect-auth-config";
|
||||
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 { organization } from "./organization";
|
||||
import { createAuthClient } from "../../client";
|
||||
@@ -15,7 +15,6 @@ import { ownerAc } from "./access";
|
||||
import { nextCookies } from "../../integrations/next-js";
|
||||
|
||||
describe("organization", async (it) => {
|
||||
const onInvitationAccepted = vi.fn();
|
||||
const { auth, signInWithTestUser, signInWithUser, cookieSetter } =
|
||||
await getTestInstance({
|
||||
user: {
|
||||
@@ -37,7 +36,6 @@ describe("organization", async (it) => {
|
||||
},
|
||||
},
|
||||
invitationLimit: 3,
|
||||
onInvitationAccepted,
|
||||
}),
|
||||
],
|
||||
logger: {
|
||||
@@ -295,94 +293,7 @@ describe("organization", async (it) => {
|
||||
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 () => {
|
||||
const invite = await client.organization.inviteMember({
|
||||
organizationId: organizationId,
|
||||
@@ -1996,3 +1907,79 @@ describe("Additional Fields", async () => {
|
||||
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
|
||||
} = ctx.body;
|
||||
|
||||
const invitation = await adapter.createInvitation({
|
||||
invitation: {
|
||||
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({
|
||||
invitation: invitationData,
|
||||
user: session.user,
|
||||
});
|
||||
|
||||
@@ -405,6 +428,15 @@ export const createInvitation = <O extends OrganizationOptions>(option: O) => {
|
||||
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);
|
||||
},
|
||||
);
|
||||
@@ -492,6 +524,25 @@ export const acceptInvitation = <O extends OrganizationOptions>(options: O) =>
|
||||
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({
|
||||
invitationId: ctx.body.invitationId,
|
||||
status: "accepted",
|
||||
@@ -573,35 +624,13 @@ export const acceptInvitation = <O extends OrganizationOptions>(options: O) =>
|
||||
},
|
||||
});
|
||||
}
|
||||
if (ctx.context.orgOptions.onInvitationAccepted) {
|
||||
const organization = await adapter.findOrganizationById(
|
||||
invitation.organizationId,
|
||||
);
|
||||
|
||||
const inviterMember = await adapter.findMemberByOrgId({
|
||||
userId: invitation.inviterId,
|
||||
organizationId: invitation.organizationId,
|
||||
if (options?.organizationHooks?.afterAcceptInvitation) {
|
||||
await options?.organizationHooks.afterAcceptInvitation({
|
||||
invitation: acceptedI as unknown as Invitation,
|
||||
member,
|
||||
user: session.user,
|
||||
organization,
|
||||
});
|
||||
|
||||
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({
|
||||
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({
|
||||
invitationId: ctx.body.invitationId,
|
||||
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({
|
||||
invitation: rejectedI,
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
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({
|
||||
invitationId: ctx.body.invitationId,
|
||||
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);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -147,13 +147,42 @@ export const addMember = <O extends OrganizationOptions>(option: O) => {
|
||||
...additionalFields
|
||||
} = 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,
|
||||
userId: user.id,
|
||||
role: parseRoles(ctx.body.role as string | string[]),
|
||||
createdAt: new Date(),
|
||||
...(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) {
|
||||
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);
|
||||
},
|
||||
);
|
||||
@@ -308,6 +346,32 @@ export const removeMember = <O extends OrganizationOptions>(options: O) =>
|
||||
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);
|
||||
if (
|
||||
session.user.id === toBeRemovedMember.userId &&
|
||||
@@ -316,6 +380,16 @@ export const removeMember = <O extends OrganizationOptions>(options: O) =>
|
||||
) {
|
||||
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({
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
parseRoles(ctx.body.role as string | string[]),
|
||||
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(
|
||||
ctx.body.memberId,
|
||||
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);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from "../../../db";
|
||||
|
||||
export const createOrganization = <O extends OrganizationOptions>(
|
||||
options: O,
|
||||
options?: O,
|
||||
) => {
|
||||
const additionalFieldsSchema = toZodSchema({
|
||||
fields: options?.schema?.organization?.additionalFields || {},
|
||||
@@ -162,12 +162,6 @@ export const createOrganization = <O extends OrganizationOptions>(
|
||||
...orgData
|
||||
} = ctx.body;
|
||||
|
||||
let hookResponse:
|
||||
| {
|
||||
data: Record<string, any>;
|
||||
}
|
||||
| undefined = undefined;
|
||||
|
||||
if (options.organizationCreation?.beforeCreate) {
|
||||
const response = await options.organizationCreation.beforeCreate(
|
||||
{
|
||||
@@ -180,7 +174,24 @@ export const createOrganization = <O extends OrganizationOptions>(
|
||||
ctx.request,
|
||||
);
|
||||
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: {
|
||||
...orgData,
|
||||
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) {
|
||||
await adapter.setActiveOrganization(
|
||||
ctx.context.session.session.token,
|
||||
@@ -297,7 +315,7 @@ export const checkOrganizationSlug = <O extends OrganizationOptions>(
|
||||
);
|
||||
|
||||
export const updateOrganization = <O extends OrganizationOptions>(
|
||||
options: O,
|
||||
options?: O,
|
||||
) => {
|
||||
const additionalFieldsSchema = toZodSchema({
|
||||
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,
|
||||
});
|
||||
}
|
||||
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(
|
||||
organizationId,
|
||||
ctx.body.data,
|
||||
);
|
||||
if (options?.organizationHooks?.afterUpdateOrganization) {
|
||||
await options.organizationHooks.afterUpdateOrganization({
|
||||
organization: updatedOrg,
|
||||
user: session.user,
|
||||
member,
|
||||
});
|
||||
}
|
||||
return ctx.json(updatedOrg);
|
||||
},
|
||||
);
|
||||
@@ -459,6 +498,19 @@ export const deleteOrganization = <O extends OrganizationOptions>(
|
||||
},
|
||||
},
|
||||
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);
|
||||
if (!session) {
|
||||
throw new APIError("UNAUTHORIZED", { status: 401 });
|
||||
@@ -503,23 +555,20 @@ export const deleteOrganization = <O extends OrganizationOptions>(
|
||||
*/
|
||||
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);
|
||||
if (!org) {
|
||||
throw new APIError("BAD_REQUEST");
|
||||
}
|
||||
if (option?.beforeDelete) {
|
||||
await option.beforeDelete({
|
||||
if (options?.organizationHooks?.beforeDeleteOrganization) {
|
||||
await options.organizationHooks.beforeDeleteOrganization({
|
||||
organization: org,
|
||||
user: session.user,
|
||||
});
|
||||
}
|
||||
await adapter.deleteOrganization(organizationId);
|
||||
if (option?.afterDelete) {
|
||||
await option.afterDelete({
|
||||
if (options?.organizationHooks?.afterDeleteOrganization) {
|
||||
await options.organizationHooks.afterDeleteOrganization({
|
||||
organization: org,
|
||||
user: session.user,
|
||||
});
|
||||
|
||||
@@ -155,13 +155,52 @@ export const createTeam = <O extends OrganizationOptions>(options: O) => {
|
||||
});
|
||||
}
|
||||
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,
|
||||
organizationId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
...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);
|
||||
},
|
||||
);
|
||||
@@ -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);
|
||||
|
||||
// Run afterDeleteTeam hook
|
||||
if (options?.organizationHooks?.afterDeleteTeam) {
|
||||
await options?.organizationHooks.afterDeleteTeam({
|
||||
team,
|
||||
user: session?.user,
|
||||
organization,
|
||||
});
|
||||
}
|
||||
|
||||
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 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,
|
||||
...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);
|
||||
},
|
||||
@@ -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({
|
||||
teamId: ctx.body.teamId,
|
||||
userId: ctx.body.userId,
|
||||
});
|
||||
|
||||
// Run afterAddTeamMember hook
|
||||
if (options?.organizationHooks?.afterAddTeamMember) {
|
||||
await options?.organizationHooks.afterAddTeamMember({
|
||||
teamMember,
|
||||
team,
|
||||
user: userBeingAdded,
|
||||
organization,
|
||||
});
|
||||
}
|
||||
|
||||
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({
|
||||
teamId: ctx.body.teamId,
|
||||
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." });
|
||||
},
|
||||
);
|
||||
|
||||
@@ -221,42 +221,6 @@ export interface OrganizationOptions {
|
||||
*/
|
||||
request?: Request,
|
||||
) => 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.
|
||||
*/
|
||||
@@ -310,18 +274,29 @@ export interface OrganizationOptions {
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Disable organization deletion
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
disableOrganizationDeletion?: boolean;
|
||||
/**
|
||||
* Configure how organization deletion is handled
|
||||
*
|
||||
* @deprecated Use `organizationHooks` instead
|
||||
*/
|
||||
organizationDeletion?: {
|
||||
/**
|
||||
* disable deleting organization
|
||||
*
|
||||
* @deprecated Use `disableOrganizationDeletion` instead
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* A callback that runs before the organization is
|
||||
* deleted
|
||||
*
|
||||
* @deprecated Use `organizationHooks` instead
|
||||
* @param data - organization and user object
|
||||
* @param request - the request object
|
||||
* @returns
|
||||
@@ -337,6 +312,7 @@ export interface OrganizationOptions {
|
||||
* A callback that runs after the organization is
|
||||
* deleted
|
||||
*
|
||||
* @deprecated Use `organizationHooks` instead
|
||||
* @param data - organization and user object
|
||||
* @param request - the request object
|
||||
* @returns
|
||||
@@ -349,12 +325,15 @@ export interface OrganizationOptions {
|
||||
request?: Request,
|
||||
) => Promise<void>;
|
||||
};
|
||||
/**
|
||||
* @deprecated Use `organizationHooks` instead
|
||||
*/
|
||||
organizationCreation?: {
|
||||
disabled?: boolean;
|
||||
beforeCreate?: (
|
||||
data: {
|
||||
organization: Omit<Organization, "id"> & Record<string, any>;
|
||||
user: User;
|
||||
user: User & Record<string, any>;
|
||||
},
|
||||
request?: Request,
|
||||
) => Promise<void | {
|
||||
@@ -364,11 +343,433 @@ export interface OrganizationOptions {
|
||||
data: {
|
||||
organization: Organization & Record<string, any>;
|
||||
member: Member & Record<string, any>;
|
||||
user: User;
|
||||
user: User & Record<string, any>;
|
||||
},
|
||||
request?: Request,
|
||||
) => 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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user