feat: New Organization Schema along with the ability to invite existing users to an organization (#13002)
* Add profile table Copy to profile table Update schema Migrations * wip * Get switcher working * Add organization seeder * wip * Remove dead code * Fix unit tests * relevantProfile -> profile * cache bust * One more comment * Squash migrations * Copy to profile table * Update schema * Fix existing user invite flow * Add profile table * Copy to profile table * Update schema * Format * Add profile table Copy to profile table Update schema Migrations * Format * wip Get switcher working Add organization seeder wip Remove dead code Fix unit tests relevantProfile -> profile cache bust One more comment Squash migrations Copy to profile table Update schema Fix existing user invite flow Add profile table Copy to profile table Update schema chore: small improvements to signup (#13227) * removed features on mobile, better background color * nit * dark mode nits New Crowdin translations by Github Action fix: event reschedule workflow trigger (#13122) * fix event reschedule workflow trigger * uncomment previously failed test * review fix --------- Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: CarinaWolli <wollencarina@gmail.com> chore: added community support to help (#13213) refactor: Abstract `handleSeats` from `handleNewBooking` (#12062) * Refactor createBooking * Type fix * Abstract handleSeats * Create Invitee type * Create OrganizerUser type * Abstract addVideoCallDataToEvt * Abstract createLoggerWithEventDetails * Abstract `handleAppStatus` from handler * Create ReqAppsStatus type * Move `deleteMeeting` and `getCalendar` * Set parameters for `handleSeats` * Typescript refactor * Change function params from req * Type fix --------- Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: Erik <erik@erosemberg.com> fix: Dynamic group booking org-migration redirect (#13223) * fix: Dynamic group booking redirect * Fix flaky private team test test: Check availability tab and all your functions (teste2e-availability) (#12434) * add changes * add changes * add changes * add changes * Requested changes * Change way to grab element --------- Co-authored-by: gitstart-calcom <gitstart-calcom@users.noreply.github.com> Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: Keith Williams <keithwillcode@gmail.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> fix: Reschedule a cancelled booking (#13170) * fix: Reschedule a cancelled booking * canceled instead of cancelled --------- Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> v3.7.0 test: In Team Event type when location is Organizer Default App and U… (#12939) * test: In Team Event type when location is Organizer Default App and User Metadata is emoty * chore: use variable * fix: type error test: add test for invalid cal video (#13103) * test: add test for invalid cal video * chore: add confirm * chore: fix type error * chore: update code * test: add unit test * chore: type error * chore * chore: type * chore: fix test * chore --------- Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> chore: add error message for no availability (#13230) * chore: add error message for no default user availability * chore: check only availability * chore: change message * chore: add eventType fix: Team revert with members (#13228) refactor: Refactor seats logic (#12905) * Refactor createBooking * Type fix * Abstract handleSeats * Create Invitee type * Create OrganizerUser type * Abstract addVideoCallDataToEvt * Abstract createLoggerWithEventDetails * Abstract `handleAppStatus` from handler * Create ReqAppsStatus type * Move `deleteMeeting` and `getCalendar` * Set parameters for `handleSeats` * Typescript refactor * Change function params from req * Type fix * Move handleSeats * Abstract lastAttendeeDeleteBooking * Create function for rescheduling seated events * Fix imports on reschedule seats function * Fix imports * Import handleSeats function * Fix rescheduleUid type * Refactor owner reschedule to new time slot * Refactor combine two booking times together * Reschedule as an attendee * Refactor createNewSeat * Remove old handleSeats * Remove lastAttendeeDeleteBooking from handleNewBooking * Test for new attendee right params are passed * Unit test params for reschedule * Typo fix * Clean up * Create new seat test * Test when attendee already signs up for booking * Type fix * Test reschedule move attendee to existing booking * On reschedule create new booking * Test on last attendee cancel booking * Owner reschedule to new time slot * Owner rescheduling, merge two bookings together * Test: when merging more than available seats, then fail * Test: fail when event is full * Remove duplicate E2E tests * Clean up * Rename `addVideoCallDataToEvt` to `addVideoCallDataToEvent` * Refactor `calcAppsStatus` * Assign `evt` to resutl of `addVideoCallDataToEvent` * Use prisma.transaction when moving attendees * Clean create seat call * Use ErrorCode enum * Use attendeeRescheduledSeatedBooking function * Await function * Prevent double triggering of workflows * Use inviteeToAdd in createNewSeat * Remove unused error code * Remove old handleSeats file * Type fix * Type fix * Type fix * Type fix * Type fix * Type fix * Type fix * Type fix --------- Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Erik <erik@erosemberg.com> feat: Allow admins to filter bookings only by attendee emails (#13236) * chore: Allow admins to filter bookings only by attendee emails * Fix type error Fixed [CAL-2930] Org branding isnt centered in shell #13225 (#13237) Add profile table Copy to profile table Update schema Migrations Format * wip * Add profile relation everywhere * Fix schema * Fix invite handler * Add profileId during eventTypec reation * Get dashboard working with profile relatioon * Add workflow create repository * Add profileId to host * Add profileId to credential table * Add destinationCalendar repository * Add profileId to membership * Revert all profile related changes * Add acme and dunder * Fix membership profileId usgae * Various fixes * Some renaming * Fix failing tests * Debugging next-aut * Consolidate migrations * Testing fixes * Fix unit tests * Self review feedback * PR feedback * Fix orgMigration as well * Add profile schema and copy users to it * Remove migrations from here * Fix type issues * Fix type error after main merge * Remove movedFromUserId * Add missing migration file * Self review addressed * Joe's review addressed * Fix profile switcher --------- Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
This commit is contained in:
co-authored by
Joe Au-Yeung
CarinaWolli
Morgan
Erik
gitstart-calcom
gitstart-calcom
Keith Williams
Carina Wollendorfer
Peer Richelsen
parent
42a3448abb
commit
12466fb064
@@ -8,7 +8,6 @@ import { md } from "@calcom/lib/markdownIt";
|
||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import turndown from "@calcom/lib/turndownService";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { Ensure } from "@calcom/types/utils";
|
||||
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
||||
import { UserAvatar } from "@calcom/ui";
|
||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||
@@ -97,18 +96,10 @@ const UserProfile = () => {
|
||||
},
|
||||
];
|
||||
|
||||
const organization =
|
||||
user.organization && user.organization.id
|
||||
? {
|
||||
...(user.organization as Ensure<typeof user.organization, "id">),
|
||||
slug: user.organization.slug || null,
|
||||
requestedSlug: user.organization.metadata?.requestedSlug || null,
|
||||
}
|
||||
: null;
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="flex flex-row items-center justify-start rtl:justify-end">
|
||||
{user && <UserAvatar size="lg" user={user} previewSrc={imageSrc} organization={organization} />}
|
||||
{user && <UserAvatar size="lg" user={user} previewSrc={imageSrc} />}
|
||||
<input
|
||||
ref={avatarRef}
|
||||
type="hidden"
|
||||
|
||||
@@ -5,11 +5,16 @@ import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import type { UserProfile } from "@calcom/types/UserProfile";
|
||||
import { UserAvatar } from "@calcom/ui";
|
||||
|
||||
type TeamType = Omit<NonNullable<TeamWithMembers>, "inviteToken">;
|
||||
type MembersType = TeamType["members"];
|
||||
type MemberType = Pick<MembersType[number], "id" | "name" | "bio" | "username" | "organizationId"> & {
|
||||
type MemberType = Pick<
|
||||
MembersType[number],
|
||||
"id" | "name" | "bio" | "username" | "organizationId" | "avatarUrl"
|
||||
> & {
|
||||
profile: Omit<UserProfile, "upId">;
|
||||
safeBio: string | null;
|
||||
bookerUrl: string;
|
||||
};
|
||||
@@ -27,7 +32,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
||||
key={member.id}
|
||||
href={{ pathname: `${member.bookerUrl}/${member.username}`, query: queryParamsToForward }}>
|
||||
<div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 hover:cursor-pointer">
|
||||
<UserAvatar size="md" user={member} />
|
||||
<UserAvatar noOrganizationIndicator size="md" user={member} />
|
||||
<section className="mt-2 line-clamp-4 w-full space-y-1">
|
||||
<p className="text-default font-medium">{member.name}</p>
|
||||
<div className="text-subtle line-clamp-3 overflow-ellipsis text-sm font-normal">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { getBookingForReschedule, getMultipleDurationValue } from "@calcom/features/bookings/lib/get-booking";
|
||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -56,19 +57,9 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
return notFound;
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
username,
|
||||
organization: isValidOrgDomain
|
||||
? {
|
||||
slug: currentOrgDomain,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
select: {
|
||||
away: true,
|
||||
hideBranding: true,
|
||||
},
|
||||
const [user] = await UserRepository.findUsersByUsername({
|
||||
usernameList: [username],
|
||||
orgSlug: org,
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
|
||||
@@ -1589,17 +1589,20 @@ async function createTeamOutsideOrg(
|
||||
});
|
||||
}
|
||||
|
||||
async function createUserOutsideOrg(data: Omit<Prisma.UserCreateArgs["data"], "organization">) {
|
||||
async function createUserOutsideOrg(
|
||||
data: Omit<Prisma.UserCreateArgs["data"], "organization" | "movedToProfile">
|
||||
) {
|
||||
return await prismock.user.create({
|
||||
data: {
|
||||
...data,
|
||||
movedToProfileId: null,
|
||||
organizationId: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function createUserInsideTheOrg(
|
||||
data: Omit<Prisma.UserUncheckedCreateInput, "organization" | "organizationId" | "id">,
|
||||
data: Omit<Prisma.UserUncheckedCreateInput, "organization" | "organizationId" | "id" | "movedToProfileId">,
|
||||
orgId: number
|
||||
) {
|
||||
const org = await prismock.team.findUnique({
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomai
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Team, User } from "@calcom/prisma/client";
|
||||
import { RedirectType } from "@calcom/prisma/client";
|
||||
@@ -327,7 +328,7 @@ async function setOrgSlugIfNotSet(
|
||||
slug: string | null;
|
||||
},
|
||||
orgMetadata: {
|
||||
requestedSlug?: string | undefined;
|
||||
requestedSlug?: string | null | undefined;
|
||||
},
|
||||
targetOrgId: number
|
||||
) {
|
||||
@@ -348,7 +349,7 @@ async function setOrgSlugIfNotSet(
|
||||
|
||||
function assertUserPartOfOrgAndRemigrationAllowed(
|
||||
userToMoveToOrg: {
|
||||
organizationId: User["organizationId"];
|
||||
organizationId: number | null;
|
||||
},
|
||||
targetOrgId: number,
|
||||
targetOrgUsername: string,
|
||||
@@ -384,7 +385,7 @@ async function getTeamOrThrowError(targetOrgId: number) {
|
||||
|
||||
function assertUserPartOfOtherOrg(
|
||||
userToMoveToOrg: {
|
||||
organizationId: User["organizationId"];
|
||||
organizationId: number | null;
|
||||
} | null,
|
||||
userName: string | undefined,
|
||||
userId: number | undefined,
|
||||
@@ -594,6 +595,35 @@ async function dbMoveUserToOrg({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.profile.upsert({
|
||||
create: {
|
||||
uid: ProfileRepository.generateProfileUid(),
|
||||
userId: userToMoveToOrg.id,
|
||||
organizationId: targetOrgId,
|
||||
username: targetOrgUsername,
|
||||
movedFromUser: {
|
||||
connect: {
|
||||
id: userToMoveToOrg.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
update: {
|
||||
organizationId: targetOrgId,
|
||||
username: targetOrgUsername,
|
||||
movedFromUser: {
|
||||
connect: {
|
||||
id: userToMoveToOrg.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
userId_organizationId: {
|
||||
userId: userToMoveToOrg.id,
|
||||
organizationId: targetOrgId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function moveTeamsWithoutMembersToOrg({
|
||||
@@ -841,6 +871,10 @@ async function dbRemoveUserFromOrg({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await ProfileRepository.deleteMany({
|
||||
userIds: [userToRemoveFromOrg.id],
|
||||
});
|
||||
}
|
||||
|
||||
async function removeMembership({
|
||||
|
||||
@@ -121,7 +121,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
return {
|
||||
name: member.name,
|
||||
id: member.id,
|
||||
avatarUrl: member.avatarUrl,
|
||||
bio: member.bio,
|
||||
profile: member.profile,
|
||||
subteams: member.subteams,
|
||||
username: member.username,
|
||||
accepted: member.accepted,
|
||||
|
||||
@@ -729,9 +729,9 @@ const EventTypeListHeading = ({
|
||||
return (
|
||||
<div className="mb-4 flex items-center space-x-2">
|
||||
<Avatar
|
||||
alt={profile?.name || ""}
|
||||
alt={profile.name || ""}
|
||||
href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
|
||||
imageSrc={`${bookerUrl}${teamId ? "/team" : ""}/${profile.slug}/avatar.png`}
|
||||
imageSrc={profile.image}
|
||||
size="md"
|
||||
className="mt-1 inline-flex justify-center"
|
||||
/>
|
||||
@@ -739,7 +739,7 @@ const EventTypeListHeading = ({
|
||||
<Link
|
||||
href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
|
||||
className="text-emphasis font-bold">
|
||||
{profile?.name || ""}
|
||||
{profile.name || ""}
|
||||
</Link>
|
||||
{membershipCount && teamId && (
|
||||
<span className="text-subtle relative -top-px me-2 ms-2 text-xs">
|
||||
@@ -751,13 +751,13 @@ const EventTypeListHeading = ({
|
||||
</Link>
|
||||
</span>
|
||||
)}
|
||||
{profile?.slug && (
|
||||
{profile.slug && (
|
||||
<Link href={`${bookerUrl}/${profile.slug}`} className="text-subtle block text-xs">
|
||||
{`${bookerUrl.replace("https://", "").replace("http://", "")}/${profile.slug}`}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
{!profile?.slug && !!teamId && (
|
||||
{!profile.slug && !!teamId && (
|
||||
<button onClick={() => publishTeamMutation.mutate({ teamId })}>
|
||||
<Badge variant="gray" className="-ml-2 mb-1">
|
||||
{t("upgrade")}
|
||||
@@ -816,7 +816,7 @@ const CTA = ({ data }: { data: GetByViewerResponse }) => {
|
||||
const Actions = () => {
|
||||
return (
|
||||
<div className="hidden items-center md:flex">
|
||||
<TeamsFilter popoverTriggerClassNames="mb-0" showVerticalDivider={true} />
|
||||
<TeamsFilter useProfileFilter popoverTriggerClassNames="mb-0" showVerticalDivider={true} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -853,7 +853,6 @@ const Main = ({
|
||||
}) => {
|
||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||
const searchParams = useCompatSearchParams();
|
||||
const orgBranding = useOrgBranding();
|
||||
|
||||
if (!data || status === "pending") {
|
||||
return <SkeletonLoader />;
|
||||
|
||||
@@ -4,23 +4,26 @@ import { encode } from "querystring";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { handleUserRedirection } from "@calcom/features/booking-redirect/handle-user";
|
||||
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { DEFAULT_DARK_BRAND_COLOR, DEFAULT_LIGHT_BRAND_COLOR } from "@calcom/lib/constants";
|
||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { RedirectType, type EventType, type User } from "@calcom/prisma/client";
|
||||
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
||||
import { EventTypeMetaDataSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { UserProfile } from "@calcom/types/UserProfile";
|
||||
|
||||
import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
|
||||
import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["[[pages/[user]]]"] });
|
||||
export type UserPageProps = {
|
||||
trpcState: DehydratedState;
|
||||
profile: {
|
||||
@@ -33,11 +36,13 @@ export type UserPageProps = {
|
||||
requestedSlug: string | null;
|
||||
slug: string | null;
|
||||
id: number | null;
|
||||
};
|
||||
} | null;
|
||||
allowSEOIndexing: boolean;
|
||||
username: string | null;
|
||||
};
|
||||
users: Pick<User, "away" | "name" | "username" | "bio" | "verified" | "avatarUrl">[];
|
||||
users: (Pick<User, "away" | "name" | "username" | "bio" | "verified" | "avatarUrl"> & {
|
||||
profile: UserProfile;
|
||||
})[];
|
||||
themeBasis: string | null;
|
||||
markdownStrippedBio: string;
|
||||
safeBio: string;
|
||||
@@ -106,7 +111,7 @@ export const getEventTypesWithHiddenFromDB = async (userId: number) => {
|
||||
return eventTypes.reduce<typeof eventTypes>((eventTypes, eventType) => {
|
||||
const parsedMetadata = EventTypeMetaDataSchema.safeParse(eventType.metadata);
|
||||
if (!parsedMetadata.success) {
|
||||
logger.error(parsedMetadata.error);
|
||||
log.error(parsedMetadata.error);
|
||||
return eventTypes;
|
||||
}
|
||||
eventTypes.push({
|
||||
@@ -124,7 +129,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
const isOrgContext = isValidOrgDomain && currentOrgDomain;
|
||||
const dataFetchStart = Date.now();
|
||||
let outOfOffice = false;
|
||||
const isDynamicGroup = usernameList.length > 1;
|
||||
|
||||
if (usernameList.length === 1) {
|
||||
const result = await handleUserRedirection({ username: usernameList[0] });
|
||||
if (result && result.outOfOffice) {
|
||||
@@ -136,6 +141,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
}
|
||||
|
||||
if (!isOrgContext) {
|
||||
// If there is no org context, see if some redirect is setup due to org migration
|
||||
const redirect = await getTemporaryOrgRedirect({
|
||||
slugs: usernameList,
|
||||
redirectType: RedirectType.User,
|
||||
@@ -148,51 +154,22 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
}
|
||||
}
|
||||
|
||||
const usersWithoutAvatar = await prisma.user.findMany({
|
||||
where: {
|
||||
username: {
|
||||
in: usernameList,
|
||||
},
|
||||
organization: isOrgContext ? getSlugOrRequestedSlug(currentOrgDomain) : null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
name: true,
|
||||
bio: true,
|
||||
metadata: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
avatarUrl: true,
|
||||
organizationId: true,
|
||||
organization: {
|
||||
select: {
|
||||
slug: true,
|
||||
name: true,
|
||||
metadata: true,
|
||||
},
|
||||
},
|
||||
theme: true,
|
||||
away: true,
|
||||
verified: true,
|
||||
allowDynamicBooking: true,
|
||||
allowSEOIndexing: true,
|
||||
},
|
||||
const usersInOrgContext = await UserRepository.findUsersByUsername({
|
||||
usernameList,
|
||||
orgSlug: isValidOrgDomain ? currentOrgDomain : null,
|
||||
});
|
||||
|
||||
const users = usersWithoutAvatar.map((user) => ({
|
||||
...user,
|
||||
organization: {
|
||||
...user.organization,
|
||||
metadata: user.organization?.metadata ? teamMetadataSchema.parse(user.organization.metadata) : null,
|
||||
},
|
||||
avatar: `/${user.username}/avatar.png`,
|
||||
}));
|
||||
const usersWithoutAvatar = usersInOrgContext.map((user) => {
|
||||
const { avatar: _1, ...rest } = user;
|
||||
return rest;
|
||||
});
|
||||
|
||||
const isDynamicGroup = usersWithoutAvatar.length > 1;
|
||||
log.debug(safeStringify({ usersInOrgContext, isValidOrgDomain, currentOrgDomain, isDynamicGroup }));
|
||||
|
||||
if (isDynamicGroup) {
|
||||
const destinationUrl = `/${usernameList.join("+")}/dynamic`;
|
||||
logger.debug(`Dynamic group detected, redirecting to ${destinationUrl}`);
|
||||
log.debug(`Dynamic group detected, redirecting to ${destinationUrl}`);
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
@@ -206,7 +183,18 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
};
|
||||
}
|
||||
|
||||
if (!users.length || (!isValidOrgDomain && !users.some((user) => user.organizationId === null))) {
|
||||
const users = usersWithoutAvatar.map((user) => ({
|
||||
...user,
|
||||
avatar: `/${user.username}/avatar.png`,
|
||||
}));
|
||||
|
||||
const isNonOrgUser = (user: { profile: UserProfile }) => {
|
||||
return !user.profile?.organization;
|
||||
};
|
||||
|
||||
const isThereAnyNonOrgUser = users.some(isNonOrgUser);
|
||||
|
||||
if (!users.length || (!isValidOrgDomain && !isThereAnyNonOrgUser)) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as {
|
||||
@@ -225,11 +213,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
darkBrandColor: user.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR,
|
||||
allowSEOIndexing: user.allowSEOIndexing ?? true,
|
||||
username: user.username,
|
||||
organization: {
|
||||
id: user.organizationId,
|
||||
slug: user.organization?.slug ?? null,
|
||||
requestedSlug: user.organization?.metadata?.requestedSlug ?? null,
|
||||
},
|
||||
organization: user.profile.organization,
|
||||
};
|
||||
|
||||
const eventTypesWithHidden = await getEventTypesWithHiddenFromDB(user.id);
|
||||
@@ -248,7 +232,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
// if profile only has one public event-type, redirect to it
|
||||
if (eventTypes.length === 1 && context.query.redirect !== "false" && !outOfOffice) {
|
||||
// Redirect but don't change the URL
|
||||
const urlDestination = `/${user.username}/${eventTypes[0].slug}`;
|
||||
const urlDestination = `/${user.profile.username}/${eventTypes[0].slug}`;
|
||||
const { query } = context;
|
||||
const urlQuery = new URLSearchParams(encode(query));
|
||||
|
||||
@@ -263,7 +247,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
const safeBio = markdownToSafeHTML(user.bio) || "";
|
||||
|
||||
const markdownStrippedBio = stripMarkdown(user?.bio || "");
|
||||
const org = usersWithoutAvatar[0].organization;
|
||||
const org = usersWithoutAvatar[0].profile.organization;
|
||||
|
||||
return {
|
||||
props: {
|
||||
@@ -274,6 +258,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
avatarUrl: user.avatarUrl,
|
||||
away: usernameList.length === 1 ? outOfOffice : user.away,
|
||||
verified: user.verified,
|
||||
profile: user.profile,
|
||||
})),
|
||||
entity: {
|
||||
isUnpublished: org?.slug === null,
|
||||
|
||||
@@ -111,19 +111,11 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
||||
<UserAvatar
|
||||
size="xl"
|
||||
user={{
|
||||
organizationId: profile.organization?.id,
|
||||
avatarUrl: user.avatarUrl,
|
||||
profile: user.profile,
|
||||
name: profile.name,
|
||||
username: profile.username,
|
||||
}}
|
||||
organization={
|
||||
profile.organization?.id
|
||||
? {
|
||||
id: profile.organization.id,
|
||||
slug: profile.organization.slug,
|
||||
requestedSlug: null,
|
||||
}
|
||||
: null
|
||||
}
|
||||
/>
|
||||
<h1 className="font-cal text-emphasis my-1 text-3xl" data-testid="name-title">
|
||||
{profile.name}
|
||||
|
||||
@@ -5,10 +5,10 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { handleTypeRedirection } from "@calcom/features/booking-redirect/handle-type";
|
||||
import { getBookingForReschedule, getBookingForSeatedEvent } from "@calcom/features/bookings/lib/get-booking";
|
||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { orgDomainConfig, userOrgQuery } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { RedirectType } from "@calcom/prisma/client";
|
||||
|
||||
import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
|
||||
@@ -39,22 +39,13 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||
}
|
||||
}
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
username: {
|
||||
in: usernames,
|
||||
},
|
||||
organization: isValidOrgDomain
|
||||
? {
|
||||
slug: currentOrgDomain,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
select: {
|
||||
allowDynamicBooking: true,
|
||||
},
|
||||
const usersInOrgContext = await UserRepository.findUsersByUsername({
|
||||
usernameList: usernames,
|
||||
orgSlug: isValidOrgDomain ? currentOrgDomain : null,
|
||||
});
|
||||
|
||||
const users = usersInOrgContext;
|
||||
|
||||
if (!users.length) {
|
||||
return {
|
||||
notFound: true,
|
||||
@@ -129,16 +120,9 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
|
||||
const { ssrInit } = await import("@server/lib/ssr");
|
||||
const ssr = await ssrInit(context);
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
username,
|
||||
organization: userOrgQuery(context.req, context.params?.orgSlug),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
hideBranding: true,
|
||||
allowSEOIndexing: true,
|
||||
},
|
||||
const [user] = await UserRepository.findUsersByUsername({
|
||||
usernameList: [username],
|
||||
orgSlug: isValidOrgDomain ? currentOrgDomain : null,
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { getCalVideoReference } from "@calcom/features/get-cal-video-reference";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
|
||||
import { type inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
@@ -33,11 +34,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
timeZone: true,
|
||||
name: true,
|
||||
email: true,
|
||||
organization: {
|
||||
select: {
|
||||
calVideoLogo: true,
|
||||
},
|
||||
},
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
references: {
|
||||
@@ -63,6 +60,14 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
};
|
||||
}
|
||||
|
||||
const profile = booking.user
|
||||
? (
|
||||
await UserRepository.enrichUserWithItsProfile({
|
||||
user: booking.user,
|
||||
})
|
||||
).profile
|
||||
: null;
|
||||
|
||||
//daily.co calls have a 60 minute exit buffer when a user enters a call when it's not available it will trigger the modals
|
||||
const now = new Date();
|
||||
const exitDate = new Date(now.getTime() - 60 * 60 * 1000);
|
||||
@@ -102,6 +107,12 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
booking: {
|
||||
...bookingObj,
|
||||
...(bookingObj.description && { description: md.render(bookingObj.description) }),
|
||||
user: bookingObj.user
|
||||
? {
|
||||
...bookingObj.user,
|
||||
organization: profile?.organization,
|
||||
}
|
||||
: bookingObj.user,
|
||||
},
|
||||
trpcState: ssr.dehydrate(),
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||
import { TRPCError } from "@calcom/trpc/server";
|
||||
import { createContext } from "@calcom/trpc/server/createContext";
|
||||
import { bookingsRouter } from "@calcom/trpc/server/routers/viewer/bookings/_router";
|
||||
import type { UserProfile } from "@calcom/types/UserProfile";
|
||||
|
||||
enum DirectAction {
|
||||
ACCEPT = "accept",
|
||||
@@ -46,7 +47,16 @@ async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
|
||||
id: userId,
|
||||
username: "" /* Not used in this context */,
|
||||
role: UserPermissionRole.USER,
|
||||
/* Not used in this context */
|
||||
profile: {
|
||||
id: null,
|
||||
organizationId: null,
|
||||
organization: null,
|
||||
username: "",
|
||||
upId: "",
|
||||
} satisfies UserProfile,
|
||||
},
|
||||
upId: "",
|
||||
hasValidLicense: true,
|
||||
expires: "" /* Not used in this context */,
|
||||
};
|
||||
|
||||
@@ -42,14 +42,30 @@ async function getIdentityData(req: NextApiRequest) {
|
||||
: null;
|
||||
|
||||
if (username) {
|
||||
const user = await prisma.user.findFirst({
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
username,
|
||||
organization: orgQuery,
|
||||
...(orgQuery
|
||||
? {
|
||||
profiles: {
|
||||
some: {
|
||||
username,
|
||||
organization: orgQuery,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
username,
|
||||
// If a user is moved, it isn't actually available outside of the organization. So, for non-org domain check for movedToProfileId
|
||||
movedToProfileId: null,
|
||||
}),
|
||||
},
|
||||
select: { avatar: true, email: true },
|
||||
});
|
||||
|
||||
if (users.length > 1) {
|
||||
throw new Error(`More than one user found for username "${username}"`);
|
||||
}
|
||||
const [user] = users;
|
||||
return {
|
||||
name: username,
|
||||
email: user?.email,
|
||||
|
||||
@@ -230,7 +230,10 @@ const ProfileView = () => {
|
||||
|
||||
const defaultValues = {
|
||||
username: user.username || "",
|
||||
avatar: getUserAvatarUrl(user),
|
||||
avatar: getUserAvatarUrl({
|
||||
...user,
|
||||
profile: user.profile,
|
||||
}),
|
||||
name: user.name || "",
|
||||
email: user.email || "",
|
||||
bio: user.bio || "",
|
||||
@@ -443,20 +446,14 @@ const ProfileForm = ({
|
||||
const organization =
|
||||
userOrganization && userOrganization.id
|
||||
? {
|
||||
...(userOrganization as Ensure<typeof user.organization, "id">),
|
||||
...(userOrganization as Ensure<NonNullable<typeof user.organization>, "id">),
|
||||
slug: userOrganization.slug || null,
|
||||
requestedSlug: userOrganization.metadata?.requestedSlug || null,
|
||||
}
|
||||
: null;
|
||||
return (
|
||||
<>
|
||||
<UserAvatar
|
||||
data-testid="profile-upload-avatar"
|
||||
previewSrc={value}
|
||||
size="lg"
|
||||
user={user}
|
||||
organization={organization}
|
||||
/>
|
||||
<UserAvatar data-testid="profile-upload-avatar" previewSrc={value} size="lg" user={user} />
|
||||
<div className="ms-4">
|
||||
<h2 className="mb-2 text-sm font-medium">{t("profile_picture")}</h2>
|
||||
<div className="flex gap-2">
|
||||
|
||||
@@ -30,7 +30,7 @@ export const createOrgsFixture = (page: Page) => {
|
||||
};
|
||||
};
|
||||
|
||||
async function createOrgInDb({
|
||||
export async function createOrgInDb({
|
||||
name,
|
||||
slug,
|
||||
requestedSlug,
|
||||
|
||||
@@ -4,10 +4,12 @@ import type { Team } from "@prisma/client";
|
||||
import { Prisma as PrismaType } from "@prisma/client";
|
||||
import { hashSync as hash } from "bcryptjs";
|
||||
import { uuid } from "short-uuid";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
import stripe from "@calcom/features/ee/payments/server/stripe";
|
||||
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole, SchedulingType, TimeUnit, WorkflowTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
@@ -137,7 +139,21 @@ const createTeamAndAddUser = async (
|
||||
await createTeamWorkflow(user, team);
|
||||
data.children = { connect: [{ id: team.id }] };
|
||||
}
|
||||
data.orgUsers = isOrg ? { connect: [{ id: user.id }] } : undefined;
|
||||
data.orgProfiles = isOrg
|
||||
? {
|
||||
create: [
|
||||
{
|
||||
uid: ProfileRepository.generateProfileUid(),
|
||||
username: user.username ?? user.email.split("@")[0],
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: undefined;
|
||||
data.parent = organizationId ? { connect: { id: organizationId } } : undefined;
|
||||
const team = await prisma.team.create({
|
||||
data,
|
||||
@@ -181,7 +197,11 @@ export const createUsersFixture = (
|
||||
};
|
||||
},
|
||||
create: async (
|
||||
opts?: CustomUserOpts | null,
|
||||
opts?:
|
||||
| (CustomUserOpts & {
|
||||
organizationId?: number | null;
|
||||
})
|
||||
| null,
|
||||
scenario: {
|
||||
seedRoutingForms?: boolean;
|
||||
hasTeam?: true;
|
||||
@@ -199,6 +219,9 @@ export const createUsersFixture = (
|
||||
) => {
|
||||
const _user = await prisma.user.create({
|
||||
data: createUser(workerInfo, opts),
|
||||
include: {
|
||||
profiles: true,
|
||||
},
|
||||
});
|
||||
|
||||
let defaultEventTypes: SupportedTestEventTypes[] = [
|
||||
@@ -212,6 +235,9 @@ export const createUsersFixture = (
|
||||
for (const eventTypeData of defaultEventTypes) {
|
||||
eventTypeData.owner = { connect: { id: _user.id } };
|
||||
eventTypeData.users = { connect: { id: _user.id } };
|
||||
if (_user.profiles[0]) {
|
||||
eventTypeData.profile = { connect: { id: _user.profiles[0].id } };
|
||||
}
|
||||
await prisma.eventType.create({
|
||||
data: eventTypeData,
|
||||
});
|
||||
@@ -370,7 +396,7 @@ export const createUsersFixture = (
|
||||
const teamEvent = await createTeamEventType(user, team, scenario);
|
||||
if (scenario.teammates) {
|
||||
// Create Teammate users
|
||||
const teamMatesIds = [];
|
||||
const teamMates = [];
|
||||
for (const teammateObj of scenario.teammates) {
|
||||
const teamUser = await prisma.user.create({
|
||||
data: createUser(workerInfo, teammateObj),
|
||||
@@ -402,19 +428,49 @@ export const createUsersFixture = (
|
||||
}),
|
||||
store.page
|
||||
);
|
||||
teamMatesIds.push(teamUser.id);
|
||||
teamMates.push(teamUser);
|
||||
store.users.push(teammateFixture);
|
||||
}
|
||||
// Add Teammates to OrgUsers
|
||||
if (scenario.isOrg) {
|
||||
const orgProfilesCreate = teamMates
|
||||
.map((teamUser) => ({
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser.id,
|
||||
},
|
||||
},
|
||||
uid: v4(),
|
||||
username: teamUser.username || teamUser.email.split("@")[0],
|
||||
}))
|
||||
.concat([
|
||||
{
|
||||
user: { connect: { id: user.id } },
|
||||
uid: v4(),
|
||||
username: user.username || user.email.split("@")[0],
|
||||
},
|
||||
]);
|
||||
|
||||
const existingProfiles = await prisma.profile.findMany({
|
||||
where: {
|
||||
userId: _user.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.team.update({
|
||||
where: {
|
||||
id: team.id,
|
||||
},
|
||||
data: {
|
||||
orgUsers: {
|
||||
connect: teamMatesIds.map((userId) => ({ id: userId })).concat([{ id: user.id }]),
|
||||
},
|
||||
orgProfiles: _user.profiles.length
|
||||
? {
|
||||
connect: _user.profiles.map((profile) => ({ id: profile.id })),
|
||||
}
|
||||
: {
|
||||
create: orgProfilesCreate.filter(
|
||||
(profile) => !existingProfiles.map((p) => p.userId).includes(profile.user.connect.id)
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -615,7 +671,11 @@ type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & {
|
||||
// creates the actual user in the db.
|
||||
const createUser = (
|
||||
workerInfo: WorkerInfo,
|
||||
opts?: CustomUserOpts | null
|
||||
opts?:
|
||||
| (CustomUserOpts & {
|
||||
organizationId?: number | null;
|
||||
})
|
||||
| null
|
||||
): PrismaType.UserUncheckedCreateInput => {
|
||||
// build a unique name for our user
|
||||
const uname =
|
||||
@@ -666,25 +726,31 @@ const createUser = (
|
||||
throw new Error("Missing role for user in organization");
|
||||
}
|
||||
return {
|
||||
organizationId: organizationId || null,
|
||||
...(organizationId
|
||||
? {
|
||||
teams: {
|
||||
// Create membership
|
||||
create: [
|
||||
{
|
||||
team: {
|
||||
connect: {
|
||||
id: organizationId,
|
||||
},
|
||||
},
|
||||
accepted: true,
|
||||
role: MembershipRole.ADMIN,
|
||||
},
|
||||
],
|
||||
profiles: {
|
||||
create: {
|
||||
uid: ProfileRepository.generateProfileUid(),
|
||||
username: uname,
|
||||
organization: {
|
||||
connect: {
|
||||
id: organizationId,
|
||||
},
|
||||
}
|
||||
: null),
|
||||
},
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
// Create membership
|
||||
create: [
|
||||
{
|
||||
team: {
|
||||
connect: {
|
||||
id: organizationId,
|
||||
},
|
||||
},
|
||||
accepted: true,
|
||||
role: MembershipRole.ADMIN,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -286,7 +286,7 @@ test.describe("Organization", () => {
|
||||
page,
|
||||
emails,
|
||||
invitedUserEmail,
|
||||
`${team.name}'s admin invited you to join the organization ${org.name} on Cal.com`,
|
||||
`${team.name}'s admin invited you to join the team ${team.name} of organization ${org.name} on Cal.com`,
|
||||
"signup?token"
|
||||
);
|
||||
|
||||
|
||||
@@ -1490,6 +1490,7 @@
|
||||
"password_updated": "Password updated!",
|
||||
"pending_payment": "Pending payment",
|
||||
"pending_invites": "Pending Invites",
|
||||
"pending_organization_invites": "Pending Organization Invites",
|
||||
"not_on_cal": "Not on {{appName}}",
|
||||
"no_calendar_installed": "No calendar installed",
|
||||
"no_calendar_installed_description": "You have not yet connected any of your calendars",
|
||||
|
||||
@@ -269,7 +269,20 @@ function getTrpcHandlerData({
|
||||
...getSampleUserInSession(),
|
||||
...user,
|
||||
avatarUrl: user.avatarUrl || null,
|
||||
} satisfies TrpcSessionUser,
|
||||
profile: {
|
||||
upId: "",
|
||||
id: 1,
|
||||
name: "",
|
||||
avatarUrl: "",
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
username: user.username || "",
|
||||
organizationId: null,
|
||||
organization: null,
|
||||
bufferTime: 5,
|
||||
avatar: null,
|
||||
},
|
||||
} as unknown as NonNullable<TrpcSessionUser>,
|
||||
},
|
||||
input: input,
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ it("correctly handles 1 day", async () => {
|
||||
endTime: new Date(Date.UTC(1970, 1, 1, 17, 0, 0, 0)),
|
||||
date: null,
|
||||
scheduleId: 1,
|
||||
profileId: null,
|
||||
};
|
||||
|
||||
const result = availabilityAsString(availability, {
|
||||
@@ -32,6 +33,7 @@ it("correctly handles all days", async () => {
|
||||
endTime: new Date(Date.UTC(1970, 1, 1, 17, 0, 0, 0)),
|
||||
date: null,
|
||||
scheduleId: 1,
|
||||
profileId: null,
|
||||
};
|
||||
|
||||
const result = availabilityAsString(availability, {
|
||||
@@ -52,6 +54,7 @@ it("correctly handles staggered days", async () => {
|
||||
endTime: new Date(Date.UTC(1970, 1, 1, 17, 0, 0, 0)),
|
||||
date: null,
|
||||
scheduleId: 1,
|
||||
profileId: null,
|
||||
};
|
||||
|
||||
const result = availabilityAsString(availability, {
|
||||
@@ -72,6 +75,7 @@ it("correctly produces days and times - 12 hours", async () => {
|
||||
endTime: new Date(Date.UTC(1970, 1, 1, 17, 0, 0, 0)),
|
||||
date: null,
|
||||
scheduleId: 1,
|
||||
profileId: null,
|
||||
};
|
||||
|
||||
const result = availabilityAsString(availability, {
|
||||
@@ -92,6 +96,7 @@ it("correctly produces days and times - 24 hours", async () => {
|
||||
endTime: new Date(Date.UTC(1970, 1, 1, 17, 0, 0, 0)),
|
||||
date: null,
|
||||
scheduleId: 1,
|
||||
profileId: null,
|
||||
};
|
||||
|
||||
const result = availabilityAsString(availability, {
|
||||
|
||||
@@ -41,6 +41,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
hashedLink: undefined,
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
});
|
||||
expect(result.newUserIds).toEqual(undefined);
|
||||
expect(result.oldUserIds).toEqual(undefined);
|
||||
@@ -60,6 +61,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
hashedLink: undefined,
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
});
|
||||
expect(result.newUserIds).toEqual(undefined);
|
||||
expect(result.oldUserIds).toEqual(undefined);
|
||||
@@ -85,6 +87,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
hashedLink: undefined,
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
});
|
||||
expect(result.newUserIds).toEqual(undefined);
|
||||
expect(result.oldUserIds).toEqual(undefined);
|
||||
@@ -120,6 +123,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
hashedLink: undefined,
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
});
|
||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
@@ -166,6 +170,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
hashedLink: "somestring",
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
});
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
@@ -199,6 +204,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
hashedLink: undefined,
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
});
|
||||
expect(result.newUserIds).toEqual([]);
|
||||
expect(result.oldUserIds).toEqual([]);
|
||||
@@ -223,6 +229,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
hashedLink: undefined,
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
});
|
||||
// Have been called
|
||||
expect(result.newUserIds).toEqual([5]);
|
||||
@@ -259,6 +266,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
hashedLink: undefined,
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
});
|
||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
@@ -305,6 +313,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
hashedLink: undefined,
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
});
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
@@ -364,6 +373,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
hashedLink: undefined,
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
});
|
||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { weekdayToWeekIndex, type WeekDays } from "@calcom/lib/date-fns";
|
||||
import type { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
||||
import type { WorkflowActions, WorkflowTemplates, WorkflowTriggerEvents } from "@calcom/prisma/client";
|
||||
import type { SchedulingType } from "@calcom/prisma/enums";
|
||||
import type { BookingStatus } from "@calcom/prisma/enums";
|
||||
@@ -104,6 +105,7 @@ type InputUser = Omit<typeof TestData.users.example, "defaultScheduleId"> & {
|
||||
}[];
|
||||
destinationCalendar?: Prisma.DestinationCalendarCreateInput;
|
||||
weekStart?: string;
|
||||
profiles?: Prisma.ProfileUncheckedCreateWithoutUserInput[];
|
||||
};
|
||||
|
||||
export type InputEventType = {
|
||||
@@ -455,6 +457,7 @@ async function addUsersToDb(users: (Prisma.UserCreateInput & { schedules: Prisma
|
||||
include: {
|
||||
credentials: true,
|
||||
teams: true,
|
||||
profiles: true,
|
||||
schedules: {
|
||||
include: {
|
||||
availability: true,
|
||||
@@ -545,6 +548,15 @@ async function addUsers(users: InputUser[]) {
|
||||
},
|
||||
};
|
||||
}
|
||||
if (user.profiles) {
|
||||
newUser.profiles = {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error Not sure why this is not working
|
||||
createMany: {
|
||||
data: user.profiles,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
prismaUsersCreate.push(newUser);
|
||||
}
|
||||
@@ -937,6 +949,7 @@ export function getOrganizer({
|
||||
weekStart,
|
||||
teams,
|
||||
organizationId,
|
||||
profiles: [],
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
@@ -965,8 +978,18 @@ export function getScenarioData(
|
||||
) {
|
||||
const users = [organizer, ...usersApartFromOrganizer];
|
||||
if (org) {
|
||||
const orgId = org.id;
|
||||
if (!orgId) {
|
||||
throw new Error("If org is specified org.id is required");
|
||||
}
|
||||
users.forEach((user) => {
|
||||
user.organizationId = org.id;
|
||||
user.profiles = [
|
||||
{
|
||||
organizationId: orgId,
|
||||
username: user.username || "",
|
||||
uid: ProfileRepository.generateProfileUid(),
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ export function getMockRequestDataForBooking({
|
||||
}: {
|
||||
data: Partial<ReturnType<typeof getBasicMockRequestDataForBooking>> & {
|
||||
eventTypeId: number;
|
||||
user?: string;
|
||||
rescheduleUid?: string;
|
||||
bookingUid?: string;
|
||||
recurringEventId?: string;
|
||||
|
||||
@@ -8,7 +8,10 @@ export const getSampleUserInSession = function () {
|
||||
organization: {
|
||||
isOrgAdmin: false,
|
||||
metadata: null,
|
||||
id: 1,
|
||||
requestedSlug: null,
|
||||
},
|
||||
profile: null,
|
||||
defaultScheduleId: null,
|
||||
name: "",
|
||||
defaultBookerLayouts: null,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Prisma } from "@prisma/client";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { UserProfile } from "@calcom/types/UserProfile";
|
||||
|
||||
export async function checkInstalled(slug: string, userId: number) {
|
||||
const alreadyInstalled = await prisma.credential.findFirst({
|
||||
@@ -17,7 +18,10 @@ export async function checkInstalled(slug: string, userId: number) {
|
||||
|
||||
type InstallationArgs = {
|
||||
appType: string;
|
||||
userId: number;
|
||||
user: {
|
||||
id: number;
|
||||
profile: UserProfile;
|
||||
};
|
||||
slug: string;
|
||||
key?: Prisma.InputJsonValue;
|
||||
teamId?: number;
|
||||
@@ -28,7 +32,7 @@ type InstallationArgs = {
|
||||
|
||||
export async function createDefaultInstallation({
|
||||
appType,
|
||||
userId,
|
||||
user,
|
||||
slug,
|
||||
key = {},
|
||||
teamId,
|
||||
@@ -40,7 +44,7 @@ export async function createDefaultInstallation({
|
||||
data: {
|
||||
type: appType,
|
||||
key,
|
||||
...(teamId ? { teamId } : { userId }),
|
||||
...(teamId ? { teamId } : { userId: user.id }),
|
||||
appId: slug,
|
||||
subscriptionId,
|
||||
paymentStatus,
|
||||
|
||||
@@ -14,7 +14,7 @@ const handler: AppDeclarativeHandler = {
|
||||
url: "https://amie.so/signup",
|
||||
},
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function getHandler(req: NextApiRequest) {
|
||||
await checkInstalled(slug, session.user.id);
|
||||
await createDefaultInstallation({
|
||||
appType,
|
||||
userId: session.user.id,
|
||||
user: session.user,
|
||||
slug,
|
||||
key: {},
|
||||
teamId,
|
||||
|
||||
@@ -33,7 +33,7 @@ export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
await checkInstalled(slug, session.user.id);
|
||||
await createDefaultInstallation({
|
||||
appType,
|
||||
userId: session.user.id,
|
||||
user: session.user,
|
||||
slug,
|
||||
key: {
|
||||
apiKey,
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -14,7 +14,7 @@ const handler: AppDeclarativeHandler = {
|
||||
url: "https://cron.com",
|
||||
},
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -14,7 +14,7 @@ const handler: AppDeclarativeHandler = {
|
||||
url: "https://go.cal.com/deel",
|
||||
},
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -14,7 +14,7 @@ const handler: AppDeclarativeHandler = {
|
||||
url: "/apps/make/setup",
|
||||
},
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -14,7 +14,7 @@ const handler: AppDeclarativeHandler = {
|
||||
newTab: true,
|
||||
},
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function getHandler(req: NextApiRequest) {
|
||||
await checkInstalled(slug, session.user.id);
|
||||
await createDefaultInstallation({
|
||||
appType,
|
||||
userId: session.user.id,
|
||||
user: session.user,
|
||||
slug,
|
||||
key: {},
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ const handler: AppDeclarativeHandler = {
|
||||
url: "https://pipedream.com/apps/cal-com",
|
||||
},
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -16,13 +16,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
// Check that user is authenticated
|
||||
req.session = await getServerSession({ req, res });
|
||||
const { teamId } = req.query;
|
||||
const userId = req.session?.user.id;
|
||||
if (!userId) {
|
||||
const user = req.session?.user;
|
||||
if (!user) {
|
||||
throw new HttpError({ statusCode: 401, message: "You must be logged in to do this" });
|
||||
}
|
||||
const userId = user.id;
|
||||
await createDefaultInstallation({
|
||||
appType: `${appConfig.slug}_other_calendar`,
|
||||
userId: userId,
|
||||
user,
|
||||
slug: appConfig.slug,
|
||||
key: {},
|
||||
teamId: Number(teamId),
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -13,7 +13,7 @@ const handler: AppDeclarativeHandler = {
|
||||
url: "raycast://extensions/eluce2/cal-com-share-meeting-links?source=webstore",
|
||||
},
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function getHandler(req: NextApiRequest) {
|
||||
await checkInstalled(slug, session.user.id);
|
||||
await createDefaultInstallation({
|
||||
appType,
|
||||
userId: session.user.id,
|
||||
user: session.user,
|
||||
slug,
|
||||
key: {},
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { stringify } from "querystring";
|
||||
import z from "zod";
|
||||
|
||||
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { TRPCError } from "@calcom/trpc/server";
|
||||
@@ -34,15 +33,16 @@ export const getServerSideProps = async function getServerSideProps(
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
const { ProfileRepository } = await import("@calcom/lib/server/repository/profile");
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { form: formId, slug: _slug, pages: _pages, ...fieldsResponses } = queryParsed.data;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req);
|
||||
const { currentOrgDomain } = orgDomainConfig(context.req);
|
||||
|
||||
const form = await prisma.app_RoutingForms_Form.findFirst({
|
||||
where: {
|
||||
id: formId,
|
||||
user: {
|
||||
organization: isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null,
|
||||
...ProfileRepository._getPrismaWhereForProfilesOfOrg({ orgSlug: currentOrgDomain }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -19,19 +19,16 @@ export const getServerSideProps = async function getServerSideProps(
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req);
|
||||
const { currentOrgDomain } = orgDomainConfig(context.req);
|
||||
|
||||
const isEmbed = params.appPages[1] === "embed";
|
||||
const { ProfileRepository } = await import("@calcom/lib/server/repository/profile");
|
||||
|
||||
const form = await prisma.app_RoutingForms_Form.findFirst({
|
||||
where: {
|
||||
id: formId,
|
||||
user: {
|
||||
organization: isValidOrgDomain
|
||||
? {
|
||||
slug: currentOrgDomain,
|
||||
}
|
||||
: null,
|
||||
...ProfileRepository._getPrismaWhereForProfilesOfOrg({ orgSlug: currentOrgDomain }),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
|
||||
@@ -71,11 +71,13 @@ export const formsHandler = async ({ ctx, input }: FormsHandlerOptions) => {
|
||||
};
|
||||
|
||||
export default formsHandler;
|
||||
type SupportedFilters = Omit<NonNullable<NonNullable<TFormSchema>["filters"]>, "upIds"> | undefined;
|
||||
|
||||
export function getPrismaWhereFromFilters(
|
||||
user: {
|
||||
id: number;
|
||||
},
|
||||
filters: NonNullable<TFormSchema>["filters"]
|
||||
filters: SupportedFilters
|
||||
) {
|
||||
const where = {
|
||||
OR: [] as Prisma.App_RoutingForms_FormWhereInput[],
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -14,7 +14,7 @@ const handler: AppDeclarativeHandler = {
|
||||
url: "https://skiff.com/blog/skiff-cal-com-launch",
|
||||
},
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -14,7 +14,7 @@ const handler: AppDeclarativeHandler = {
|
||||
url: "https://example.com/link",
|
||||
},
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -13,7 +13,7 @@ const handler: AppDeclarativeHandler = {
|
||||
url: "/apps/typeform/how-to-use",
|
||||
},
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -14,7 +14,7 @@ const handler: AppDeclarativeHandler = {
|
||||
url: "https://cal.com/blog/cal-plus-vimcal",
|
||||
},
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -10,7 +10,7 @@ const handler: AppDeclarativeHandler = {
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function getHandler(req: NextApiRequest) {
|
||||
await checkInstalled(slug, session.user.id);
|
||||
await createDefaultInstallation({
|
||||
appType,
|
||||
userId: session.user.id,
|
||||
user: session.user,
|
||||
slug,
|
||||
key: {},
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ const handler: AppDeclarativeHandler = {
|
||||
url: "https://wordpress.org/plugins/cal-com/",
|
||||
},
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
|
||||
export const OrganizerRequestEmail = (props: React.ComponentProps<typeof OrganizerScheduledEmail>) => {
|
||||
const seedData = { bookingUid: props.calEvent.uid, userId: props.calEvent.organizer.id };
|
||||
const token = symmetricEncrypt(JSON.stringify(seedData), process.env.CALENDSO_ENCRYPTION_KEY || "");
|
||||
//TODO: We should switch to using org domain if available
|
||||
const actionHref = `${WEBAPP_URL}/api/link/?token=${encodeURIComponent(token)}`;
|
||||
return (
|
||||
<OrganizerScheduledEmail
|
||||
|
||||
@@ -11,6 +11,7 @@ type TeamInvite = {
|
||||
teamName: string;
|
||||
joinLink: string;
|
||||
isCalcomMember: boolean;
|
||||
isAutoJoin: boolean;
|
||||
isOrg: boolean;
|
||||
parentTeamName: string | undefined;
|
||||
};
|
||||
@@ -75,7 +76,9 @@ export const TeamInviteEmail = (
|
||||
</p>
|
||||
<div style={{ display: "flex", justifyContent: "center" }}>
|
||||
<CallToAction
|
||||
label={props.language(props.isCalcomMember ? "email_user_cta" : "create_your_account")}
|
||||
label={props.language(
|
||||
props.isCalcomMember ? (props.isAutoJoin ? "login" : "email_user_cta") : "create_your_account"
|
||||
)}
|
||||
href={props.joinLink}
|
||||
endIconName="linkIcon"
|
||||
/>
|
||||
@@ -163,34 +166,3 @@ export const TeamInviteEmail = (
|
||||
</V2BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
|
||||
const EmailStep = (props: { translationString: string; iconsrc: string }) => {
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "#E5E7EB",
|
||||
borderRadius: "48px",
|
||||
minHeight: "48px",
|
||||
minWidth: "48px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
color: "white",
|
||||
marginRight: "16px",
|
||||
}}>
|
||||
<img src={props.iconsrc} alt="#" style={{ height: "24px", width: "auto" }} />
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontStyle: "normal",
|
||||
fontWeight: 500,
|
||||
fontSize: "18px",
|
||||
lineHeight: "20px",
|
||||
color: "#black",
|
||||
}}>
|
||||
<>{props.translationString}</>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,10 @@ export type TeamInvite = {
|
||||
teamName: string;
|
||||
joinLink: string;
|
||||
isCalcomMember: boolean;
|
||||
/**
|
||||
* We ideally should have a separate email for auto-join(when a user is automatically accepted into a team/org), but we don't have one yet.
|
||||
*/
|
||||
isAutoJoin: boolean;
|
||||
isOrg: boolean;
|
||||
parentTeamName: string | undefined;
|
||||
};
|
||||
|
||||
@@ -5,8 +5,12 @@ import { getToken } from "next-auth/jwt";
|
||||
|
||||
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["getServerSession"] });
|
||||
/**
|
||||
* Stores the session in memory using the stringified token as the key.
|
||||
*
|
||||
@@ -29,6 +33,7 @@ export async function getServerSession(options: {
|
||||
res?: NextApiResponse | GetServerSidePropsContext["res"];
|
||||
authOptions?: AuthOptions;
|
||||
}) {
|
||||
log.debug("Getting server session");
|
||||
const { req, authOptions: { secret } = {} } = options;
|
||||
|
||||
const token = await getToken({
|
||||
@@ -37,6 +42,7 @@ export async function getServerSession(options: {
|
||||
});
|
||||
|
||||
if (!token || !token.email || !token.sub) {
|
||||
log.debug("Couldnt get token");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -46,7 +52,7 @@ export async function getServerSession(options: {
|
||||
return cachedSession;
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
const userFromDb = await prisma.user.findUnique({
|
||||
where: {
|
||||
email: token.email.toLowerCase(),
|
||||
},
|
||||
@@ -54,12 +60,29 @@ export async function getServerSession(options: {
|
||||
// cacheStrategy: { ttl: 60, swr: 1 },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
if (!userFromDb) {
|
||||
log.debug("No user found");
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasValidLicense = await checkLicense(prisma);
|
||||
|
||||
let upId = token.upId;
|
||||
|
||||
if (!upId) {
|
||||
upId = `usr-${userFromDb.id}`;
|
||||
}
|
||||
|
||||
if (!upId) {
|
||||
log.error("No upId found for session", { userId: userFromDb.id });
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await UserRepository.enrichUserWithTheProfile({
|
||||
user: userFromDb,
|
||||
upId,
|
||||
});
|
||||
|
||||
const session: Session = {
|
||||
hasValidLicense,
|
||||
expires: new Date(typeof token.exp === "number" ? token.exp * 1000 : Date.now()).toISOString(),
|
||||
@@ -75,7 +98,10 @@ export async function getServerSession(options: {
|
||||
belongsToActiveTeam: token.belongsToActiveTeam,
|
||||
org: token.org,
|
||||
locale: user.locale ?? undefined,
|
||||
profile: user.profile,
|
||||
},
|
||||
profileId: token.profileId,
|
||||
upId,
|
||||
};
|
||||
|
||||
if (token?.impersonatedBy?.id) {
|
||||
@@ -98,5 +124,6 @@ export async function getServerSession(options: {
|
||||
|
||||
CACHE.set(JSON.stringify(token), session);
|
||||
|
||||
log.debug("Returned session", safeStringify(session));
|
||||
return session;
|
||||
}
|
||||
|
||||
@@ -75,21 +75,5 @@ export default function CalComAdapter(prismaClient: PrismaClient) {
|
||||
// @NOTE: All methods below here are not being used but leaved if they are required
|
||||
unlinkAccount: (provider_providerAccountId: Prisma.AccountProviderProviderAccountIdCompoundUniqueInput) =>
|
||||
prismaClient.account.delete({ where: { provider_providerAccountId } }),
|
||||
async getSessionAndUser(sessionToken: string) {
|
||||
const userAndSession = await prismaClient.session.findUnique({
|
||||
where: { sessionToken },
|
||||
include: { user: true },
|
||||
});
|
||||
if (!userAndSession) return null;
|
||||
const { user, ...session } = userAndSession;
|
||||
return { user, session };
|
||||
},
|
||||
createSession: (data: Prisma.SessionCreateInput) => prismaClient.session.create({ data }),
|
||||
updateSession: (data: Prisma.SessionUpdateInput) =>
|
||||
prismaClient.session.update({
|
||||
where: { sessionToken: typeof data.sessionToken === "string" ? data.sessionToken : undefined },
|
||||
data,
|
||||
}),
|
||||
deleteSession: (sessionToken: string) => prismaClient.session.delete({ where: { sessionToken } }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Membership, Team, UserPermissionRole } from "@prisma/client";
|
||||
import type { AuthOptions, Session } from "next-auth";
|
||||
import type { JWT } from "next-auth/jwt";
|
||||
import { encode } from "next-auth/jwt";
|
||||
import type { Provider } from "next-auth/providers";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
@@ -11,11 +12,15 @@ import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/Imperso
|
||||
import { getOrgFullOrigin, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { clientSecretVerifier, hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { ENABLE_PROFILE_SWITCHER, IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { symmetricDecrypt, symmetricEncrypt } from "@calcom/lib/crypto";
|
||||
import { defaultCookies } from "@calcom/lib/default-cookies";
|
||||
import { isENVDev } from "@calcom/lib/env";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { randomString } from "@calcom/lib/random";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { IdentityProvider, MembershipRole } from "@calcom/prisma/enums";
|
||||
@@ -26,6 +31,7 @@ import { isPasswordValid } from "./isPasswordValid";
|
||||
import CalComAdapter from "./next-auth-custom-adapter";
|
||||
import { verifyPassword } from "./verifyPassword";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["next-auth-options"] });
|
||||
const GOOGLE_API_CREDENTIALS = process.env.GOOGLE_API_CREDENTIALS || "{}";
|
||||
const { client_id: GOOGLE_CLIENT_ID, client_secret: GOOGLE_CLIENT_SECRET } =
|
||||
JSON.parse(GOOGLE_API_CREDENTIALS)?.web || {};
|
||||
@@ -100,38 +106,7 @@ const providers: Provider[] = [
|
||||
throw new Error(ErrorCode.InternalServerError);
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
email: credentials.email.toLowerCase(),
|
||||
},
|
||||
select: {
|
||||
locked: true,
|
||||
role: true,
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
email: true,
|
||||
metadata: true,
|
||||
identityProvider: true,
|
||||
password: true,
|
||||
organizationId: true,
|
||||
twoFactorEnabled: true,
|
||||
twoFactorSecret: true,
|
||||
backupCodes: true,
|
||||
locale: true,
|
||||
organization: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
include: {
|
||||
team: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const user = await UserRepository.findByEmailAndIncludeProfiles({ email: credentials.email });
|
||||
// Don't leak information about it being username or password that is invalid
|
||||
if (!user) {
|
||||
throw new Error(ErrorCode.IncorrectEmailPassword);
|
||||
@@ -253,8 +228,8 @@ const providers: Provider[] = [
|
||||
name: user.name,
|
||||
role: validateRole(user.role),
|
||||
belongsToActiveTeam: hasActiveTeams,
|
||||
organizationId: user.organizationId,
|
||||
locale: user.locale,
|
||||
profile: user.allProfiles[0],
|
||||
};
|
||||
},
|
||||
}),
|
||||
@@ -291,15 +266,28 @@ if (isSAMLLoginEnabled) {
|
||||
params: { grant_type: "authorization_code" },
|
||||
},
|
||||
userinfo: `${WEBAPP_URL}/api/auth/saml/userinfo`,
|
||||
profile: (profile) => {
|
||||
profile: async (profile: {
|
||||
id?: number;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
locale?: string;
|
||||
}) => {
|
||||
const user = await UserRepository.findByEmailAndIncludeProfiles({ email: profile.email || "" });
|
||||
if (!user) {
|
||||
throw new Error(ErrorCode.UserNotFound);
|
||||
}
|
||||
|
||||
const [userProfile] = user.allProfiles;
|
||||
return {
|
||||
id: profile.id || "",
|
||||
id: profile.id || 0,
|
||||
firstName: profile.firstName || "",
|
||||
lastName: profile.lastName || "",
|
||||
email: profile.email || "",
|
||||
name: `${profile.firstName || ""} ${profile.lastName || ""}`.trim(),
|
||||
email_verified: true,
|
||||
locale: profile.locale,
|
||||
profile: userProfile,
|
||||
};
|
||||
},
|
||||
options: {
|
||||
@@ -351,6 +339,12 @@ if (isSAMLLoginEnabled) {
|
||||
}
|
||||
|
||||
const { id, firstName, lastName, email } = userInfo;
|
||||
const user = await UserRepository.findByEmailAndIncludeProfiles({ email });
|
||||
if (!user) {
|
||||
throw new Error(ErrorCode.UserNotFound);
|
||||
}
|
||||
|
||||
const [profile] = user.allProfiles;
|
||||
|
||||
return {
|
||||
id: id as unknown as number,
|
||||
@@ -359,6 +353,7 @@ if (isSAMLLoginEnabled) {
|
||||
email,
|
||||
name: `${firstName} ${lastName}`.trim(),
|
||||
email_verified: true,
|
||||
profile,
|
||||
};
|
||||
},
|
||||
})
|
||||
@@ -427,15 +422,31 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
},
|
||||
providers,
|
||||
callbacks: {
|
||||
async jwt({ token, user, account, trigger, session }) {
|
||||
async jwt({
|
||||
// Always available but with a little difference in value
|
||||
token,
|
||||
// Available only in case of signIn, signUp or useSession().update call.
|
||||
trigger,
|
||||
// Available when useSession().update is called. The value will be the POST data
|
||||
session,
|
||||
// Available only in the first call once the user signs in. Not available in subsequent calls
|
||||
user,
|
||||
// Available only in the first call once the user signs in. Not available in subsequent calls
|
||||
account,
|
||||
}) {
|
||||
log.debug("callbacks:jwt", safeStringify({ token, user, account, trigger, session }));
|
||||
|
||||
// The data available in 'session' depends on what data was supplied in update method call of session
|
||||
if (trigger === "update") {
|
||||
return {
|
||||
...token,
|
||||
profileId: session?.profileId ?? token.profileId ?? null,
|
||||
upId: session?.upId ?? token.upId ?? null,
|
||||
locale: session?.locale ?? token.locale ?? "en",
|
||||
name: session?.name ?? token.name,
|
||||
username: session?.username ?? token.username,
|
||||
email: session?.email ?? token.email,
|
||||
};
|
||||
} as JWT;
|
||||
}
|
||||
const autoMergeIdentities = async () => {
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
@@ -446,16 +457,9 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
username: true,
|
||||
name: true,
|
||||
email: true,
|
||||
organization: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
metadata: true,
|
||||
},
|
||||
},
|
||||
role: true,
|
||||
locale: true,
|
||||
movedToProfileId: true,
|
||||
teams: {
|
||||
include: {
|
||||
team: true,
|
||||
@@ -470,24 +474,41 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
|
||||
// Check if the existingUser has any active teams
|
||||
const belongsToActiveTeam = checkIfUserBelongsToActiveTeam(existingUser);
|
||||
const { teams: _teams, organization, ...existingUserWithoutTeamsField } = existingUser;
|
||||
const { teams: _teams, ...existingUserWithoutTeamsField } = existingUser;
|
||||
const allProfiles = await ProfileRepository.findAllProfilesForUserIncludingMovedUser(existingUser);
|
||||
log.debug(
|
||||
"callbacks:jwt:autoMergeIdentities",
|
||||
safeStringify({
|
||||
allProfiles,
|
||||
})
|
||||
);
|
||||
const { upId } = determineProfile({ profiles: allProfiles, token });
|
||||
|
||||
const parsedOrgMetadata = teamMetadataSchema.parse(organization?.metadata ?? {});
|
||||
const profile = await ProfileRepository.findByUpId(upId);
|
||||
if (!profile) {
|
||||
throw new Error("Profile not found");
|
||||
}
|
||||
|
||||
const profileOrg = profile?.organization;
|
||||
|
||||
return {
|
||||
...existingUserWithoutTeamsField,
|
||||
...token,
|
||||
profileId: profile.id,
|
||||
upId,
|
||||
belongsToActiveTeam,
|
||||
org: organization
|
||||
// All organizations in the token would be too big to store. It breaks the sessions request.
|
||||
// So, we just set the currently switched organization only here.
|
||||
org: profileOrg
|
||||
? {
|
||||
id: organization.id,
|
||||
name: organization.name,
|
||||
slug: organization.slug ?? parsedOrgMetadata?.requestedSlug ?? "",
|
||||
fullDomain: getOrgFullOrigin(organization.slug ?? parsedOrgMetadata?.requestedSlug ?? ""),
|
||||
id: profileOrg.id,
|
||||
name: profileOrg.name,
|
||||
slug: profileOrg.slug ?? profileOrg.requestedSlug ?? "",
|
||||
fullDomain: getOrgFullOrigin(profileOrg.slug ?? profileOrg.requestedSlug ?? ""),
|
||||
domainSuffix: subdomainSuffix(),
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
: null,
|
||||
} as JWT;
|
||||
};
|
||||
if (!user) {
|
||||
return await autoMergeIdentities();
|
||||
@@ -512,7 +533,9 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
belongsToActiveTeam: user?.belongsToActiveTeam,
|
||||
org: user?.org,
|
||||
locale: user?.locale,
|
||||
};
|
||||
profileId: user.profile?.id ?? token.profileId ?? null,
|
||||
upId: user.profile?.upId ?? token.upId ?? null,
|
||||
} as JWT;
|
||||
}
|
||||
|
||||
// The arguments above are from the provider so we need to look up the
|
||||
@@ -551,15 +574,23 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
|
||||
org: token?.org,
|
||||
locale: existingUser.locale,
|
||||
};
|
||||
} as JWT;
|
||||
}
|
||||
|
||||
if (account.type === "email") {
|
||||
return await autoMergeIdentities();
|
||||
}
|
||||
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
async session({ session, token, user }) {
|
||||
log.debug("callbacks:session - Session callback called", safeStringify({ session, token, user }));
|
||||
const hasValidLicense = await checkLicense(prisma);
|
||||
const profileId = token.profileId;
|
||||
const calendsoSession: Session = {
|
||||
...session,
|
||||
profileId,
|
||||
upId: token.upId || session.upId,
|
||||
hasValidLicense,
|
||||
user: {
|
||||
...session.user,
|
||||
@@ -576,7 +607,19 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
return calendsoSession;
|
||||
},
|
||||
async signIn(params) {
|
||||
const { user, account, profile } = params;
|
||||
const {
|
||||
/**
|
||||
* Available when Credentials provider is used - Has the value returned by authorize callback
|
||||
*/
|
||||
user,
|
||||
/**
|
||||
* Available when Credentials provider is used - Has the value submitted as the body of the HTTP POST submission
|
||||
*/
|
||||
profile,
|
||||
account,
|
||||
} = params;
|
||||
|
||||
log.debug("callbacks:signin", safeStringify(params));
|
||||
|
||||
if (account?.provider === "email") {
|
||||
return true;
|
||||
@@ -814,6 +857,9 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* Used to handle the navigation right after successful login or logout
|
||||
*/
|
||||
async redirect({ url, baseUrl }) {
|
||||
// Allows relative callback URLs
|
||||
if (url.startsWith("/")) return `${baseUrl}${url}`;
|
||||
@@ -823,3 +869,27 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Identifies the profile the user should be logged into.
|
||||
*/
|
||||
const determineProfile = ({
|
||||
token,
|
||||
profiles,
|
||||
}: {
|
||||
token: JWT;
|
||||
profiles: { id: number | null; upId: string }[];
|
||||
}) => {
|
||||
// If profile switcher is disabled, we can only show the first profile.
|
||||
if (!ENABLE_PROFILE_SWITCHER) {
|
||||
return profiles[0];
|
||||
}
|
||||
|
||||
if (token.upId) {
|
||||
// Otherwise use what's in the token
|
||||
return { profileId: token.profileId, upId: token.upId as string };
|
||||
}
|
||||
|
||||
// If there is just one profile it has to be the one we want to log into.
|
||||
return profiles[0];
|
||||
};
|
||||
|
||||
@@ -127,6 +127,9 @@ async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) {
|
||||
where: {
|
||||
id: foundToken.teamId,
|
||||
},
|
||||
include: {
|
||||
parent: true,
|
||||
},
|
||||
});
|
||||
if (team) {
|
||||
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
|
||||
@@ -157,10 +160,10 @@ async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) {
|
||||
closeComUpsertTeamUser(team, user, membership.role);
|
||||
|
||||
// Accept any child team invites for orgs.
|
||||
if (team.parentId) {
|
||||
if (team.parent) {
|
||||
await joinAnyChildTeamOnOrgInvite({
|
||||
userId: user.id,
|
||||
orgId: team.parentId,
|
||||
org: team.parent,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
where: {
|
||||
id: foundToken.teamId,
|
||||
},
|
||||
include: {
|
||||
parent: true,
|
||||
},
|
||||
});
|
||||
if (team) {
|
||||
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
|
||||
@@ -97,10 +100,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
closeComUpsertTeamUser(team, user, membership.role);
|
||||
|
||||
// Accept any child team invites for orgs.
|
||||
if (team.parentId) {
|
||||
if (team.parent) {
|
||||
await joinAnyChildTeamOnOrgInvite({
|
||||
userId: user.id,
|
||||
orgId: team.parentId,
|
||||
org: team.parent,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,11 @@ const usernameCheck = async (usernameRaw: string) => {
|
||||
const username = slugify(usernameRaw);
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { username, organizationId: null },
|
||||
where: {
|
||||
username,
|
||||
// Simply remove it when we drop organizationId column
|
||||
organizationId: null,
|
||||
},
|
||||
select: {
|
||||
username: true,
|
||||
},
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import type z from "zod";
|
||||
|
||||
import { updateNewTeamMemberEventTypes } from "@calcom/lib/server/queries";
|
||||
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { Team, User } from "@calcom/prisma/client";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import type { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { getOrgUsernameFromEmail } from "./getOrgUsernameFromEmail";
|
||||
|
||||
export const createOrUpdateMemberships = async ({
|
||||
teamMetadata,
|
||||
user,
|
||||
@@ -17,13 +20,39 @@ export const createOrUpdateMemberships = async ({
|
||||
}) => {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
if (teamMetadata?.isOrganization) {
|
||||
await tx.user.update({
|
||||
const dbUser = await tx.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
organizationId: team.id,
|
||||
},
|
||||
select: {
|
||||
username: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Ideally dbUser.username should never be null, but just in case.
|
||||
// This method being called only during signup means that dbUser.username should be the correct org username
|
||||
const orgUsername =
|
||||
dbUser.username || getOrgUsernameFromEmail(dbUser.email, teamMetadata?.orgAutoAcceptEmail ?? null);
|
||||
await tx.profile.upsert({
|
||||
create: {
|
||||
uid: ProfileRepository.generateProfileUid(),
|
||||
userId: user.id,
|
||||
organizationId: team.id,
|
||||
username: orgUsername,
|
||||
},
|
||||
update: {
|
||||
username: orgUsername,
|
||||
},
|
||||
where: {
|
||||
userId_organizationId: {
|
||||
userId: user.id,
|
||||
organizationId: team.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
const membership = await tx.membership.upsert({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
|
||||
export const getOrgUsernameFromEmail = (email: string, autoAcceptEmailDomain: string) => {
|
||||
export const getOrgUsernameFromEmail = (email: string, autoAcceptEmailDomain: string | null) => {
|
||||
const [emailUser, emailDomain = ""] = email.split("@");
|
||||
const username =
|
||||
emailDomain === autoAcceptEmailDomain
|
||||
|
||||
@@ -1,38 +1,58 @@
|
||||
import { updateNewTeamMemberEventTypes } from "@calcom/lib/server/queries";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { Team } from "@calcom/prisma/client";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
export async function joinOrganization({
|
||||
organizationId,
|
||||
userId,
|
||||
}: {
|
||||
userId: number;
|
||||
organizationId: number;
|
||||
}) {
|
||||
return await prisma.user.update({
|
||||
import { getOrgUsernameFromEmail } from "./getOrgUsernameFromEmail";
|
||||
|
||||
export async function joinAnyChildTeamOnOrgInvite({ userId, org }: { userId: number; org: Team }) {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
organizationId: organizationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
const orgMetadata = teamMetadataSchema.parse(org.metadata);
|
||||
|
||||
const orgUsername =
|
||||
user.username || getOrgUsernameFromEmail(user.email, orgMetadata?.orgAutoAcceptEmail ?? null);
|
||||
|
||||
export async function joinAnyChildTeamOnOrgInvite({ userId, orgId }: { userId: number; orgId: number }) {
|
||||
await prisma.$transaction([
|
||||
// Simply remove this update when we remove the `organizationId` field from the user table
|
||||
prisma.user.update({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
organizationId: orgId,
|
||||
organizationId: org.id,
|
||||
},
|
||||
}),
|
||||
prisma.profile.upsert({
|
||||
create: {
|
||||
uid: ProfileRepository.generateProfileUid(),
|
||||
userId: userId,
|
||||
organizationId: org.id,
|
||||
username: orgUsername,
|
||||
},
|
||||
update: {
|
||||
username: orgUsername,
|
||||
},
|
||||
where: {
|
||||
userId_organizationId: {
|
||||
userId: user.id,
|
||||
organizationId: org.id,
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.membership.updateMany({
|
||||
where: {
|
||||
userId,
|
||||
team: {
|
||||
id: orgId,
|
||||
id: org.id,
|
||||
},
|
||||
accepted: false,
|
||||
},
|
||||
@@ -44,7 +64,7 @@ export async function joinAnyChildTeamOnOrgInvite({ userId, orgId }: { userId: n
|
||||
where: {
|
||||
userId,
|
||||
team: {
|
||||
parentId: orgId,
|
||||
parentId: org.id,
|
||||
},
|
||||
accepted: false,
|
||||
},
|
||||
@@ -54,5 +74,5 @@ export async function joinAnyChildTeamOnOrgInvite({ userId, orgId }: { userId: n
|
||||
}),
|
||||
]);
|
||||
|
||||
await updateNewTeamMemberEventTypes(userId, orgId);
|
||||
await updateNewTeamMemberEventTypes(userId, org.id);
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ export const BookerSeo = (props: BookerSeoProps) => {
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
||||
const profileName = event?.profile?.name ?? "";
|
||||
const profileImage = event?.profile?.image;
|
||||
const profileName = event?.profile.name ?? "";
|
||||
const profileImage = event?.profile.image;
|
||||
const title = event?.title ?? "";
|
||||
return (
|
||||
<HeadSeo
|
||||
|
||||
@@ -40,7 +40,7 @@ import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/
|
||||
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
||||
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
|
||||
import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled";
|
||||
import { userOrgQuery } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import {
|
||||
allowDisablingAttendeeConfirmationEmails,
|
||||
allowDisablingHostConfirmationEmails,
|
||||
@@ -69,6 +69,7 @@ import { getPiiFreeCalendarEvent, getPiiFreeEventType, getPiiFreeUser } from "@c
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { checkBookingLimits, checkDurationLimits, getLuckyUser } from "@calcom/lib/server";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import { slugify } from "@calcom/lib/slugify";
|
||||
import { updateWebUser as syncServicesUpdateWebUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
@@ -185,7 +186,7 @@ export async function refreshCredentials(
|
||||
* Gets credentials from the user, team, and org if applicable
|
||||
*
|
||||
*/
|
||||
export const getAllCredentials = async (
|
||||
const getAllCredentials = async (
|
||||
user: User & { credentials: CredentialPayload[] },
|
||||
eventType: Awaited<ReturnType<typeof getEventTypesFromDB>>
|
||||
) => {
|
||||
@@ -223,11 +224,15 @@ export const getAllCredentials = async (
|
||||
}
|
||||
}
|
||||
|
||||
const { profile } = await UserRepository.enrichUserWithItsProfile({
|
||||
user: user,
|
||||
});
|
||||
|
||||
// If the user is a part of an organization, query for the organization's credentials
|
||||
if (user?.organizationId) {
|
||||
if (profile?.organizationId) {
|
||||
const org = await prisma.team.findUnique({
|
||||
where: {
|
||||
id: user.organizationId,
|
||||
id: profile.organizationId,
|
||||
},
|
||||
select: {
|
||||
credentials: {
|
||||
@@ -366,11 +371,6 @@ export const getEventTypesFromDB = async (eventTypeId: number) => {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
...userSelect.select,
|
||||
organization: {
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -409,18 +409,10 @@ const loadUsers = async (eventType: NewBookingEventType, dynamicUserList: string
|
||||
if (!Array.isArray(dynamicUserList) || dynamicUserList.length === 0) {
|
||||
throw new Error("dynamicUserList is not properly defined or empty.");
|
||||
}
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
username: { in: dynamicUserList },
|
||||
organization: userOrgQuery(req),
|
||||
},
|
||||
select: {
|
||||
...userSelect.select,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
metadata: true,
|
||||
},
|
||||
const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig(req);
|
||||
const users = await findUsersByUsername({
|
||||
usernameList: dynamicUserList,
|
||||
orgSlug: isValidOrgDomain ? currentOrgDomain : null,
|
||||
});
|
||||
|
||||
return users;
|
||||
@@ -1320,7 +1312,6 @@ async function handler(
|
||||
const [organizerUser] = users;
|
||||
|
||||
const tOrganizer = await getTranslation(organizerUser?.locale ?? "en", "common");
|
||||
|
||||
const allCredentials = await getAllCredentials(organizerUser, eventType);
|
||||
|
||||
const { userReschedulingIsOwner, isConfirmedByDefault } = getRequiresConfirmationFlags({
|
||||
@@ -1449,11 +1440,20 @@ async function handler(
|
||||
});
|
||||
// For bookings made before introducing iCalSequence, assume that the sequence should start at 1. For new bookings start at 0.
|
||||
const iCalSequence = getICalSequence(originalRescheduledBooking);
|
||||
const organizerOrganizationProfile = await prisma.profile.findFirst({
|
||||
where: {
|
||||
userId: organizerUser.id,
|
||||
username: dynamicUserList[0],
|
||||
},
|
||||
});
|
||||
|
||||
const organizerOrganizationId = organizerOrganizationProfile?.organizationId;
|
||||
const bookerUrl = eventType.team
|
||||
? await getBookerBaseUrl(eventType.team.parentId)
|
||||
: await getBookerBaseUrl(organizerOrganizationId ?? null);
|
||||
|
||||
let evt: CalendarEvent = {
|
||||
bookerUrl: eventType.team
|
||||
? await getBookerBaseUrl({ organizationId: eventType.team.parentId })
|
||||
: await getBookerBaseUrl(organizerUser),
|
||||
bookerUrl,
|
||||
type: eventType.slug,
|
||||
title: getEventName(eventNameObject), //this needs to be either forced in english, or fetched for each attendee and organizer separately
|
||||
description: eventType.description,
|
||||
@@ -2401,3 +2401,40 @@ function handleCustomInputs(
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is mostly same as the one in UserRepository but it includes a lot more relations which are specific requirement here
|
||||
* TODO: Figure out how to keep it in UserRepository and use it here
|
||||
*/
|
||||
export const findUsersByUsername = async ({
|
||||
usernameList,
|
||||
orgSlug,
|
||||
}: {
|
||||
orgSlug: string | null;
|
||||
usernameList: string[];
|
||||
}) => {
|
||||
log.debug("findUsersByUsername", { usernameList, orgSlug });
|
||||
const { where, profiles } = await UserRepository._getWhereClauseForFindingUsersByUsername({
|
||||
orgSlug,
|
||||
usernameList,
|
||||
});
|
||||
return (
|
||||
await prisma.user.findMany({
|
||||
where,
|
||||
select: {
|
||||
...userSelect.select,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
metadata: true,
|
||||
},
|
||||
})
|
||||
).map((user) => {
|
||||
const profile = profiles?.find((profile) => profile.user.id === user.id) ?? null;
|
||||
return {
|
||||
...user,
|
||||
organizationId: profile?.organizationId ?? null,
|
||||
profile,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -157,6 +157,7 @@ describe("handleNewBooking", () => {
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
user: organizer.username,
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
@@ -1991,6 +1992,7 @@ describe("handleNewBooking", () => {
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
user: organizer.username,
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
|
||||
@@ -4,15 +4,30 @@ import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getSession } from "@calcom/features/auth/lib/getSession";
|
||||
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import type { Membership } from "@calcom/prisma/client";
|
||||
import type { OrgProfile, PersonalProfile, UserAsPersonalProfile } from "@calcom/types/UserProfile";
|
||||
|
||||
const teamIdschema = z.object({
|
||||
teamId: z.preprocess((a) => parseInt(z.string().parse(a), 10), z.number().positive()),
|
||||
});
|
||||
|
||||
type ProfileType =
|
||||
| UserAsPersonalProfile
|
||||
| PersonalProfile
|
||||
| (Omit<OrgProfile, "organization"> & {
|
||||
organization: OrgProfile["organization"] & {
|
||||
members: Membership[];
|
||||
};
|
||||
});
|
||||
|
||||
const auditAndReturnNextUser = async (
|
||||
impersonatedUser: Pick<User, "id" | "username" | "email" | "name" | "role" | "organizationId" | "locale">,
|
||||
impersonatedUser: Pick<User, "id" | "username" | "email" | "name" | "role" | "locale"> & {
|
||||
organizationId: number | null;
|
||||
profile: ProfileType;
|
||||
},
|
||||
impersonatedByUID: number,
|
||||
hasTeam?: boolean,
|
||||
isReturningToSelf?: boolean
|
||||
@@ -42,6 +57,7 @@ const auditAndReturnNextUser = async (
|
||||
belongsToActiveTeam: hasTeam,
|
||||
organizationId: impersonatedUser.organizationId,
|
||||
locale: impersonatedUser.locale,
|
||||
profile: impersonatedUser.profile,
|
||||
};
|
||||
|
||||
if (!isReturningToSelf) {
|
||||
@@ -136,7 +152,6 @@ async function getImpersonatedUser({
|
||||
role: true,
|
||||
name: true,
|
||||
email: true,
|
||||
organizationId: true,
|
||||
disableImpersonation: true,
|
||||
locale: true,
|
||||
teams: {
|
||||
@@ -154,7 +169,13 @@ async function getImpersonatedUser({
|
||||
throw new Error("This user does not exist");
|
||||
}
|
||||
|
||||
return impersonatedUser;
|
||||
const profile = await findProfile(impersonatedUser);
|
||||
|
||||
return {
|
||||
...impersonatedUser,
|
||||
organizationId: profile.organization?.id ?? null,
|
||||
profile,
|
||||
};
|
||||
}
|
||||
|
||||
async function isReturningToSelf({ session, creds }: { session: Session | null; creds: Credentials | null }) {
|
||||
@@ -195,6 +216,8 @@ async function isReturningToSelf({ session, creds }: { session: Session | null;
|
||||
if (returningUser.role !== "ADMIN" && !returningUser.organizationId) return;
|
||||
|
||||
const hasTeams = returningUser.teams.length >= 1;
|
||||
|
||||
const profile = await findProfile(returningUser);
|
||||
return {
|
||||
user: {
|
||||
id: returningUser.id,
|
||||
@@ -204,6 +227,7 @@ async function isReturningToSelf({ session, creds }: { session: Session | null;
|
||||
organizationId: returningUser.organizationId,
|
||||
role: returningUser.role,
|
||||
username: returningUser.username,
|
||||
profile,
|
||||
},
|
||||
impersonatedByUID,
|
||||
hasTeams,
|
||||
@@ -242,7 +266,6 @@ const ImpersonationProvider = CredentialsProvider({
|
||||
checkGlobalPermission(session);
|
||||
|
||||
const impersonatedUser = await getImpersonatedUser({ session, teamId, creds });
|
||||
|
||||
if (session?.user.role === "ADMIN") {
|
||||
if (impersonatedUser.disableImpersonation) {
|
||||
throw new Error("This user has disabled Impersonation.");
|
||||
@@ -302,3 +325,29 @@ const ImpersonationProvider = CredentialsProvider({
|
||||
});
|
||||
|
||||
export default ImpersonationProvider;
|
||||
async function findProfile(returningUser: { id: number; username: string | null }) {
|
||||
const allOrgProfiles = await ProfileRepository.findAllProfilesForUserIncludingMovedUser({
|
||||
id: returningUser.id,
|
||||
username: returningUser.username,
|
||||
});
|
||||
|
||||
const firstOrgProfile = allOrgProfiles[0];
|
||||
const orgMembers = firstOrgProfile.organizationId
|
||||
? await prisma.membership.findMany({
|
||||
where: {
|
||||
teamId: firstOrgProfile.organizationId,
|
||||
},
|
||||
})
|
||||
: [];
|
||||
|
||||
const profile = !firstOrgProfile.organization
|
||||
? firstOrgProfile
|
||||
: {
|
||||
...firstOrgProfile,
|
||||
organization: {
|
||||
...firstOrgProfile.organization,
|
||||
members: orgMembers,
|
||||
},
|
||||
};
|
||||
return profile;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ const generateHashedLink = (id: number) => {
|
||||
|
||||
interface handleChildrenEventTypesProps {
|
||||
eventTypeId: number;
|
||||
profileId: number | null;
|
||||
updatedEventType: {
|
||||
schedulingType: SchedulingType | null;
|
||||
slug: string;
|
||||
@@ -101,6 +102,7 @@ export default async function handleChildrenEventTypes({
|
||||
connectedLink,
|
||||
children,
|
||||
prisma,
|
||||
profileId,
|
||||
}: handleChildrenEventTypesProps) {
|
||||
// Check we are dealing with a managed event type
|
||||
if (updatedEventType?.schedulingType !== SchedulingType.MANAGED)
|
||||
@@ -177,6 +179,7 @@ export default async function handleChildrenEventTypes({
|
||||
newUserIds.map((userId) => {
|
||||
return prisma.eventType.create({
|
||||
data: {
|
||||
profileId: profileId ?? null,
|
||||
...managedEventTypeValues,
|
||||
...unlockedEventTypeValues,
|
||||
bookingLimits:
|
||||
@@ -233,6 +236,7 @@ export default async function handleChildrenEventTypes({
|
||||
},
|
||||
data: {
|
||||
...managedEventTypeValues,
|
||||
profileId: profileId ?? null,
|
||||
hidden: children?.find((ch) => ch.owner.id === userId)?.hidden ?? false,
|
||||
bookingLimits:
|
||||
(managedEventTypeValues.bookingLimits as unknown as Prisma.InputJsonObject) ?? undefined,
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function MemberListItem(props: Props) {
|
||||
<div className="my-4 flex justify-between">
|
||||
<div className="flex w-full flex-col justify-between overflow-hidden sm:flex-row">
|
||||
<div className="flex">
|
||||
<UserAvatar size="sm" user={user} className="h-10 w-10 rounded-full" />
|
||||
<UserAvatar noOrganizationIndicator size="sm" user={user} className="h-10 w-10 rounded-full" />
|
||||
|
||||
<div className="ms-3 inline-block overflow-hidden">
|
||||
<div className="mb-1 flex">
|
||||
|
||||
@@ -118,16 +118,7 @@ const MembersView = () => {
|
||||
router.push("/settings");
|
||||
}
|
||||
},
|
||||
[otherMembersError, otherTeamError]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
function refactorMeWithoutEffect() {
|
||||
if (data) {
|
||||
router.push("/settings");
|
||||
}
|
||||
},
|
||||
[data]
|
||||
[router, otherMembersError, otherTeamError]
|
||||
);
|
||||
|
||||
const isPending = isTeamLoading || isOrgListLoading;
|
||||
|
||||
@@ -138,12 +138,18 @@ export default function MemberListItem(props: Props) {
|
||||
)
|
||||
) : null;
|
||||
});
|
||||
|
||||
return (
|
||||
<li className="divide-subtle divide-y px-5">
|
||||
<div className="my-4 flex justify-between">
|
||||
<div className="flex w-full flex-col justify-between truncate sm:flex-row">
|
||||
<div className="flex">
|
||||
<UserAvatar size="sm" user={props.member} className="h-10 w-10 rounded-full" />
|
||||
<UserAvatar
|
||||
noOrganizationIndicator
|
||||
size="sm"
|
||||
user={props.member}
|
||||
className="h-10 w-10 rounded-full"
|
||||
/>
|
||||
<div className="ms-3 inline-block">
|
||||
<div className="mb-1 flex" data-testid={`member-${props.member.username}`}>
|
||||
<span data-testid="member-name" className="text-default mr-2 text-sm font-bold leading-4">
|
||||
|
||||
@@ -6,6 +6,7 @@ import InviteLinkSettingsModal from "@calcom/ee/teams/components/InviteLinkSetti
|
||||
import MemberInvitationModal from "@calcom/ee/teams/components/MemberInvitationModal";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { isOrganization } from "@calcom/lib/entityPermissionUtils";
|
||||
import { getTeamUrlSync } from "@calcom/lib/getBookerUrl/client";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -42,7 +43,6 @@ import {
|
||||
X,
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
import { useOrgBranding } from "../../organizations/context/provider";
|
||||
import { TeamRole } from "./TeamPill";
|
||||
|
||||
interface Props {
|
||||
@@ -86,7 +86,6 @@ export default function TeamListItem(props: Props) {
|
||||
|
||||
const acceptInvite = () => acceptOrLeave(true);
|
||||
const declineInvite = () => acceptOrLeave(false);
|
||||
const orgBranding = useOrgBranding();
|
||||
|
||||
const isOwner = props.team.role === MembershipRole.OWNER;
|
||||
const isInvitee = !props.team.accepted;
|
||||
@@ -94,7 +93,9 @@ export default function TeamListItem(props: Props) {
|
||||
const { hideDropdown, setHideDropdown } = props;
|
||||
|
||||
if (!team) return <></>;
|
||||
|
||||
const teamUrl = isOrganization({ team })
|
||||
? getTeamUrlSync({ orgSlug: team.slug, teamSlug: null })
|
||||
: getTeamUrlSync({ orgSlug: team.parent ? team.parent.slug : null, teamSlug: team.slug });
|
||||
const teamInfo = (
|
||||
<div className="item-center flex px-5 py-5">
|
||||
<Avatar
|
||||
@@ -106,11 +107,7 @@ export default function TeamListItem(props: Props) {
|
||||
<div className="ms-3 inline-block truncate">
|
||||
<span className="text-default text-sm font-bold">{team.name}</span>
|
||||
<span className="text-muted block text-xs">
|
||||
{team.slug ? (
|
||||
`${getTeamUrlSync({ orgSlug: team.parent ? team.parent.slug : null, teamSlug: team.slug })}`
|
||||
) : (
|
||||
<Badge>{t("upgrade")}</Badge>
|
||||
)}
|
||||
{team.slug ? `${teamUrl}` : <Badge>{t("upgrade")}</Badge>}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { isOrganization } from "@calcom/lib/entityPermissionUtils";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
@@ -21,9 +22,14 @@ export function TeamsListing() {
|
||||
|
||||
const [inviteTokenChecked, setInviteTokenChecked] = useState(false);
|
||||
|
||||
const { data, isPending, error } = trpc.viewer.teams.list.useQuery(undefined, {
|
||||
enabled: inviteTokenChecked,
|
||||
});
|
||||
const { data, isPending, error } = trpc.viewer.teams.list.useQuery(
|
||||
{
|
||||
includeOrgs: true,
|
||||
},
|
||||
{
|
||||
enabled: inviteTokenChecked,
|
||||
}
|
||||
);
|
||||
|
||||
const { data: user } = trpc.viewer.me.useQuery();
|
||||
|
||||
@@ -40,8 +46,22 @@ export function TeamsListing() {
|
||||
},
|
||||
});
|
||||
|
||||
const teams = useMemo(() => data?.filter((m) => m.accepted) || [], [data]);
|
||||
const invites = useMemo(() => data?.filter((m) => !m.accepted) || [], [data]);
|
||||
const teams = useMemo(() => data?.filter((m) => m.accepted && !isOrganization({ team: m })) || [], [data]);
|
||||
|
||||
const teamInvites = useMemo(
|
||||
() => data?.filter((m) => !m.accepted && !isOrganization({ team: m })) || [],
|
||||
[data]
|
||||
);
|
||||
|
||||
const organizationInvites = (data?.filter((m) => !m.accepted && isOrganization({ team: m })) || []).filter(
|
||||
(orgInvite) => {
|
||||
const isThereASubTeamOfTheOrganizationInInvites = teamInvites.find(
|
||||
(teamInvite) => teamInvite.parentId === orgInvite.id
|
||||
);
|
||||
// Accepting a subteam invite automatically accepts the invite for the parent organization. So, need to show such an organization's invite
|
||||
return !isThereASubTeamOfTheOrganizationInInvites;
|
||||
}
|
||||
);
|
||||
|
||||
const isCreateTeamButtonDisabled = !!(user?.organizationId && !user?.organization?.isOrgAdmin);
|
||||
|
||||
@@ -92,12 +112,20 @@ export function TeamsListing() {
|
||||
<>
|
||||
{!!error && <Alert severity="error" title={error.message} />}
|
||||
|
||||
{invites.length > 0 && (
|
||||
{organizationInvites.length > 0 && (
|
||||
<div className="bg-subtle mb-6 rounded-md p-5">
|
||||
<Label className="text-emphasis pb-2 font-semibold">{t("pending_invites")}</Label>
|
||||
<TeamList teams={invites} pending />
|
||||
<Label className="text-emphasis pb-2 font-semibold">{t("pending_organization_invites")}</Label>
|
||||
<TeamList teams={organizationInvites} pending />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{teamInvites.length > 0 && (
|
||||
<div className="bg-subtle mb-6 rounded-md p-5">
|
||||
<Label className="text-emphasis pb-2 font-semibold">{t("pending_invites")}</Label>
|
||||
<TeamList teams={teamInvites} pending />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<UpgradeTip
|
||||
plan="team"
|
||||
title={t("calcom_is_better_with_team", { appName: APP_NAME })}
|
||||
|
||||
@@ -58,9 +58,10 @@ const getUserAdminTeams = async ({
|
||||
},
|
||||
}),
|
||||
},
|
||||
orderBy: {
|
||||
orgUsers: { _count: "desc" },
|
||||
},
|
||||
// FIXME - OrgNewSchema: Fix this orderBy
|
||||
// orderBy: {
|
||||
// orgUsers: { _count: "desc" },
|
||||
// },
|
||||
});
|
||||
|
||||
if (teams.length && getUserInfo) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getStripeCustomerIdFromUserId } from "@calcom/app-store/stripepayment/l
|
||||
import stripe from "@calcom/app-store/stripepayment/lib/server";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { ORGANIZATION_MIN_SEATS } from "@calcom/lib/constants";
|
||||
import { isOrganization } from "@calcom/lib/entityPermissionUtils";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
@@ -120,8 +121,9 @@ export const purchaseTeamSubscription = async (input: {
|
||||
const getTeamWithPaymentMetadata = async (teamId: number) => {
|
||||
const team = await prisma.team.findUniqueOrThrow({
|
||||
where: { id: teamId },
|
||||
select: { metadata: true, members: true, _count: { select: { orgUsers: true } } },
|
||||
select: { metadata: true, members: true },
|
||||
});
|
||||
|
||||
const metadata = teamPaymentMetadataSchema.parse(team.metadata);
|
||||
return { ...team, metadata };
|
||||
};
|
||||
@@ -155,7 +157,7 @@ export const updateQuantitySubscriptionFromStripe = async (teamId: number) => {
|
||||
)?.quantity;
|
||||
if (!subscriptionQuantity) throw new Error("Subscription not found");
|
||||
|
||||
if (!!team._count.orgUsers && membershipCount < ORGANIZATION_MIN_SEATS) {
|
||||
if (!!isOrganization({ team }) && membershipCount < ORGANIZATION_MIN_SEATS) {
|
||||
console.info(
|
||||
`Org ${teamId} has less members than the min ${ORGANIZATION_MIN_SEATS}, skipping updating subscription.`
|
||||
);
|
||||
|
||||
@@ -176,6 +176,7 @@ function UsersTableBare() {
|
||||
<Avatar
|
||||
size="md"
|
||||
alt={`Avatar of ${user.username || "Nameless"}`}
|
||||
// @ts-expect-error - Figure it out later. Ideally we should show all the profiles here for the user.
|
||||
imageSrc={`${WEBAPP_URL}/${user.username}/avatar.png?orgId=${user.organizationId}`}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { User } from "@prisma/client";
|
||||
import type { User as UserType } from "@prisma/client";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import type { LocationObject } from "@calcom/app-store/locations";
|
||||
@@ -10,6 +10,7 @@ import { isRecurringEvent, parseRecurringEvent } from "@calcom/lib";
|
||||
import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { BookerLayoutSettings } from "@calcom/prisma/zod-utils";
|
||||
import {
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
BookerLayouts,
|
||||
teamMetadataSchema,
|
||||
} from "@calcom/prisma/zod-utils";
|
||||
import type { UserProfile } from "@calcom/types/UserProfile";
|
||||
|
||||
const publicEventSelect = Prisma.validator<Prisma.EventTypeSelect>()({
|
||||
id: true,
|
||||
@@ -76,18 +78,14 @@ const publicEventSelect = Prisma.validator<Prisma.EventTypeSelect>()({
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
avatarUrl: true,
|
||||
username: true,
|
||||
name: true,
|
||||
weekStart: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
theme: true,
|
||||
organizationId: true,
|
||||
organization: {
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
metadata: true,
|
||||
},
|
||||
},
|
||||
@@ -95,6 +93,8 @@ const publicEventSelect = Prisma.validator<Prisma.EventTypeSelect>()({
|
||||
},
|
||||
owner: {
|
||||
select: {
|
||||
id: true,
|
||||
avatarUrl: true,
|
||||
weekStart: true,
|
||||
username: true,
|
||||
name: true,
|
||||
@@ -102,13 +102,6 @@ const publicEventSelect = Prisma.validator<Prisma.EventTypeSelect>()({
|
||||
metadata: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
organizationId: true,
|
||||
organization: {
|
||||
select: {
|
||||
name: true,
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
hidden: true,
|
||||
@@ -126,30 +119,12 @@ export const getPublicEvent = async (
|
||||
const orgQuery = org ? getSlugOrRequestedSlug(org) : null;
|
||||
// In case of dynamic group event, we fetch user's data and use the default event.
|
||||
if (usernameList.length > 1) {
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
username: {
|
||||
in: usernameList,
|
||||
},
|
||||
organization: orgQuery,
|
||||
},
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
weekStart: true,
|
||||
metadata: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
theme: true,
|
||||
organizationId: true,
|
||||
organization: {
|
||||
select: {
|
||||
slug: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
const usersInOrgContext = await UserRepository.findUsersByUsername({
|
||||
usernameList,
|
||||
orgSlug: org,
|
||||
});
|
||||
console.log("getPublicEvent - dynamic", usersInOrgContext);
|
||||
const users = usersInOrgContext;
|
||||
|
||||
const defaultEvent = getDefaultEvent(eventSlug);
|
||||
let locations = defaultEvent.locations ? (defaultEvent.locations as LocationObject[]) : [];
|
||||
@@ -173,7 +148,7 @@ export const getPublicEvent = async (
|
||||
defaultLayout: BookerLayouts.MONTH_VIEW,
|
||||
} as BookerLayoutSettings;
|
||||
const disableBookingTitle = !defaultEvent.isDynamic;
|
||||
const unPublishedOrgUser = users.find((user) => user.organization?.slug === null);
|
||||
const unPublishedOrgUser = users.find((user) => user.profile?.organization?.slug === null);
|
||||
|
||||
return {
|
||||
...defaultEvent,
|
||||
@@ -182,7 +157,7 @@ export const getPublicEvent = async (
|
||||
users: users.map((user) => ({
|
||||
...user,
|
||||
metadata: undefined,
|
||||
bookerUrl: getBookerBaseUrlSync(user.organization?.slug ?? null),
|
||||
bookerUrl: getBookerBaseUrlSync(user.profile?.organization?.slug ?? null),
|
||||
})),
|
||||
locations: privacyFilteredLocations(locations),
|
||||
profile: {
|
||||
@@ -200,7 +175,7 @@ export const getPublicEvent = async (
|
||||
entity: {
|
||||
isUnpublished: unPublishedOrgUser !== undefined,
|
||||
orgSlug: org,
|
||||
name: unPublishedOrgUser?.organization?.name ?? null,
|
||||
name: unPublishedOrgUser?.profile?.organization?.name ?? null,
|
||||
},
|
||||
isInstantEvent: false,
|
||||
};
|
||||
@@ -216,8 +191,17 @@ export const getPublicEvent = async (
|
||||
: {
|
||||
users: {
|
||||
some: {
|
||||
username,
|
||||
organization: orgQuery,
|
||||
...(orgQuery
|
||||
? {
|
||||
profiles: {
|
||||
some: {
|
||||
organization: orgQuery,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
username,
|
||||
}),
|
||||
},
|
||||
},
|
||||
team: null,
|
||||
@@ -236,35 +220,61 @@ export const getPublicEvent = async (
|
||||
|
||||
const eventMetaData = EventTypeMetaDataSchema.parse(event.metadata || {});
|
||||
const teamMetadata = teamMetadataSchema.parse(event.team?.metadata || {});
|
||||
const hosts = [];
|
||||
for (const host of event.hosts) {
|
||||
hosts.push({
|
||||
...host,
|
||||
user: await UserRepository.enrichUserWithItsProfile({
|
||||
user: host.user,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const eventWithUserProfiles = {
|
||||
...event,
|
||||
owner: event.owner
|
||||
? await UserRepository.enrichUserWithItsProfile({
|
||||
user: event.owner,
|
||||
})
|
||||
: null,
|
||||
hosts: hosts,
|
||||
};
|
||||
|
||||
const users = getUsersFromEvent(eventWithUserProfiles) || (await getOwnerFromUsersArray(prisma, event.id));
|
||||
|
||||
const users = getUsersFromEvent(event) || (await getOwnerFromUsersArray(prisma, event.id));
|
||||
if (users === null) {
|
||||
throw new Error("Event has no owner");
|
||||
}
|
||||
|
||||
return {
|
||||
...event,
|
||||
...eventWithUserProfiles,
|
||||
bookerLayouts: bookerLayoutsSchema.parse(eventMetaData?.bookerLayouts || null),
|
||||
description: markdownToSafeHTML(event.description),
|
||||
description: markdownToSafeHTML(eventWithUserProfiles.description),
|
||||
metadata: eventMetaData,
|
||||
customInputs: customInputSchema.array().parse(event.customInputs || []),
|
||||
locations: privacyFilteredLocations((event.locations || []) as LocationObject[]),
|
||||
locations: privacyFilteredLocations((eventWithUserProfiles.locations || []) as LocationObject[]),
|
||||
bookingFields: getBookingFieldsWithSystemFields(event),
|
||||
recurringEvent: isRecurringEvent(event.recurringEvent) ? parseRecurringEvent(event.recurringEvent) : null,
|
||||
recurringEvent: isRecurringEvent(eventWithUserProfiles.recurringEvent)
|
||||
? parseRecurringEvent(event.recurringEvent)
|
||||
: null,
|
||||
// Sets user data on profile object for easier access
|
||||
profile: getProfileFromEvent(event),
|
||||
profile: getProfileFromEvent(eventWithUserProfiles),
|
||||
users,
|
||||
entity: {
|
||||
isUnpublished:
|
||||
event.team?.slug === null ||
|
||||
event.owner?.organization?.slug === null ||
|
||||
event.team?.parent?.slug === null,
|
||||
eventWithUserProfiles.team?.slug === null ||
|
||||
eventWithUserProfiles.owner?.profile?.organization?.slug === null ||
|
||||
eventWithUserProfiles.team?.parent?.slug === null,
|
||||
orgSlug: org,
|
||||
teamSlug: (event.team?.slug || teamMetadata?.requestedSlug) ?? null,
|
||||
name: (event.owner?.organization?.name || event.team?.parent?.name || event.team?.name) ?? null,
|
||||
teamSlug: (eventWithUserProfiles.team?.slug || teamMetadata?.requestedSlug) ?? null,
|
||||
name:
|
||||
(eventWithUserProfiles.owner?.profile?.organization?.name ||
|
||||
eventWithUserProfiles.team?.parent?.name ||
|
||||
eventWithUserProfiles.team?.name) ??
|
||||
null,
|
||||
},
|
||||
isDynamic: false,
|
||||
isInstantEvent: event.isInstantEvent,
|
||||
isInstantEvent: eventWithUserProfiles.isInstantEvent,
|
||||
assignAllTeamMembers: event.assignAllTeamMembers,
|
||||
};
|
||||
};
|
||||
@@ -301,8 +311,20 @@ function getProfileFromEvent(event: Event) {
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function getUsersFromEvent(event: Event) {
|
||||
function getUsersFromEvent(
|
||||
event: Omit<Event, "owner" | "hosts"> & {
|
||||
owner:
|
||||
| (Event["owner"] & {
|
||||
profile: UserProfile;
|
||||
})
|
||||
| null;
|
||||
hosts: (Omit<Event["hosts"][number], "user"> & {
|
||||
user: Event["hosts"][number]["user"] & {
|
||||
profile: UserProfile;
|
||||
};
|
||||
})[];
|
||||
}
|
||||
) {
|
||||
const { team, hosts, owner } = event;
|
||||
if (team) {
|
||||
return (hosts || []).filter((host) => host.user.username).map(mapHostsToUsers);
|
||||
@@ -310,14 +332,17 @@ function getUsersFromEvent(event: Event) {
|
||||
if (!owner) {
|
||||
return null;
|
||||
}
|
||||
const { username, name, weekStart, organizationId } = owner;
|
||||
const { username, name, weekStart, profile, avatarUrl } = owner;
|
||||
const organizationId = profile?.organization?.id ?? null;
|
||||
return [
|
||||
{
|
||||
username,
|
||||
name,
|
||||
weekStart,
|
||||
organizationId,
|
||||
bookerUrl: getBookerBaseUrlSync(owner.organization?.slug ?? null),
|
||||
avatarUrl,
|
||||
profile,
|
||||
bookerUrl: getBookerBaseUrlSync(owner.profile?.organization?.slug ?? null),
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -328,33 +353,48 @@ async function getOwnerFromUsersArray(prisma: PrismaClient, eventTypeId: number)
|
||||
select: {
|
||||
users: {
|
||||
select: {
|
||||
avatarUrl: true,
|
||||
username: true,
|
||||
name: true,
|
||||
weekStart: true,
|
||||
organizationId: true,
|
||||
organization: {
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!users.length) return null;
|
||||
return [{ ...users[0], bookerUrl: getBookerBaseUrlSync(users[0].organization?.slug ?? null) }];
|
||||
const usersWithUserProfile = [];
|
||||
for (const user of users) {
|
||||
const { profile } = await UserRepository.enrichUserWithItsProfile({
|
||||
user: user,
|
||||
});
|
||||
usersWithUserProfile.push({
|
||||
...user,
|
||||
organizationId: profile?.organization?.id ?? null,
|
||||
organization: profile?.organization,
|
||||
profile,
|
||||
});
|
||||
}
|
||||
return [
|
||||
{
|
||||
...usersWithUserProfile[0],
|
||||
bookerUrl: getBookerBaseUrlSync(usersWithUserProfile[0].organization?.slug ?? null),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function mapHostsToUsers(host: {
|
||||
user: Pick<User, "username" | "name" | "weekStart" | "organizationId"> & {
|
||||
organization: { slug: string | null } | null;
|
||||
user: Pick<UserType, "username" | "name" | "weekStart" | "avatarUrl"> & {
|
||||
profile: UserProfile;
|
||||
};
|
||||
}) {
|
||||
return {
|
||||
username: host.user.username,
|
||||
name: host.user.name,
|
||||
avatarUrl: host.user.avatarUrl,
|
||||
weekStart: host.user.weekStart,
|
||||
organizationId: host.user.organizationId,
|
||||
bookerUrl: getBookerBaseUrlSync(host.user.organization?.slug ?? null),
|
||||
organizationId: host.user.profile?.organizationId ?? null,
|
||||
bookerUrl: getBookerBaseUrlSync(host.user.profile?.organization?.slug ?? null),
|
||||
profile: host.user.profile,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,10 +24,12 @@ function useFilterQuery() {
|
||||
|
||||
export const TeamsFilter = ({
|
||||
popoverTriggerClassNames,
|
||||
useProfileFilter = false,
|
||||
showVerticalDivider = false,
|
||||
}: {
|
||||
popoverTriggerClassNames?: string;
|
||||
showVerticalDivider?: boolean;
|
||||
useProfileFilter?: boolean;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const session = useSession();
|
||||
@@ -39,6 +41,7 @@ export const TeamsFilter = ({
|
||||
const getCheckedOptionsNames = () => {
|
||||
const checkedOptions: string[] = [];
|
||||
const teamIds = query.teamIds;
|
||||
const users = useProfileFilter ? query.upIds : query.userIds;
|
||||
if (teamIds) {
|
||||
const selectedTeamsNames = teams
|
||||
?.filter((team) => {
|
||||
@@ -50,7 +53,7 @@ export const TeamsFilter = ({
|
||||
}
|
||||
return `${t("team")}: ${checkedOptions.join(",")}`;
|
||||
}
|
||||
if (query.userIds) {
|
||||
if (users) {
|
||||
return t("yours");
|
||||
}
|
||||
return t("all");
|
||||
@@ -58,6 +61,9 @@ export const TeamsFilter = ({
|
||||
|
||||
if (!teams || !teams.length) return null;
|
||||
|
||||
const userId = session.data?.user?.id || 0;
|
||||
const upId = session.data?.upId || "";
|
||||
const isUserInQuery = useProfileFilter ? query.upIds?.includes(upId) : query.userIds?.includes(userId);
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<AnimatedPopover
|
||||
@@ -68,7 +74,7 @@ export const TeamsFilter = ({
|
||||
<FilterCheckboxField
|
||||
id="all"
|
||||
icon={<Layers className="h-4 w-4" />}
|
||||
checked={!query.teamIds && !query.userIds?.includes(session.data?.user.id || 0)}
|
||||
checked={!query.teamIds && !isUserInQuery}
|
||||
onChange={removeAllQueryParams}
|
||||
label={t("all")}
|
||||
/>
|
||||
@@ -76,12 +82,14 @@ export const TeamsFilter = ({
|
||||
<FilterCheckboxField
|
||||
id="yours"
|
||||
icon={<User className="h-4 w-4" />}
|
||||
checked={!!query.userIds?.includes(session.data?.user.id || 0)}
|
||||
checked={!!isUserInQuery}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
pushItemToKey("userIds", session.data?.user.id || 0);
|
||||
if (useProfileFilter) pushItemToKey("upIds", upId);
|
||||
else pushItemToKey("userIds", userId);
|
||||
} else if (!e.target.checked) {
|
||||
removeItemByKeyAndValue("userIds", session.data?.user.id || 0);
|
||||
if (useProfileFilter) removeItemByKeyAndValue("upIds", upId);
|
||||
else removeItemByKeyAndValue("userIds", userId);
|
||||
}
|
||||
}}
|
||||
label={t("yours")}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user