import { whereClauseForOrgWithSlugOrRequestedSlug } from "@calcom/ee/organizations/lib/orgDomains"; import { getParsedTeam } from "@calcom/features/ee/teams/lib/getParsedTeam"; import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository"; import { getTranslation } from "@calcom/i18n/server"; import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability"; import { buildNonDelegationCredentials } from "@calcom/lib/delegationCredential"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { withSelectedCalendars } from "@calcom/lib/server/withSelectedCalendars"; import type { PrismaClient } from "@calcom/prisma"; import { availabilityUserSelect } from "@calcom/prisma"; import type { DestinationCalendar, SelectedCalendar, User as UserType } from "@calcom/prisma/client"; import { Prisma } from "@calcom/prisma/client"; import type { CreationSource } from "@calcom/prisma/enums"; import { BookingStatus, MembershipRole } from "@calcom/prisma/enums"; import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import { userSelect as prismaUserSelect } from "@calcom/prisma/selects/user"; import { userMetadata } from "@calcom/prisma/zod-utils"; import type { UpId, UserProfile } from "@calcom/types/UserProfile"; import type { z } from "zod"; export type { UserWithLegacySelectedCalendars } from "@calcom/lib/server/withSelectedCalendars"; export { withSelectedCalendars }; export type UserAdminTeams = number[]; export type SessionUser = { id: number; username: string | null; name: string | null; email: string; emailVerified: Date | null; bio: string | null; avatarUrl: string | null; timeZone: string; weekStart: string; startTime: number; endTime: number; defaultScheduleId: number | null; bufferTime: number; theme: string | null; appTheme: string | null; createdDate: Date; hideBranding: boolean; twoFactorEnabled: boolean; disableImpersonation: boolean; identityProvider: string | null; identityProviderId: string | null; brandColor: string | null; darkBrandColor: string | null; movedToProfileId: number | null; completedOnboarding: boolean; destinationCalendar: DestinationCalendar | null; locale: string; timeFormat: number | null; trialEndsAt: Date | null; metadata: z.infer; role: string; allowDynamicBooking: boolean; allowSEOIndexing: boolean; receiveMonthlyDigestEmail: boolean; requiresBookerEmailVerification: boolean; profiles: UserProfile[]; allSelectedCalendars: SelectedCalendar[]; userLevelSelectedCalendars: SelectedCalendar[]; }; const log = logger.getSubLogger({ prefix: ["[repository/user]"] }); export const ORGANIZATION_ID_UNKNOWN = "ORGANIZATION_ID_UNKNOWN"; const teamSelect = { id: true, name: true, slug: true, metadata: true, logoUrl: true, organizationSettings: true, isOrganization: true, isPlatform: true, } satisfies Prisma.TeamSelect; const userSelect = { id: true, uuid: true, username: true, name: true, email: true, emailVerified: true, bio: true, avatarUrl: true, timeZone: true, weekStart: true, bufferTime: true, hideBranding: true, theme: true, createdDate: true, trialEndsAt: true, completedOnboarding: true, locale: true, timeFormat: true, twoFactorSecret: true, twoFactorEnabled: true, backupCodes: true, identityProviderId: true, invitedTo: true, brandColor: true, darkBrandColor: true, allowDynamicBooking: true, allowSEOIndexing: true, receiveMonthlyDigestEmail: true, requiresBookerEmailVerification: true, verified: true, disableImpersonation: true, locked: true, movedToProfileId: true, metadata: true, isPlatformManaged: true, lastActiveAt: true, identityProvider: true, teams: true, profiles: true, } satisfies Prisma.UserSelect; export class UserRepository { constructor(private prismaClient: PrismaClient) {} async findTeamsByUserId({ userId }: { userId: UserType["id"] }) { const teamMemberships = await this.prismaClient.membership.findMany({ where: { userId: userId, }, include: { team: { select: teamSelect, }, }, }); const acceptedTeamMemberships = teamMemberships.filter((membership) => membership.accepted); const pendingTeamMemberships = teamMemberships.filter((membership) => !membership.accepted); return { teams: acceptedTeamMemberships.map((membership) => membership.team), memberships: teamMemberships, acceptedTeamMemberships, pendingTeamMemberships, }; } async findOrganizations({ userId }: { userId: UserType["id"] }) { const { acceptedTeamMemberships } = await this.findTeamsByUserId({ userId, }); const acceptedOrgMemberships = acceptedTeamMemberships.filter( (membership) => membership.team.isOrganization ); const organizations = acceptedOrgMemberships.map((membership) => membership.team); return { organizations, }; } /** * It is aware of the fact that a user can be part of multiple organizations. */ async findUsersByUsername({ orgSlug, usernameList }: { orgSlug: string | null; usernameList: string[] }) { const { where, profiles } = await this._getWhereClauseForFindingUsersByUsername({ orgSlug, usernameList, }); return ( await this.prismaClient.user.findMany({ select: userSelect, where, }) ).map((user) => { // User isn't part of any organization if (!profiles) { return { ...user, profile: ProfileRepository.buildPersonalProfileFromUser({ user }), }; } const profile = profiles.find((profile) => profile.user.id === user.id) ?? null; if (!profile) { log.error("Profile not found for user", safeStringify({ user, profiles })); // Profile must be there because profile itself was used to retrieve the user throw new Error("Profile couldn't be found"); } const { user: _1, ...profileWithoutUser } = profile; return { ...user, profile: profileWithoutUser, }; }); } async findPlatformMembersByUsernames({ usernameList }: { usernameList: string[] }) { return ( await this.prismaClient.user.findMany({ select: userSelect, where: { username: { in: usernameList, }, isPlatformManaged: false, profiles: { some: { organization: { isPlatform: true, }, }, }, }, }) ).map((user) => { return { ...user, profile: ProfileRepository.buildPersonalProfileFromUser({ user }), }; }); } async _getWhereClauseForFindingUsersByUsername({ orgSlug, usernameList, }: { orgSlug: string | null; usernameList: string[]; }) { // Lookup in profiles because that's where the organization usernames exist const profiles = orgSlug ? ( await ProfileRepository.findManyByOrgSlugOrRequestedSlug({ orgSlug: orgSlug, usernames: usernameList, }) ).map((profile) => ({ ...profile, organization: getParsedTeam(profile.organization), })) : null; const where = profiles && profiles.length > 0 ? { // Get UserIds from profiles id: { in: profiles.map((profile) => profile.user.id), }, } : { username: { in: usernameList, }, ...(orgSlug ? { organization: whereClauseForOrgWithSlugOrRequestedSlug(orgSlug), } : { organization: null, }), }; return { where, profiles }; } async findByEmail({ email }: { email: string }) { const user = await this.prismaClient.user.findUnique({ where: { email: email.toLowerCase(), }, select: userSelect, }); return user; } async findManyByEmailsWithEmailVerificationSettings({ emails }: { emails: string[] }) { const normalizedEmails = emails.map((e) => e.toLowerCase()); if (!normalizedEmails.length) return []; const users = await this.findVerifiedUsersByEmailsRaw(normalizedEmails); if (!users.length) return []; return users.map((u) => ({ email: u.email, matchedEmail: u.matchedEmail, requiresBookerEmailVerification: u.requiresBookerEmailVerification, })); } private async findVerifiedUsersByEmailsRaw(emails: string[]) { const emailListSql = Prisma.join(emails.map((e) => Prisma.sql`${e}`)); return this.prismaClient.$queryRaw< Array<{ id: number; email: string; matchedEmail: string; requiresBookerEmailVerification: boolean; }> >(Prisma.sql` SELECT u."id", u."email", u."email" AS "matchedEmail", u."requiresBookerEmailVerification" FROM "public"."users" AS u WHERE u."email" IN (${emailListSql}) AND u."emailVerified" IS NOT NULL AND u."locked" = FALSE UNION SELECT u."id", u."email", t0."email" AS "matchedEmail", u."requiresBookerEmailVerification" FROM "public"."users" AS u INNER JOIN "public"."SecondaryEmail" AS t0 ON t0."userId" = u."id" WHERE t0."email" IN (${emailListSql}) AND t0."emailVerified" IS NOT NULL AND u."locked" = FALSE `); } async findByEmailAndIncludeProfilesAndPassword({ email }: { email: string }) { const user = await this.prismaClient.user.findUnique({ where: { email: email.toLowerCase(), }, select: { locked: true, role: true, id: true, uuid: true, username: true, name: true, email: true, metadata: true, identityProvider: true, password: true, twoFactorEnabled: true, twoFactorSecret: true, backupCodes: true, locale: true, teams: { include: { team: { select: teamSelect, }, }, }, createdDate: true, }, }); if (!user) { return null; } const allProfiles = await ProfileRepository.findAllProfilesForUserIncludingMovedUser(user); return { ...user, allProfiles, }; } async findById({ id }: { id: number }) { const user = await this.prismaClient.user.findUnique({ where: { id, }, select: userSelect, }); if (!user) { return null; } return { ...user, metadata: userMetadata.parse(user.metadata), }; } async findSecondaryEmailByUserIdAndEmail({ userId, email }: { userId: number; email: string }) { return this.prismaClient.secondaryEmail.findUnique({ where: { userId_email: { userId, email, }, }, select: { id: true, emailVerified: true, }, }); } async findByUuid({ uuid }: { uuid: string }) { return this.prismaClient.user.findUnique({ where: { uuid, }, select: { name: true, email: true, avatarUrl: true, }, }); } async findByIds({ ids }: { ids: number[] }) { return this.prismaClient.user.findMany({ where: { id: { in: ids, }, }, select: userSelect, }); } async findByIdsWithPagination({ ids, search, cursor, limit, }: { ids: number[]; search?: string | null; cursor?: number | null; limit?: number | null; }) { const where: Record = { id: cursor ? { in: ids, gt: cursor } : { in: ids }, }; if (search) { where.OR = [ { name: { contains: search, mode: "insensitive" } }, { email: { contains: search, mode: "insensitive" } }, ]; } const users = await this.prismaClient.user.findMany({ where, select: { id: true, name: true, email: true, }, orderBy: { id: "asc" }, ...(limit ? { take: limit + 1 } : {}), }); if (!limit) { return { users, nextCursor: undefined, total: users.length }; } const hasMore = users.length > limit; const items = hasMore ? users.slice(0, limit) : users; const nextCursor = hasMore ? items[items.length - 1].id : undefined; // Only count on the first page to avoid an extra query on every scroll let total: number | undefined; if (!cursor) { const countWhere: Record = { id: { in: ids }, }; if (search) { countWhere.OR = [ { name: { contains: search, mode: "insensitive" } }, { email: { contains: search, mode: "insensitive" } }, ]; } total = await this.prismaClient.user.count({ where: countWhere }); } return { users: items, nextCursor, total }; } async findByUuids({ uuids }: { uuids: string[] }) { if (uuids.length === 0) return []; return this.prismaClient.user.findMany({ where: { uuid: { in: uuids, }, }, select: { id: true, uuid: true, name: true, email: true, avatarUrl: true, }, }); } async findByIdOrThrow({ id }: { id: number }) { const user = await this.findById({ id }); if (!user) { throw new Error(`User with id ${id} not found`); } return user; } async findManyByOrganization({ organizationId }: { organizationId: number }) { const profiles = await ProfileRepository.findManyForOrg({ organizationId }); return profiles.map((profile) => profile.user); } isAMemberOfOrganization({ user, organizationId, }: { user: { profiles: { organizationId: number }[] }; organizationId: number; }) { return user.profiles.some((profile) => profile.organizationId === organizationId); } async findIfAMemberOfSomeOrganization({ user }: { user: { id: number } }) { return !!( await ProfileRepository.findManyForUser({ id: user.id, }) ).length; } isMigratedToOrganization({ user, }: { user: { metadata?: { migratedToOrgFrom?: unknown; } | null; }; }) { return !!user.metadata?.migratedToOrgFrom; } async isMovedToAProfile({ user }: { user: Pick }) { return !!user.movedToProfileId; } async swapPrimaryEmailWithSecondaryEmail({ userId, secondaryEmailId, oldPrimaryEmail, oldPrimaryEmailVerified, newPrimaryEmail, userUpdateData, }: { userId: number; secondaryEmailId: number; oldPrimaryEmail: string; oldPrimaryEmailVerified: Date | null; newPrimaryEmail: string; userUpdateData?: Prisma.UserUpdateInput; }) { const [, updatedUser] = await this.prismaClient.$transaction([ this.prismaClient.secondaryEmail.update({ where: { id: secondaryEmailId, userId: userId, }, data: { email: oldPrimaryEmail, emailVerified: oldPrimaryEmailVerified, }, }), this.prismaClient.user.update({ where: { id: userId }, data: { ...userUpdateData, email: newPrimaryEmail, }, }), ]); return updatedUser; } async enrichUserWithTheProfile({ user, upId, }: { user: T; upId: UpId; }) { const profile = await ProfileRepository.findByUpIdWithAuth(upId, user.id); if (!profile) { return { ...user, profile: ProfileRepository.buildPersonalProfileFromUser({ user }), }; } return { ...user, profile, }; } /** * Use this method instead of `enrichUserWithTheProfile` if you don't directly have the profileId. * It can happen in following cases: * 1. While dealing with a User that hasn't been added to any organization yet and thus have no Profile entries. * 2. While dealing with a User that has been moved to a Profile i.e. he was invited to an organization when he was an existing user. * 3. We haven't added profileId to all the entities, so they aren't aware of which profile they belong to. So, we still mostly use this function to enrich the user with its profile. */ async enrichUserWithItsProfile< T extends { id: number; username: string | null; }, >({ user, }: { user: T; }): Promise< T & { nonProfileUsername: string | null; profile: UserProfile; } > { const profiles = await ProfileRepository.findManyForUser({ id: user.id }); if (profiles.length) { const profile = profiles[0]; // platform org user doesn't need org profile if (profile?.organization?.isPlatform) { return { ...user, nonProfileUsername: user.username, profile: ProfileRepository.buildPersonalProfileFromUser({ user }), }; } return { ...user, username: profile.username, nonProfileUsername: user.username, profile, }; } // If no organization profile exists, use the personal profile so that the returned user is normalized to have a profile always return { ...user, nonProfileUsername: user.username, profile: ProfileRepository.buildPersonalProfileFromUser({ user }), }; } async enrichUserWithItsProfileExcludingOrgMetadata< T extends { id: number; username: string | null; }, >({ user, }: { user: T; }): Promise< T & { nonProfileUsername: string | null; profile: UserProfile; } > { const enrichedUser = await this.enrichUserWithItsProfile({ user }); const { profile } = enrichedUser; if (!profile || !profile.organization) { return enrichedUser; } // Exclude organization metadata const sanitizedProfile: UserProfile = { ...profile, organization: { ...profile.organization, metadata: {}, organizationSettings: profile.organization.organizationSettings, }, }; return { ...enrichedUser, profile: sanitizedProfile, }; } async enrichUsersWithTheirProfiles( users: T[] ): Promise< Array< T & { nonProfileUsername: string | null; profile: UserProfile; } > > { if (users.length === 0) return []; const userIds = users.map((user) => user.id); const profiles = await ProfileRepository.findManyForUsers(userIds); // Create a Map for faster lookups, preserving arrays of profiles per user const profileMap = new Map(); profiles.forEach((profile) => { if (!profileMap.has(profile.userId)) { profileMap.set(profile.userId, []); } profileMap.get(profile.userId)!.push(profile); }); // Precompute personal profiles for all users const personalProfileMap = new Map(); users.forEach((user) => { personalProfileMap.set(user.id, ProfileRepository.buildPersonalProfileFromUser({ user })); }); return users.map((user) => { const userProfiles = profileMap.get(user.id) || []; if (userProfiles.length > 0) { const profile = userProfiles[0]; if (profile?.organization?.isPlatform) { return { ...user, nonProfileUsername: user.username, profile: personalProfileMap.get(user.id)!, }; } return { ...user, username: profile.username, nonProfileUsername: user.username, profile, }; } // If no organization profile exists, use the precomputed personal profile return { ...user, nonProfileUsername: user.username, profile: personalProfileMap.get(user.id)!, }; }); } async enrichUsersWithTheirProfileExcludingOrgMetadata( users: T[] ): Promise< Array< T & { nonProfileUsername: string | null; profile: UserProfile; } > > { const enrichedUsers = await this.enrichUsersWithTheirProfiles(users); return enrichedUsers.map((enrichedUser) => { const { profile } = enrichedUser; if (!profile || !profile.organization) { return enrichedUser; } // Exclude organization metadata const sanitizedProfile: UserProfile = { ...profile, organization: { ...profile.organization, metadata: {}, organizationSettings: profile.organization.organizationSettings, }, }; return { ...enrichedUser, profile: sanitizedProfile, }; }); } enrichUserWithItsProfileBuiltFromUser({ user, }: { user: T; }): T & { nonProfileUsername: string | null; profile: UserProfile; } { // If no organization profile exists, use the personal profile so that the returned user is normalized to have a profile always return { ...user, nonProfileUsername: user.username, profile: ProfileRepository.buildPersonalProfileFromUser({ user }), }; } async enrichEntityWithProfile< T extends | { profile: { id: number; username: string | null; organizationId: number | null; organization?: { id: number; name: string; calVideoLogo?: string | null; bannerUrl: string | null; slug: string | null; metadata: Prisma.JsonValue; }; }; } | { user: { username: string | null; id: number; }; }, >(entity: T) { if ("profile" in entity) { const { profile, ...entityWithoutProfile } = entity; const { organization, ...profileWithoutOrganization } = profile || {}; const parsedOrg = organization ? getParsedTeam(organization) : null; const ret = { ...entityWithoutProfile, profile: { ...profileWithoutOrganization, ...(parsedOrg ? { organization: parsedOrg, } : { organization: null, }), }, }; return ret; } else { const profiles = await ProfileRepository.findManyForUser(entity.user); if (!profiles.length) { return { ...entity, profile: ProfileRepository.buildPersonalProfileFromUser({ user: entity.user, }), }; } else { return { ...entity, profile: profiles[0], }; } } } async updateWhereId({ whereId, data, }: { whereId: number; data: { movedToProfileId?: number | null; }; }) { return this.prismaClient.user.update({ where: { id: whereId, }, data: { movedToProfile: data.movedToProfileId ? { connect: { id: data.movedToProfileId, }, } : undefined, }, }); } async create( data: Omit & { username: string; hashedPassword?: string; organizationId: number | null; creationSource: CreationSource; locked: boolean; } ) { const organizationIdValue = data.organizationId; const { email, username, creationSource, locked, hashedPassword, ...rest } = data; logger.info("create user", { email, username, organizationIdValue, locked, }); const t = await getTranslation("en", "common"); const availability = getAvailabilityFromSchedule(DEFAULT_SCHEDULE); const user = await this.prismaClient.user.create({ data: { username, email: email, ...(hashedPassword && { password: { create: { hash: hashedPassword } }, }), // Default schedule schedules: { create: { name: t("default_schedule_name"), availability: { createMany: { data: availability.map((schedule) => ({ days: schedule.days, startTime: schedule.startTime, endTime: schedule.endTime, })), }, }, }, }, creationSource, locked, ...(organizationIdValue ? { organizationId: organizationIdValue, profiles: { create: { username, organizationId: organizationIdValue, uid: ProfileRepository.generateProfileUid(), }, }, } : {}), ...rest, }, }); return user; } async getUserAdminTeams({ userId }: { userId: number }) { return await this.prismaClient.user.findUnique({ where: { id: userId, }, select: { id: true, avatarUrl: true, name: true, username: true, teams: { where: { accepted: true, OR: [ { role: { in: [MembershipRole.ADMIN, MembershipRole.OWNER] }, }, { team: { parent: { members: { some: { id: userId, role: { in: [MembershipRole.ADMIN, MembershipRole.OWNER], }, }, }, }, }, }, ], }, select: { team: { select: { id: true, name: true, logoUrl: true, isOrganization: true, parent: { select: { logoUrl: true, name: true, id: true, }, }, }, }, }, }, }, }); } async isAdminOfTeamOrParentOrg({ userId, teamId }: { userId: number; teamId: number }) { const membershipQuery = { members: { some: { userId, role: { in: [MembershipRole.ADMIN, MembershipRole.OWNER] }, }, }, }; const teams = await this.prismaClient.team.findMany({ where: { id: teamId, OR: [ membershipQuery, { parent: { ...membershipQuery }, }, ], }, select: { id: true, }, }); return !!teams.length; } async isAdminOrOwnerOfTeam({ userId, teamId }: { userId: number; teamId: number }) { const isAdminOrOwnerOfTeam = await this.prismaClient.membership.findUnique({ where: { userId_teamId: { userId, teamId, }, role: { in: [MembershipRole.ADMIN, MembershipRole.OWNER] }, accepted: true, }, select: { id: true, }, }); return !!isAdminOrOwnerOfTeam; } async getUserOrganizationAndTeams({ userId }: { userId: number }) { return await this.prismaClient.user.findUnique({ where: { id: userId }, select: { organizationId: true, teams: { where: { accepted: true }, select: { teamId: true }, }, }, }); } async getTimeZoneAndDefaultScheduleId({ userId }: { userId: number }) { return await this.prismaClient.user.findUnique({ where: { id: userId, }, select: { timeZone: true, defaultScheduleId: true, }, }); } async adminFindById(userId: number) { return await this.prismaClient.user.findUniqueOrThrow({ where: { id: userId, }, }); } async findUserTeams({ id }: { id: number }) { const user = await this.prismaClient.user.findUnique({ where: { id, }, select: { completedOnboarding: true, teams: { select: { accepted: true, team: { select: { id: true, name: true, logoUrl: true, }, }, }, }, }, }); if (!user) { return null; } return user; } async updateAvatar({ id, avatarUrl }: { id: number; avatarUrl: string }) { // Using updateMany here since if the user already has a profile it would throw an error // because no records were found to update the profile picture await this.prismaClient.user.updateMany({ where: { id, avatarUrl: { equals: null, }, }, data: { avatarUrl, }, }); } async findUserWithCredentials({ id }: { id: number }) { const user = await this.prismaClient.user.findUnique({ where: { id, }, select: { credentials: { select: credentialForCalendarServiceSelect, }, timeZone: true, id: true, selectedCalendars: true, }, }); if (!user) { return null; } const { credentials, ...userWithSelectedCalendars } = withSelectedCalendars(user); return { ...userWithSelectedCalendars, credentials: buildNonDelegationCredentials(credentials), }; } async findUnlockedUserForSession({ userId }: { userId: number }) { const user = await this.prismaClient.user.findUnique({ where: { id: userId, // Locked users can't login locked: false, }, select: { id: true, uuid: true, username: true, name: true, email: true, emailVerified: true, bio: true, avatarUrl: true, timeZone: true, weekStart: true, defaultScheduleId: true, bufferTime: true, theme: true, appTheme: true, createdDate: true, hideBranding: true, twoFactorEnabled: true, disableImpersonation: true, identityProvider: true, identityProviderId: true, brandColor: true, darkBrandColor: true, movedToProfileId: true, selectedCalendars: { select: { id: true, eventTypeId: true, externalId: true, integration: true, updatedAt: true, googleChannelId: true, }, }, completedOnboarding: true, destinationCalendar: true, locale: true, timeFormat: true, trialEndsAt: true, metadata: true, role: true, allowDynamicBooking: true, allowSEOIndexing: true, receiveMonthlyDigestEmail: true, requiresBookerEmailVerification: true, profiles: true, }, }); if (!user) { return null; } return withSelectedCalendars(user); } async getUserStats({ userId }: { userId: number }) { const user = await this.prismaClient.user.findUnique({ where: { id: userId, }, select: { _count: { select: { bookings: true, // We only need user level selected calendars selectedCalendars: { where: { eventTypeId: null, }, }, teams: true, eventTypes: true, }, }, teams: { select: { team: { select: { eventTypes: { select: { id: true, }, }, }, }, }, }, }, }); if (!user) { return null; } const { _count, ...restUser } = user; const { selectedCalendars, ...restCount } = _count; return { ...restUser, _count: { ...restCount, userLevelSelectedCalendars: selectedCalendars, }, }; } async findManyByIdsIncludeDestinationAndSelectedCalendars({ ids }: { ids: number[] }) { const users = await this.prismaClient.user.findMany({ where: { id: { in: ids } }, include: { selectedCalendars: true, destinationCalendar: true, }, }); return users.map(withSelectedCalendars); } async updateStripeCustomerId({ id, stripeCustomerId, existingMetadata, }: { id: number; stripeCustomerId: string; existingMetadata: z.infer; }) { return this.prismaClient.user.update({ where: { id }, data: { metadata: { ...existingMetadata, stripeCustomerId } }, }); } async updateWhitelistWorkflows({ id, whitelistWorkflows }: { id: number; whitelistWorkflows: boolean }) { return this.prismaClient.user.update({ where: { id }, data: { whitelistWorkflows }, }); } async findManyUsersForDynamicEventType({ currentOrgDomain, usernameList, }: { currentOrgDomain: string | null; usernameList: string[]; }) { const { where } = await this._getWhereClauseForFindingUsersByUsername({ orgSlug: currentOrgDomain, usernameList, }); // TODO: Should be moved to UserRepository return this.prismaClient.user.findMany({ where, select: { locked: true, allowDynamicBooking: true, ...availabilityUserSelect, credentials: { select: credentialForCalendarServiceSelect, }, }, }); } async findUsersByIds(userIds: number[]) { return this.prismaClient.user.findMany({ where: { id: { in: userIds }, }, select: { id: true, name: true, email: true, }, }); } async findUsersWithLastBooking({ userIds, eventTypeId }: { userIds: number[]; eventTypeId: number }) { return this.prismaClient.user.findMany({ where: { id: { in: userIds, }, }, select: { id: true, bookings: { select: { createdAt: true, }, where: { eventTypeId, status: BookingStatus.ACCEPTED, attendees: { some: { noShow: false, }, }, OR: [ { noShowHost: false, }, { noShowHost: null, }, ], }, orderBy: { createdAt: "desc", }, take: 1, }, }, }); } async findUserWithHideBranding({ userId }: { userId: number }) { return this.prismaClient.user.findUnique({ where: { id: userId }, select: { hideBranding: true, profiles: { select: { organization: { select: { hideBranding: true, }, }, }, }, }, }); } async findByIdWithCredentialsAndCalendar({ userId }: { userId: number }) { return this.prismaClient.user.findUnique({ where: { id: userId }, select: { id: true, username: true, email: true, name: true, timeZone: true, locale: true, timeFormat: true, metadata: true, credentials: { select: credentialForCalendarServiceSelect, }, destinationCalendar: true, }, }); } async findForPasswordReset({ id }: { id: number }) { return this.prismaClient.user.findUnique({ where: { id }, select: { email: true, name: true, locale: true, }, }); } /** * Finds a user by ID returning only their username * @param userId - The user ID * @returns User with username or null */ async findByIdWithUsername(userId: number): Promise<{ username: string | null } | null> { return this.prismaClient.user.findUnique({ where: { id: userId }, select: { username: true }, }); } async findManyByIdsWithCredentialsAndSelectedCalendars({ userIds }: { userIds: number[] }) { const users = await this.prismaClient.user.findMany({ where: { id: { in: userIds, }, }, select: { ...prismaUserSelect, // Use the proper userSelect from @calcom/prisma/selects/user which includes schedules credentials: { select: credentialForCalendarServiceSelect, }, selectedCalendars: { select: { eventTypeId: true, }, }, }, }); return users.map(withSelectedCalendars); } async findByEmailAndTeamId({ email, teamId }: { email: string; teamId: number }) { return this.prismaClient.user.findFirst({ where: { email: email.toLowerCase(), teams: { some: { teamId, accepted: true, }, }, }, select: userSelect, }); } async findByIdWithSelectedCalendars({ userId }: { userId: number }) { return this.prismaClient.user.findUnique({ where: { id: userId }, select: { id: true, email: true, selectedCalendars: true, destinationCalendar: true, }, }); } async lockByEmail({ email }: { email: string }) { await this.prismaClient.user.updateMany({ where: { email }, data: { locked: true }, }); } async unlockByEmail({ email, }: { email: string; }): Promise<{ email: string; username: string | null } | null> { const user = await this.prismaClient.user.findFirst({ where: { email, locked: true }, select: { id: true, email: true, username: true }, }); if (!user) return null; await this.prismaClient.user.update({ where: { id: user.id }, data: { locked: false }, }); return { email: user.email, username: user.username }; } }