refactor: SCIM - handleGroupEvents (#20537)
* Add logging to `handleGroupEvents` * `createUsersAndConnectOrg` skipDuplicates when creating new users * `ProfileRepository.createMany` to get username from `orgAutoAcceptEmail` * Refactor `createUsersAndConnectToOrg` to use `createMany` instead of individual db calls * Add error logging to `sendSignupToOrganizationEmail` * Move `sendSignupToOrganizationEmail` to a `Promise.allSettled` * Add `createManyForExistingUsers` and `createManyPromise` * Move creating existing users profiles to `ProfileRepository.createManyForExistingUsers` * Move `sendExistingUserTeamInviteEmails` to a `Promise.allSettled` * Refactor `createUsersAndConnectToOrg` to use `ProfileRepository.createManyPromise` * Type fix * Create `generateNewChildEventTypeDataForDB` * Create `addNewMembersToEventTypes` * Type fix * Type fix * Type fix * `createMany` new workflowsOnEventTypes * Introduce `DATABASE_CHUNK_SIZE` * Batch connect children event types * Batch connect profiles to existing users
This commit is contained in:
@@ -441,3 +441,5 @@ NEXT_PUBLIC_QUICK_AVAILABILITY_ROLLOUT=10
|
||||
# Deprecated. Use NEXT_PUBLIC_BODY_SCRIPTS instead. NEXT_PUBLIC_HEAD_SCRIPTS are now injected into the body only.
|
||||
NEXT_PUBLIC_HEAD_SCRIPTS=
|
||||
NEXT_PUBLIC_BODY_SCRIPTS=
|
||||
|
||||
DATABASE_CHUNK_SIZE=
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { DirectorySyncEvent, Group } from "@boxyhq/saml-jackson";
|
||||
|
||||
import { createAProfileForAnExistingUser } from "@calcom/lib/createAProfileForAnExistingUser";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { updateNewTeamMemberEventTypes } from "@calcom/lib/server/queries";
|
||||
import { addNewMembersToEventTypes } from "@calcom/lib/server/queries";
|
||||
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { IdentityProvider, MembershipRole } from "@calcom/prisma/enums";
|
||||
import {
|
||||
@@ -15,18 +15,28 @@ import {
|
||||
|
||||
import createUsersAndConnectToOrg from "./users/createUsersAndConnectToOrg";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["dsync/handleGroupEvents"] });
|
||||
|
||||
const handleGroupEvents = async (event: DirectorySyncEvent, organizationId: number) => {
|
||||
log.debug("called", safeStringify(event));
|
||||
const log = logger.getSubLogger({
|
||||
prefix: [`dsync/handleGroupEvents: directoryId: ${event.directory_id}`],
|
||||
});
|
||||
|
||||
log.info(
|
||||
"called",
|
||||
safeStringify({
|
||||
event: event.event,
|
||||
tenant: event.tenant,
|
||||
product: event.product,
|
||||
})
|
||||
);
|
||||
// Find the group name associated with the event
|
||||
const eventData = event.data as Group;
|
||||
|
||||
// If the group doesn't have any members assigned then return early
|
||||
if (!eventData.raw.members.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(`Event contains ${eventData.raw.members.length} members`);
|
||||
|
||||
const groupNames = await prisma.dSyncTeamGroupMapping.findMany({
|
||||
where: {
|
||||
directoryId: event.directory_id,
|
||||
@@ -67,6 +77,8 @@ const handleGroupEvents = async (event: DirectorySyncEvent, organizationId: numb
|
||||
userEmails = eventData.raw.members.map((member: { display: string }) => member.display);
|
||||
}
|
||||
|
||||
log.info(`Event contains ${userEmails.length} emails`);
|
||||
|
||||
// Find existing users
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
@@ -97,6 +109,9 @@ const handleGroupEvents = async (event: DirectorySyncEvent, organizationId: numb
|
||||
|
||||
const newUserEmails = userEmails.filter((email) => !users.find((user) => user.email === email));
|
||||
let newUsers;
|
||||
|
||||
log.info(`Event processing ${newUserEmails.length} new users and ${users.length} existing users`);
|
||||
|
||||
// For each team linked to the dsync group name provision members
|
||||
for (const group of groupNames) {
|
||||
if (newUserEmails.length) {
|
||||
@@ -117,7 +132,7 @@ const handleGroupEvents = async (event: DirectorySyncEvent, organizationId: numb
|
||||
accepted: true,
|
||||
})),
|
||||
});
|
||||
await Promise.all(
|
||||
await Promise.allSettled(
|
||||
newUserEmails.map((email) => {
|
||||
return sendSignupToOrganizationEmail({
|
||||
usernameOrEmail: email,
|
||||
@@ -126,6 +141,16 @@ const handleGroupEvents = async (event: DirectorySyncEvent, organizationId: numb
|
||||
inviterName: org.name,
|
||||
teamId: group.teamId,
|
||||
isOrg: false,
|
||||
}).catch((error) => {
|
||||
log.error(
|
||||
"Failed to send signup to organization email",
|
||||
safeStringify({
|
||||
email,
|
||||
organizationId,
|
||||
teamId: group.teamId,
|
||||
}),
|
||||
error
|
||||
);
|
||||
});
|
||||
})
|
||||
);
|
||||
@@ -161,7 +186,7 @@ const handleGroupEvents = async (event: DirectorySyncEvent, organizationId: numb
|
||||
const newOrgMembers = users.filter(
|
||||
(user) => !user.profiles.find((profile) => profile.organizationId === organizationId)
|
||||
);
|
||||
await Promise.all([
|
||||
await Promise.allSettled([
|
||||
...newMembers.map(async (user) => {
|
||||
const translation = await getTranslation(user.locale || "en", "common");
|
||||
return sendExistingUserTeamInviteEmails({
|
||||
@@ -178,28 +203,31 @@ const handleGroupEvents = async (event: DirectorySyncEvent, organizationId: numb
|
||||
isAutoJoin: true,
|
||||
currentUserParentTeamName: org.name,
|
||||
orgSlug: null,
|
||||
});
|
||||
}),
|
||||
...newOrgMembers.map((user) => {
|
||||
return createAProfileForAnExistingUser({
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
currentUsername: user.username,
|
||||
},
|
||||
organizationId,
|
||||
}).catch((error) => {
|
||||
log.error(
|
||||
"Failed to send team invite to existing user",
|
||||
safeStringify({
|
||||
email: user.email,
|
||||
organizationId,
|
||||
teamId: group.teamId,
|
||||
}),
|
||||
error
|
||||
);
|
||||
});
|
||||
}),
|
||||
]);
|
||||
|
||||
await ProfileRepository.createManyForExistingUsers({
|
||||
users: newOrgMembers,
|
||||
organizationId,
|
||||
orgAutoAcceptEmail: org.organizationSettings?.orgAutoAcceptEmail ?? "",
|
||||
});
|
||||
|
||||
// Add users to team event types if assignAllTeamMembers is enabled
|
||||
await Promise.all([
|
||||
...(newUsers?.map((newUser) => {
|
||||
return updateNewTeamMemberEventTypes(newUser.id, group.teamId);
|
||||
}) ?? []),
|
||||
...users.map((user) => {
|
||||
return updateNewTeamMemberEventTypes(user.id, group.teamId);
|
||||
}),
|
||||
]);
|
||||
await addNewMembersToEventTypes({
|
||||
userIds: [...(newUsers ? newUsers.map((user) => user.id) : []), ...users.map((user) => user.id)],
|
||||
teamId: group.teamId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createOrUpdateMemberships } from "@calcom/features/auth/signup/utils/createOrUpdateMemberships";
|
||||
import { MembershipRepository } from "@calcom/lib/server/repository/membership";
|
||||
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { IdentityProvider } from "@calcom/prisma/enums";
|
||||
import { CreationSource } from "@calcom/prisma/enums";
|
||||
import { CreationSource, MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import {
|
||||
deriveNameFromOrgUsername,
|
||||
@@ -48,6 +49,7 @@ export const createUsersAndConnectToOrg = async ({
|
||||
creationSource: CreationSource.WEBAPP,
|
||||
};
|
||||
}),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
@@ -58,17 +60,24 @@ export const createUsersAndConnectToOrg = async ({
|
||||
},
|
||||
select: dSyncUserSelect,
|
||||
});
|
||||
// Assign created users to organization
|
||||
for (const user of users) {
|
||||
await createOrUpdateMemberships({
|
||||
user,
|
||||
team: {
|
||||
id: org.id,
|
||||
isOrganization: true,
|
||||
parentId: null, // orgs don't have a parentId
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Create profiles for new users
|
||||
await ProfileRepository.createManyPromise({
|
||||
users,
|
||||
organizationId: org.id,
|
||||
orgAutoAcceptEmail: org.organizationSettings?.orgAutoAcceptEmail ?? "",
|
||||
});
|
||||
|
||||
// Create memberships for new members
|
||||
await MembershipRepository.createMany(
|
||||
users.map((user) => ({
|
||||
userId: user.id,
|
||||
teamId: org.id,
|
||||
role: MembershipRole.MEMBER,
|
||||
accepted: true,
|
||||
}))
|
||||
);
|
||||
|
||||
return users;
|
||||
};
|
||||
|
||||
|
||||
@@ -217,3 +217,5 @@ export const IS_DUB_REFERRALS_ENABLED =
|
||||
!!process.env.NEXT_PUBLIC_DUB_PROGRAM_ID && process.env.NEXT_PUBLIC_DUB_PROGRAM_ID !== "";
|
||||
|
||||
export const CAL_VIDEO_MEETING_LINK_FOR_TESTING = process.env.CAL_VIDEO_MEETING_LINK_FOR_TESTING;
|
||||
|
||||
export const DATABASE_CHUNK_SIZE = parseInt(process.env.DATABASE_CHUNK_SIZE || "25", 10);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import { getAppFromSlug } from "@calcom/app-store/utils";
|
||||
import { DATABASE_CHUNK_SIZE } from "@calcom/lib/constants";
|
||||
import { parseBookingLimit } from "@calcom/lib/intervalLimits/isBookingLimits";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import prisma, { baseEventTypeSelect } from "@calcom/prisma";
|
||||
import type { Team } from "@calcom/prisma/client";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
@@ -422,8 +425,64 @@ export async function isTeamMember(userId: number, teamId: number) {
|
||||
}));
|
||||
}
|
||||
|
||||
export async function updateNewTeamMemberEventTypes(userId: number, teamId: number) {
|
||||
const eventTypesToAdd = await prisma.eventType.findMany({
|
||||
export function generateNewChildEventTypeDataForDB({
|
||||
eventType,
|
||||
userId,
|
||||
includeWorkflow = true,
|
||||
includeUserConnect = true,
|
||||
}: {
|
||||
eventType: Prisma.EventTypeGetPayload<{ select: typeof allManagedEventTypeProps & { id: true } }>;
|
||||
userId: number;
|
||||
includeWorkflow?: boolean;
|
||||
includeUserConnect?: boolean;
|
||||
}) {
|
||||
const allManagedEventTypePropsZod = _EventTypeModel.pick(allManagedEventTypeProps).extend({
|
||||
bookingFields: _EventTypeModel.shape.bookingFields.nullish(),
|
||||
});
|
||||
|
||||
const managedEventTypeValues = allManagedEventTypePropsZod
|
||||
.omit(unlockedManagedEventTypeProps)
|
||||
.parse(eventType);
|
||||
|
||||
// Define the values for unlocked properties to use on creation, not updation
|
||||
const unlockedEventTypeValues = allManagedEventTypePropsZod
|
||||
.pick(unlockedManagedEventTypeProps)
|
||||
.parse(eventType);
|
||||
|
||||
// Calculate if there are new workflows for which assigned members will get too
|
||||
const currentWorkflowIds = Array.isArray(eventType.workflows)
|
||||
? eventType.workflows.map((wf) => wf.workflowId)
|
||||
: [];
|
||||
|
||||
return {
|
||||
...managedEventTypeValues,
|
||||
...unlockedEventTypeValues,
|
||||
bookingLimits: (managedEventTypeValues.bookingLimits as unknown as Prisma.InputJsonObject) ?? undefined,
|
||||
recurringEvent: (managedEventTypeValues.recurringEvent as unknown as Prisma.InputJsonValue) ?? undefined,
|
||||
metadata: (managedEventTypeValues.metadata as Prisma.InputJsonValue) ?? undefined,
|
||||
bookingFields: (managedEventTypeValues.bookingFields as Prisma.InputJsonValue) ?? undefined,
|
||||
durationLimits: (managedEventTypeValues.durationLimits as Prisma.InputJsonValue) ?? undefined,
|
||||
eventTypeColor: (managedEventTypeValues.eventTypeColor as Prisma.InputJsonValue) ?? undefined,
|
||||
rrSegmentQueryValue: (managedEventTypeValues.rrSegmentQueryValue as Prisma.InputJsonValue) ?? undefined,
|
||||
onlyShowFirstAvailableSlot: managedEventTypeValues.onlyShowFirstAvailableSlot ?? false,
|
||||
userId,
|
||||
...(includeUserConnect && {
|
||||
users: {
|
||||
connect: [{ id: userId }],
|
||||
},
|
||||
}),
|
||||
parentId: eventType.id,
|
||||
hidden: false,
|
||||
...(includeWorkflow && {
|
||||
workflows: currentWorkflowIds && {
|
||||
create: currentWorkflowIds.map((wfId) => ({ workflowId: wfId })),
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async function getEventTypesToAddNewMembers(teamId: number) {
|
||||
return await prisma.eventType.findMany({
|
||||
where: {
|
||||
team: { id: teamId },
|
||||
assignAllTeamMembers: true,
|
||||
@@ -434,52 +493,20 @@ export async function updateNewTeamMemberEventTypes(userId: number, teamId: numb
|
||||
schedulingType: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const allManagedEventTypePropsZod = _EventTypeModel.pick(allManagedEventTypeProps).extend({
|
||||
bookingFields: _EventTypeModel.shape.bookingFields.nullish(),
|
||||
});
|
||||
export async function updateNewTeamMemberEventTypes(userId: number, teamId: number) {
|
||||
const eventTypesToAdd = await getEventTypesToAddNewMembers(teamId);
|
||||
|
||||
eventTypesToAdd.length > 0 &&
|
||||
(await prisma.$transaction(
|
||||
eventTypesToAdd.map((eventType) => {
|
||||
if (eventType.schedulingType === "MANAGED") {
|
||||
const managedEventTypeValues = allManagedEventTypePropsZod
|
||||
.omit(unlockedManagedEventTypeProps)
|
||||
.parse(eventType);
|
||||
|
||||
// Define the values for unlocked properties to use on creation, not updation
|
||||
const unlockedEventTypeValues = allManagedEventTypePropsZod
|
||||
.pick(unlockedManagedEventTypeProps)
|
||||
.parse(eventType);
|
||||
|
||||
// Calculate if there are new workflows for which assigned members will get too
|
||||
const currentWorkflowIds = eventType.workflows?.map((wf) => wf.workflowId);
|
||||
|
||||
return prisma.eventType.create({
|
||||
data: {
|
||||
...managedEventTypeValues,
|
||||
...unlockedEventTypeValues,
|
||||
bookingLimits:
|
||||
(managedEventTypeValues.bookingLimits as unknown as Prisma.InputJsonObject) ?? undefined,
|
||||
recurringEvent:
|
||||
(managedEventTypeValues.recurringEvent as unknown as Prisma.InputJsonValue) ?? undefined,
|
||||
metadata: (managedEventTypeValues.metadata as Prisma.InputJsonValue) ?? undefined,
|
||||
bookingFields: (managedEventTypeValues.bookingFields as Prisma.InputJsonValue) ?? undefined,
|
||||
durationLimits: (managedEventTypeValues.durationLimits as Prisma.InputJsonValue) ?? undefined,
|
||||
eventTypeColor: (managedEventTypeValues.eventTypeColor as Prisma.InputJsonValue) ?? undefined,
|
||||
rrSegmentQueryValue:
|
||||
(managedEventTypeValues.rrSegmentQueryValue as Prisma.InputJsonValue) ?? undefined,
|
||||
onlyShowFirstAvailableSlot: managedEventTypeValues.onlyShowFirstAvailableSlot ?? false,
|
||||
data: generateNewChildEventTypeDataForDB({
|
||||
eventType,
|
||||
userId,
|
||||
users: {
|
||||
connect: [{ id: userId }],
|
||||
},
|
||||
parentId: eventType.id,
|
||||
hidden: false,
|
||||
workflows: currentWorkflowIds && {
|
||||
create: currentWorkflowIds.map((wfId) => ({ workflowId: wfId })),
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
return prisma.eventType.update({
|
||||
@@ -490,3 +517,147 @@ export async function updateNewTeamMemberEventTypes(userId: number, teamId: numb
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
export async function addNewMembersToEventTypes({ userIds, teamId }: { userIds: number[]; teamId: number }) {
|
||||
const log = logger.getSubLogger({
|
||||
prefix: ["addNewMembersToEventTypes"],
|
||||
});
|
||||
|
||||
const eventTypesToAdd = await getEventTypesToAddNewMembers(teamId);
|
||||
|
||||
const managedEventTypes = eventTypesToAdd.filter((eventType) => eventType.schedulingType === "MANAGED");
|
||||
const teamEventTypes = eventTypesToAdd.filter((eventType) => eventType.schedulingType !== "MANAGED");
|
||||
|
||||
await Promise.allSettled([
|
||||
prisma.eventType
|
||||
.createMany({
|
||||
data: managedEventTypes
|
||||
.map((eventType) =>
|
||||
userIds.map((userId) =>
|
||||
generateNewChildEventTypeDataForDB({
|
||||
eventType,
|
||||
userId,
|
||||
includeWorkflow: false,
|
||||
includeUserConnect: false,
|
||||
})
|
||||
)
|
||||
)
|
||||
.flat(),
|
||||
skipDuplicates: true,
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error(
|
||||
`Failed to add new members to managed event types`,
|
||||
safeStringify({
|
||||
teamId,
|
||||
error,
|
||||
})
|
||||
);
|
||||
}),
|
||||
prisma.host
|
||||
.createMany({
|
||||
data: teamEventTypes
|
||||
.map((eventType) => {
|
||||
return userIds.map((userId) => {
|
||||
return {
|
||||
userId,
|
||||
eventTypeId: eventType.id,
|
||||
isFixed: eventType.schedulingType === "COLLECTIVE",
|
||||
};
|
||||
});
|
||||
})
|
||||
.flat(),
|
||||
skipDuplicates: true,
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error(
|
||||
`Failed to add new members as hosts`,
|
||||
safeStringify({
|
||||
teamId,
|
||||
error,
|
||||
})
|
||||
);
|
||||
}),
|
||||
]);
|
||||
|
||||
// Connect to users and workflows
|
||||
const createdChildrenEventTypes = await prisma.eventType.findMany({
|
||||
where: {
|
||||
userId: {
|
||||
in: userIds,
|
||||
},
|
||||
parent: {
|
||||
id: {
|
||||
in: managedEventTypes.map((eventType) => eventType.id),
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
workflows: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (createdChildrenEventTypes.length > 0) {
|
||||
await Promise.allSettled([
|
||||
prisma.workflowsOnEventTypes
|
||||
.createMany({
|
||||
data: createdChildrenEventTypes
|
||||
.map((eventType) =>
|
||||
eventType.workflows.map((workflow) => ({
|
||||
eventTypeId: eventType.id,
|
||||
workflowId: workflow.id,
|
||||
}))
|
||||
)
|
||||
.flat(),
|
||||
skipDuplicates: true,
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error(
|
||||
`Failed to connect new children event types to workflows`,
|
||||
safeStringify({
|
||||
teamId,
|
||||
error,
|
||||
})
|
||||
);
|
||||
}),
|
||||
]);
|
||||
// Connect children event types to users
|
||||
for (let i = 0; i < createdChildrenEventTypes.length; i += DATABASE_CHUNK_SIZE) {
|
||||
const childrenEventTypeBatch = createdChildrenEventTypes.slice(i, i + DATABASE_CHUNK_SIZE);
|
||||
|
||||
await Promise.allSettled([
|
||||
childrenEventTypeBatch.map((childEventType) => {
|
||||
if (!childEventType.userId) return;
|
||||
return prisma.eventType
|
||||
.update({
|
||||
where: {
|
||||
id: childEventType.id,
|
||||
},
|
||||
data: {
|
||||
users: {
|
||||
connect: [{ id: childEventType.userId }],
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error(
|
||||
`Failed to connect new children event types to users`,
|
||||
safeStringify({
|
||||
teamId,
|
||||
childEventTypeId: childEventType.id,
|
||||
userId: childEventType.userId,
|
||||
error,
|
||||
})
|
||||
);
|
||||
});
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { User as PrismaUser } from "@prisma/client";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { whereClauseForOrgWithSlugOrRequestedSlug } from "@calcom/ee/organizations/lib/orgDomains";
|
||||
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
|
||||
import { DATABASE_CHUNK_SIZE } from "@calcom/lib/constants";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
@@ -249,20 +251,91 @@ export class ProfileRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async createManyForExistingUsers({
|
||||
users,
|
||||
organizationId,
|
||||
orgAutoAcceptEmail,
|
||||
}: {
|
||||
users: { id: number; username: string | null; email: string }[];
|
||||
organizationId: number;
|
||||
orgAutoAcceptEmail: string;
|
||||
}) {
|
||||
await prisma.profile.createMany({
|
||||
data: users.map((user) => ({
|
||||
uid: ProfileRepository.generateProfileUid(),
|
||||
userId: user.id,
|
||||
organizationId,
|
||||
username: user?.username || getOrgUsernameFromEmail(user.email, orgAutoAcceptEmail),
|
||||
})),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
// Populate the movedFromUser
|
||||
const createdProfiles = await prisma.profile.findMany({
|
||||
where: {
|
||||
userId: {
|
||||
in: users.map((user) => user.id),
|
||||
},
|
||||
organizationId,
|
||||
},
|
||||
});
|
||||
|
||||
for (let i = 0; i < createdProfiles.length; i += DATABASE_CHUNK_SIZE) {
|
||||
const profilesBatch = createdProfiles.slice(i, i + DATABASE_CHUNK_SIZE);
|
||||
await Promise.allSettled([
|
||||
profilesBatch.map((profile) => {
|
||||
prisma.user.update({
|
||||
where: {
|
||||
id: profile.userId,
|
||||
},
|
||||
data: {
|
||||
movedToProfile: {
|
||||
connect: { id: profile.id },
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
static async createManyPromise({
|
||||
users,
|
||||
organizationId,
|
||||
orgAutoAcceptEmail,
|
||||
}: {
|
||||
users: { id: number; username: string | null; email: string }[];
|
||||
organizationId: number;
|
||||
orgAutoAcceptEmail: string;
|
||||
}) {
|
||||
return await prisma.profile.createMany({
|
||||
data: users.map((user) => ({
|
||||
uid: ProfileRepository.generateProfileUid(),
|
||||
userId: user.id,
|
||||
organizationId,
|
||||
username: user?.username || getOrgUsernameFromEmail(user.email, orgAutoAcceptEmail),
|
||||
})),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
|
||||
static createMany({
|
||||
users,
|
||||
organizationId,
|
||||
orgAutoAcceptEmail,
|
||||
}: {
|
||||
users: { id: number; username: string; email: string }[];
|
||||
users: { id: number; username: string | null; email: string }[];
|
||||
organizationId: number;
|
||||
orgAutoAcceptEmail: string;
|
||||
}) {
|
||||
return prisma.profile.createMany({
|
||||
data: users.map((user) => ({
|
||||
uid: ProfileRepository.generateProfileUid(),
|
||||
userId: user.id,
|
||||
organizationId,
|
||||
username: user.username || user.email.split("@")[0],
|
||||
username: user?.username || getOrgUsernameFromEmail(user.email, orgAutoAcceptEmail),
|
||||
})),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -117,6 +117,7 @@ export const adminVerifyHandler = async ({ input }: AdminVerifyOptions) => {
|
||||
};
|
||||
}),
|
||||
organizationId: input.orgId,
|
||||
orgAutoAcceptEmail: acceptedEmailDomain,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
||||
@@ -457,35 +457,46 @@ export async function sendSignupToOrganizationEmail({
|
||||
teamId: number;
|
||||
isOrg: boolean;
|
||||
}) {
|
||||
const token: string = randomBytes(32).toString("hex");
|
||||
try {
|
||||
const token: string = randomBytes(32).toString("hex");
|
||||
|
||||
await prisma.verificationToken.create({
|
||||
data: {
|
||||
identifier: usernameOrEmail,
|
||||
token,
|
||||
expires: new Date(new Date().setHours(168)), // +1 week
|
||||
team: {
|
||||
connect: {
|
||||
id: teamId,
|
||||
await prisma.verificationToken.create({
|
||||
data: {
|
||||
identifier: usernameOrEmail,
|
||||
token,
|
||||
expires: new Date(new Date().setHours(168)), // +1 week
|
||||
team: {
|
||||
connect: {
|
||||
id: teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await sendTeamInviteEmail({
|
||||
language: translation,
|
||||
from: inviterName || `${team.name}'s admin`,
|
||||
to: usernameOrEmail,
|
||||
teamName: team.name,
|
||||
joinLink: `${WEBAPP_URL}/signup?token=${token}&callbackUrl=/getting-started`,
|
||||
isCalcomMember: false,
|
||||
isOrg: isOrg,
|
||||
parentTeamName: team?.parent?.name,
|
||||
isAutoJoin: false,
|
||||
isExistingUserMovedToOrg: false,
|
||||
// For a new user there is no prev and new links.
|
||||
prevLink: null,
|
||||
newLink: null,
|
||||
});
|
||||
});
|
||||
await sendTeamInviteEmail({
|
||||
language: translation,
|
||||
from: inviterName || `${team.name}'s admin`,
|
||||
to: usernameOrEmail,
|
||||
teamName: team.name,
|
||||
joinLink: `${WEBAPP_URL}/signup?token=${token}&callbackUrl=/getting-started`,
|
||||
isCalcomMember: false,
|
||||
isOrg: isOrg,
|
||||
parentTeamName: team?.parent?.name,
|
||||
isAutoJoin: false,
|
||||
isExistingUserMovedToOrg: false,
|
||||
// For a new user there is no prev and new links.
|
||||
prevLink: null,
|
||||
newLink: null,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Failed to send signup to organization email",
|
||||
safeStringify({
|
||||
usernameOrEmail,
|
||||
orgId: teamId,
|
||||
}),
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type TeamAndOrganizationSettings = Team & {
|
||||
|
||||
@@ -275,6 +275,7 @@
|
||||
"DAILY_SCALE_PLAN",
|
||||
"DAILY_WEBHOOK_SECRET",
|
||||
"DAILY_MEETING_ENDED_WEBHOOK_SECRET",
|
||||
"DATABASE_CHUNK_SIZE",
|
||||
"DATABASE_DIRECT_URL",
|
||||
"DATABASE_URL",
|
||||
"DEBUG",
|
||||
|
||||
Reference in New Issue
Block a user