fix: Broken routing form public links(old links) on migrating the owner user to an organization (#13416)
* Fix routing-form access restrictions * Update packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> * Fix routing form access issue for users who are never migrated * Improve comment * wip * Fix event-type listing not working * Automatic reload of the app when being added to a different organization --------- Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
co-authored by
Carina Wollendorfer
Peer Richelsen
parent
63f537a08f
commit
4fe75771b8
@@ -0,0 +1,57 @@
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
|
||||
|
||||
export const enrichFormWithMigrationData = <
|
||||
T extends {
|
||||
user: {
|
||||
movedToProfileId: number | null;
|
||||
metadata?: unknown;
|
||||
username: string | null;
|
||||
nonProfileUsername: string | null;
|
||||
profile: {
|
||||
organization: {
|
||||
slug: string | null;
|
||||
} | null;
|
||||
};
|
||||
};
|
||||
team: {
|
||||
parent: {
|
||||
slug: string | null;
|
||||
} | null;
|
||||
metadata?: unknown;
|
||||
} | null;
|
||||
}
|
||||
>(
|
||||
form: T
|
||||
) => {
|
||||
const parsedUserMetadata = userMetadata.parse(form.user.metadata ?? null);
|
||||
const parsedTeamMetadata = teamMetadataSchema.parse(form.team?.metadata ?? null);
|
||||
const formOwnerOrgSlug = form.user.profile.organization?.slug ?? null;
|
||||
const nonOrgUsername = parsedUserMetadata?.migratedToOrgFrom?.username ?? form.user.nonProfileUsername;
|
||||
const nonOrgTeamslug = parsedTeamMetadata?.migratedToOrgFrom?.teamSlug ?? null;
|
||||
|
||||
return {
|
||||
...form,
|
||||
user: {
|
||||
...form.user,
|
||||
metadata: parsedUserMetadata,
|
||||
},
|
||||
team: {
|
||||
...form.team,
|
||||
metadata: teamMetadataSchema.parse(form.team?.metadata ?? null),
|
||||
},
|
||||
userOrigin: formOwnerOrgSlug
|
||||
? getOrgFullOrigin(formOwnerOrgSlug, {
|
||||
protocol: true,
|
||||
})
|
||||
: CAL_URL,
|
||||
teamOrigin: form.team?.parent?.slug
|
||||
? getOrgFullOrigin(form.team.parent.slug, {
|
||||
protocol: true,
|
||||
})
|
||||
: CAL_URL,
|
||||
nonOrgUsername,
|
||||
nonOrgTeamslug,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
|
||||
function getUserAndEventTypeSlug(eventTypeRedirectUrl: string) {
|
||||
if (eventTypeRedirectUrl.startsWith("/")) {
|
||||
eventTypeRedirectUrl = eventTypeRedirectUrl.slice(1);
|
||||
}
|
||||
const parts = eventTypeRedirectUrl.split("/");
|
||||
const isTeamSlug = parts[0] == "team" ? true : false;
|
||||
const teamSlug = isTeamSlug ? parts[1] : null;
|
||||
const eventTypeSlug = teamSlug ? parts[2] : parts[1];
|
||||
const username = isTeamSlug ? null : parts[0];
|
||||
return { teamSlug, eventTypeSlug, username };
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the following cases
|
||||
* 1. A team form where the team isn't a sub-team
|
||||
* 1.1 A team form where team isn't a sub-team and the user is migrated. i.e. User has been migrated but not the team
|
||||
* 1.2 A team form where team isn't a sub-team and the user is not migrated i.e. Both user and team are not migrated
|
||||
* 2. A team form where the team is a sub-team
|
||||
* 1.1 A team form where the team is a sub-team and the user is migrated i.e. Both user and team are migrated
|
||||
* 1.2 A team form where the team is a sub-team and the user is not migrated i.e. Team has been migrated but not the user
|
||||
* 3. A user form where the user is migrated
|
||||
* 3.1 A user form where the user is migrated and the team is migrated i.e. Both user and team are migrated
|
||||
* 3.2 A user form where the user is migrated and the team is not migrated i.e. User has been migrated but not the team
|
||||
* 4. A user form where the user is not migrated
|
||||
* 4.1 A user form where the user is not migrated and the team is migrated i.e. Team has been migrated but not the user
|
||||
* 4.2 A user form where the user is not migrated and the team is not migrated i.e. Both user and team are not migrated
|
||||
*/
|
||||
export function getAbsoluteEventTypeRedirectUrl({
|
||||
eventTypeRedirectUrl,
|
||||
form,
|
||||
allURLSearchParams,
|
||||
}: {
|
||||
eventTypeRedirectUrl: string;
|
||||
form: {
|
||||
team: {
|
||||
// parentId is set if the team is a sub-team
|
||||
parentId: number | null;
|
||||
} | null;
|
||||
/**
|
||||
* Set only if user is migrated
|
||||
*/
|
||||
nonOrgUsername: string | null;
|
||||
/**
|
||||
* Set only if team is migrated
|
||||
*/
|
||||
nonOrgTeamslug: string | null;
|
||||
/**
|
||||
* The origin for the user
|
||||
*/
|
||||
userOrigin: string;
|
||||
/**
|
||||
* The origin for the team the form belongs to
|
||||
*/
|
||||
teamOrigin: string;
|
||||
};
|
||||
allURLSearchParams: URLSearchParams;
|
||||
}) {
|
||||
// It could be using the old(before migration) username/team-slug or it could be using the new one(after migration)
|
||||
// If it's using the old one, it would work by redirection as long as we use CAL_URL(which is non-org domain)
|
||||
// But if it's using the new one, it has to use the org domain.
|
||||
// The format is /user/abc or /team/team1/abc
|
||||
const { username: usernameInRedirectUrl, teamSlug: teamSlugInRedirectUrl } =
|
||||
getUserAndEventTypeSlug(eventTypeRedirectUrl);
|
||||
|
||||
if (!teamSlugInRedirectUrl && !usernameInRedirectUrl) {
|
||||
throw new Error("eventTypeRedirectUrl must have username or teamSlug");
|
||||
}
|
||||
|
||||
if (teamSlugInRedirectUrl && form.nonOrgTeamslug) {
|
||||
const isEventTypeRedirectToOldTeamSlug = teamSlugInRedirectUrl === form.nonOrgTeamslug;
|
||||
if (isEventTypeRedirectToOldTeamSlug) {
|
||||
return `${CAL_URL}/${eventTypeRedirectUrl}?${allURLSearchParams}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (usernameInRedirectUrl && form.nonOrgUsername) {
|
||||
const isEventTypeRedirectToOldUser = usernameInRedirectUrl === form.nonOrgUsername;
|
||||
if (isEventTypeRedirectToOldUser) {
|
||||
return `${CAL_URL}/${eventTypeRedirectUrl}?${allURLSearchParams}`;
|
||||
}
|
||||
}
|
||||
|
||||
const origin = teamSlugInRedirectUrl ? form.teamOrigin : form.userOrigin;
|
||||
|
||||
return `${origin}/${eventTypeRedirectUrl}?${allURLSearchParams}`;
|
||||
}
|
||||
@@ -6,11 +6,15 @@ import logger from "@calcom/lib/logger";
|
||||
import { TRPCError } from "@calcom/trpc/server";
|
||||
import type { AppGetServerSidePropsContext, AppPrisma } from "@calcom/types/AppGetServerSideProps";
|
||||
|
||||
import { enrichFormWithMigrationData } from "../../enrichFormWithMigrationData";
|
||||
import { getAbsoluteEventTypeRedirectUrl } from "../../getEventTypeRedirectUrl";
|
||||
import getFieldIdentifier from "../../lib/getFieldIdentifier";
|
||||
import { getSerializableForm } from "../../lib/getSerializableForm";
|
||||
import { processRoute } from "../../lib/processRoute";
|
||||
import { substituteVariables } from "../../lib/substituteVariables";
|
||||
import transformResponse from "../../lib/transformResponse";
|
||||
import type { Response } from "../../types/types";
|
||||
import { isAuthorizedToViewTheForm } from "../routing-link/getServerSideProps";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["[routing-forms]", "[router]"] });
|
||||
|
||||
@@ -33,7 +37,6 @@ 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 } = orgDomainConfig(context.req);
|
||||
@@ -41,8 +44,32 @@ export const getServerSideProps = async function getServerSideProps(
|
||||
const form = await prisma.app_RoutingForms_Form.findFirst({
|
||||
where: {
|
||||
id: formId,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
...ProfileRepository._getPrismaWhereForProfilesOfOrg({ orgSlug: currentOrgDomain }),
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
movedToProfileId: true,
|
||||
metadata: true,
|
||||
organization: {
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
parentId: true,
|
||||
parent: {
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
slug: true,
|
||||
metadata: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -52,10 +79,28 @@ export const getServerSideProps = async function getServerSideProps(
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
const serializableForm = await getSerializableForm({ form });
|
||||
|
||||
const { UserRepository } = await import("@calcom/lib/server/repository/user");
|
||||
const formWithUserProfile = {
|
||||
...form,
|
||||
user: await UserRepository.enrichUserWithItsProfile({ user: form.user }),
|
||||
};
|
||||
|
||||
if (!(await isAuthorizedToViewTheForm({ user: formWithUserProfile.user, currentOrgDomain }))) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
const serializableForm = await getSerializableForm({
|
||||
form: enrichFormWithMigrationData(formWithUserProfile),
|
||||
});
|
||||
|
||||
const response: Response = {};
|
||||
serializableForm.fields?.forEach((field) => {
|
||||
if (!serializableForm.fields) {
|
||||
throw new Error("Form has no fields");
|
||||
}
|
||||
serializableForm.fields.forEach((field) => {
|
||||
const fieldResponse = fieldsResponses[getFieldIdentifier(field)] || "";
|
||||
|
||||
response[field.id] = {
|
||||
@@ -102,9 +147,18 @@ export const getServerSideProps = async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
} else if (decidedAction.type === "eventTypeRedirectUrl") {
|
||||
const eventTypeUrlWithResolvedVariables = substituteVariables(
|
||||
decidedAction.value,
|
||||
response,
|
||||
serializableForm.fields
|
||||
);
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/${decidedAction.value}?${stringify(context.query)}`,
|
||||
destination: getAbsoluteEventTypeRedirectUrl({
|
||||
eventTypeRedirectUrl: eventTypeUrlWithResolvedVariables,
|
||||
form: serializableForm,
|
||||
allURLSearchParams: new URLSearchParams(stringify(context.query)),
|
||||
}),
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import { Button, showToast, useCalcomTheme } from "@calcom/ui";
|
||||
|
||||
import FormInputFields from "../../components/FormInputFields";
|
||||
import { getAbsoluteEventTypeRedirectUrl } from "../../getEventTypeRedirectUrl";
|
||||
import getFieldIdentifier from "../../lib/getFieldIdentifier";
|
||||
import { processRoute } from "../../lib/processRoute";
|
||||
import { substituteVariables } from "../../lib/substituteVariables";
|
||||
@@ -102,9 +103,14 @@ function RoutingForm({ form, profile, ...restProps }: Props) {
|
||||
if (decidedAction.type === "customPageMessage") {
|
||||
setCustomPageMessage(decidedAction.value);
|
||||
} else if (decidedAction.type === "eventTypeRedirectUrl") {
|
||||
const eventTypeUrlWithVariables = substituteVariables(decidedAction.value, response, fields);
|
||||
|
||||
await router.push(`/${eventTypeUrlWithVariables}?${allURLSearchParams}`);
|
||||
const eventTypeUrlWithResolvedVariables = substituteVariables(decidedAction.value, response, fields);
|
||||
router.push(
|
||||
getAbsoluteEventTypeRedirectUrl({
|
||||
form,
|
||||
eventTypeRedirectUrl: eventTypeUrlWithResolvedVariables,
|
||||
allURLSearchParams,
|
||||
})
|
||||
);
|
||||
} else if (decidedAction.type === "externalRedirectUrl") {
|
||||
window.parent.location.href = `${decidedAction.value}?${allURLSearchParams}`;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,43 @@
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||
import type { AppGetServerSidePropsContext, AppPrisma } from "@calcom/types/AppGetServerSideProps";
|
||||
|
||||
import { enrichFormWithMigrationData } from "../../enrichFormWithMigrationData";
|
||||
import { getSerializableForm } from "../../lib/getSerializableForm";
|
||||
|
||||
export async function isAuthorizedToViewTheForm({
|
||||
user,
|
||||
currentOrgDomain,
|
||||
}: {
|
||||
user: {
|
||||
username: string | null;
|
||||
metadata: Prisma.JsonValue;
|
||||
movedToProfileId: number | null;
|
||||
profile: {
|
||||
organization: { slug: string | null } | null;
|
||||
};
|
||||
id: number;
|
||||
};
|
||||
currentOrgDomain: string | null;
|
||||
}) {
|
||||
const formUser = {
|
||||
...user,
|
||||
metadata: userMetadata.parse(user.metadata),
|
||||
};
|
||||
|
||||
if (!currentOrgDomain) {
|
||||
// If not on org domain, let's allow serving any form belong to any organization so that even if the form owner is migrate to an organization, old links for the form keep working
|
||||
return true;
|
||||
} else if (currentOrgDomain !== formUser.profile.organization?.slug) {
|
||||
// If on org domain,
|
||||
// We don't serve the form that is of another org
|
||||
// We don't serve the form that doesn't belong to any org
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export const getServerSideProps = async function getServerSideProps(
|
||||
context: AppGetServerSidePropsContext,
|
||||
prisma: AppPrisma
|
||||
@@ -22,22 +57,36 @@ export const getServerSideProps = async function getServerSideProps(
|
||||
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: {
|
||||
...ProfileRepository._getPrismaWhereForProfilesOfOrg({ orgSlug: currentOrgDomain }),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
movedToProfileId: true,
|
||||
organization: {
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
username: true,
|
||||
theme: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
metadata: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
slug: true,
|
||||
parent: {
|
||||
select: { slug: true },
|
||||
},
|
||||
parentId: true,
|
||||
metadata: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -49,6 +98,17 @@ export const getServerSideProps = async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
|
||||
const { UserRepository } = await import("@calcom/lib/server/repository/user");
|
||||
const formWithUserProfile = {
|
||||
...form,
|
||||
user: await UserRepository.enrichUserWithItsProfile({ user: form.user }),
|
||||
};
|
||||
|
||||
if (!(await isAuthorizedToViewTheForm({ user: formWithUserProfile.user, currentOrgDomain }))) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
isEmbed,
|
||||
@@ -58,7 +118,7 @@ export const getServerSideProps = async function getServerSideProps(
|
||||
brandColor: form.user.brandColor,
|
||||
darkBrandColor: form.user.darkBrandColor,
|
||||
},
|
||||
form: await getSerializableForm({ form }),
|
||||
form: await getSerializableForm({ form: enrichFormWithMigrationData(formWithUserProfile) }),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -58,6 +58,7 @@ export default function TeamListItem(props: Props) {
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { t, i18n } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const user = trpc.viewer.me.useQuery().data;
|
||||
const team = props.team;
|
||||
|
||||
const showDialog = searchParams?.get("inviteModal") === "true";
|
||||
@@ -68,12 +69,19 @@ export default function TeamListItem(props: Props) {
|
||||
const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation();
|
||||
|
||||
const acceptOrLeaveMutation = trpc.viewer.teams.acceptOrLeave.useMutation({
|
||||
onSuccess: () => {
|
||||
onSuccess: (_data, variables) => {
|
||||
showToast(t("success"), "success");
|
||||
utils.viewer.teams.get.invalidate();
|
||||
utils.viewer.teams.list.invalidate();
|
||||
utils.viewer.teams.hasTeamPlan.invalidate();
|
||||
utils.viewer.teams.listInvites.invalidate();
|
||||
const userOrganizationId = user?.profile?.organization?.id;
|
||||
const isSubTeamOfDifferentOrg = team.parentId ? team.parentId != userOrganizationId : false;
|
||||
const isDifferentOrg = isOrganization({ team }) && team.id !== userOrganizationId;
|
||||
// If the user team being accepted is a sub-team of different organization or the different organization itself then page must be reloaded to let the session change reflect reliably everywhere.
|
||||
if (variables.accept && (isSubTeamOfDifferentOrg || isDifferentOrg)) {
|
||||
window.location.reload();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -87,48 +87,79 @@ export class EventTypeRepository {
|
||||
) {
|
||||
if (!upId) return [];
|
||||
const lookupTarget = ProfileRepository.getLookupTarget(upId);
|
||||
const eventTypeWhere = {
|
||||
...(lookupTarget.type === LookupTarget.User
|
||||
? {
|
||||
userId: lookupTarget.id,
|
||||
}
|
||||
: {
|
||||
profileId: lookupTarget.id,
|
||||
}),
|
||||
...where,
|
||||
const profileId = lookupTarget.type === LookupTarget.User ? null : lookupTarget.id;
|
||||
const include = {
|
||||
// TODO: As required by getByViewHandler - Make it configurable
|
||||
team: {
|
||||
include: {
|
||||
eventTypes: true,
|
||||
},
|
||||
},
|
||||
hashedLink: true,
|
||||
users: true,
|
||||
children: {
|
||||
include: {
|
||||
users: true,
|
||||
},
|
||||
},
|
||||
hosts: {
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
log.debug(
|
||||
"findAllByUpId",
|
||||
safeStringify({
|
||||
upId,
|
||||
orderBy,
|
||||
argumentWhere: where,
|
||||
where: eventTypeWhere,
|
||||
})
|
||||
);
|
||||
return await prisma.eventType.findMany({
|
||||
where: eventTypeWhere,
|
||||
include: {
|
||||
// TODO: As required by getByViewHandler - Make it configurable
|
||||
team: {
|
||||
include: {
|
||||
eventTypes: true,
|
||||
},
|
||||
|
||||
if (!profileId) {
|
||||
// Lookup is by userId
|
||||
return await prisma.eventType.findMany({
|
||||
where: {
|
||||
userId: lookupTarget.id,
|
||||
...where,
|
||||
},
|
||||
hashedLink: true,
|
||||
users: true,
|
||||
children: {
|
||||
include: {
|
||||
users: true,
|
||||
},
|
||||
include,
|
||||
orderBy,
|
||||
});
|
||||
}
|
||||
|
||||
const profile = await ProfileRepository.findById(profileId);
|
||||
if (profile?.movedFromUser) {
|
||||
// Because the user has been moved to this profile, we need to get all user events except those that belong to some other profile
|
||||
// This is because those event-types that are created after moving to profile would have profileId but existing event-types would have profileId set to null
|
||||
return await prisma.eventType.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
// Existing events
|
||||
{
|
||||
userId: profile.movedFromUser.id,
|
||||
profileId: null,
|
||||
},
|
||||
// New events
|
||||
{
|
||||
profileId,
|
||||
},
|
||||
],
|
||||
...where,
|
||||
},
|
||||
hosts: {
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
include,
|
||||
orderBy,
|
||||
});
|
||||
} else {
|
||||
return await prisma.eventType.findMany({
|
||||
where: {
|
||||
profileId,
|
||||
},
|
||||
},
|
||||
orderBy,
|
||||
});
|
||||
include,
|
||||
orderBy,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@ export class ProfileRepository {
|
||||
|
||||
static async findByUpId(upId: string) {
|
||||
const lookupTarget = ProfileRepository.getLookupTarget(upId);
|
||||
log.debug("findById", safeStringify({ upId, lookupTarget }));
|
||||
log.debug("findByUpId", safeStringify({ upId, lookupTarget }));
|
||||
if (lookupTarget.type === LookupTarget.User) {
|
||||
const user = await UserRepository.findById({ id: lookupTarget.id });
|
||||
if (!user) {
|
||||
@@ -314,6 +314,7 @@ export class ProfileRepository {
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
movedFromUser: true,
|
||||
organization: {
|
||||
include: {
|
||||
members: true,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { whereClauseForOrgWithSlugOrRequestedSlug } from "@calcom/ee/organizations/lib/orgDomains";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { User as UserType, Prisma } from "@calcom/prisma/client";
|
||||
import type { UpId, UserProfile } from "@calcom/types/UserProfile";
|
||||
|
||||
import { isOrganization } from "../../entityPermissionUtils";
|
||||
@@ -7,7 +8,6 @@ import logger from "../../logger";
|
||||
import { safeStringify } from "../../safeStringify";
|
||||
import { ProfileRepository } from "./profile";
|
||||
import { getParsedTeam } from "./teamUtils";
|
||||
import type { User as UserType, Prisma } from ".prisma/client";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["[repository/user]"] });
|
||||
|
||||
@@ -202,6 +202,30 @@ export class UserRepository {
|
||||
return user.profiles.some((profile) => profile.organizationId === organizationId);
|
||||
}
|
||||
|
||||
static async findIfAMemberOfSomeOrganization({ user }: { user: { id: number } }) {
|
||||
return !!(
|
||||
await ProfileRepository.findManyForUser({
|
||||
id: user.id,
|
||||
})
|
||||
).length;
|
||||
}
|
||||
|
||||
static isMigratedToOrganization({
|
||||
user,
|
||||
}: {
|
||||
user: {
|
||||
metadata?: {
|
||||
migratedToOrgFrom?: unknown;
|
||||
} | null;
|
||||
};
|
||||
}) {
|
||||
return !!user.metadata?.migratedToOrgFrom;
|
||||
}
|
||||
|
||||
static async isMovedToAProfile({ user }: { user: Pick<UserType, "movedToProfileId"> }) {
|
||||
return !!user.movedToProfileId;
|
||||
}
|
||||
|
||||
static async enrichUserWithTheProfile<T extends { username: string | null; id: number }>({
|
||||
user,
|
||||
upId,
|
||||
@@ -233,13 +257,19 @@ export class UserRepository {
|
||||
user,
|
||||
}: {
|
||||
user: T;
|
||||
}): Promise<T & { profile: UserProfile }> {
|
||||
}): Promise<
|
||||
T & {
|
||||
nonProfileUsername: string | null;
|
||||
profile: UserProfile;
|
||||
}
|
||||
> {
|
||||
const profiles = await ProfileRepository.findManyForUser({ id: user.id });
|
||||
if (profiles.length) {
|
||||
const profile = profiles[0];
|
||||
return {
|
||||
...user,
|
||||
username: profile.username,
|
||||
nonProfileUsername: user.username,
|
||||
profile,
|
||||
};
|
||||
}
|
||||
@@ -247,6 +277,7 @@ export class UserRepository {
|
||||
// If no organization profile exists, use the personal profile so that the returned user is normalized to have a profile always
|
||||
return {
|
||||
...user,
|
||||
nonProfileUsername: user.username,
|
||||
profile: ProfileRepository.buildPersonalProfileFromUser({ user }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -329,6 +329,14 @@ export const userMetadata = z
|
||||
})
|
||||
.optional(),
|
||||
defaultBookerLayouts: bookerLayouts.optional(),
|
||||
migratedToOrgFrom: z
|
||||
.object({
|
||||
username: z.string().or(z.null()).optional(),
|
||||
lastMigrationTime: z.string().optional(),
|
||||
reverted: z.boolean().optional(),
|
||||
revertTime: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.nullable();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user