fix: #16028 Org Settings - SEOIndexing,Disable Org Url (#16075)

Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
This commit is contained in:
Vijay
2024-11-29 10:21:14 +05:30
committed by GitHub
co-authored by Amit Sharma Hariom Syed Ali Shahbaz
parent b5c6d7af91
commit f631abfdc4
20 changed files with 454 additions and 23 deletions
@@ -5,6 +5,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { getBookingForReschedule } from "@calcom/features/bookings/lib/get-booking";
import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { RedirectType } from "@calcom/prisma/client";
@@ -51,7 +52,18 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
select: {
id: true,
hideBranding: true,
parent: true,
parent: {
select: {
slug: true,
name: true,
bannerUrl: true,
organizationSettings: {
select: {
allowSEOIndexing: true,
},
},
},
},
name: true,
slug: true,
eventTypes: {
@@ -66,6 +78,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
length: true,
},
},
isOrganization: true,
organizationSettings: {
select: {
allowSEOIndexing: true,
},
},
},
});
@@ -98,6 +116,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
eventData,
});
const organizationSettings = OrganizationRepository.utils.getOrganizationSEOSettings(team);
const allowSEOIndexing = organizationSettings?.allowSEOIndexing ?? false;
return {
props: {
eventData: {
@@ -124,6 +145,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
isSEOIndexable: allowSEOIndexing,
},
};
};
@@ -7,10 +7,11 @@ import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
import logger from "@calcom/lib/logger";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
import slugify from "@calcom/lib/slugify";
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
import prisma from "@calcom/prisma";
import type { Team } from "@calcom/prisma/client";
import type { Team, OrganizationSettings } from "@calcom/prisma/client";
import { RedirectType } from "@calcom/prisma/client";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
@@ -20,6 +21,30 @@ import { ssrInit } from "@server/lib/ssr";
const log = logger.getSubLogger({ prefix: ["team/[slug]"] });
function getOrgProfileRedirectToVerifiedDomain(
team: {
isOrganization: boolean;
},
settings: Pick<OrganizationSettings, "orgAutoAcceptEmail" | "orgProfileRedirectsToVerifiedDomain">
) {
if (!team.isOrganization) {
return null;
}
const verifiedDomain = OrganizationRepository.utils.getVerifiedDomain(settings);
if (!settings.orgProfileRedirectsToVerifiedDomain || !verifiedDomain) {
return null;
}
return {
redirect: {
permanent: false,
destination: `https://${verifiedDomain}`,
},
};
}
const getTheLastArrayElement = (value: ReadonlyArray<string> | string | undefined): string | undefined => {
if (value === undefined || typeof value === "string") {
return value;
@@ -120,6 +145,17 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
} as const;
}
const organizationSettings = OrganizationRepository.utils.getOrganizationSettings(team);
const allowSEOIndexing = organizationSettings?.allowSEOIndexing ?? false;
const redirectToVerifiedDomain = organizationSettings
? getOrgProfileRedirectToVerifiedDomain(team, organizationSettings)
: null;
if (redirectToVerifiedDomain) {
return redirectToVerifiedDomain;
}
const isTeamOrParentOrgPrivate = team.isPrivate || (team.parent?.isOrganization && team.parent?.isPrivate);
team.eventTypes =
@@ -189,6 +225,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
markdownStrippedBio,
isValidOrgDomain,
currentOrgDomain,
isSEOIndexable: allowSEOIndexing,
},
} as const;
};
@@ -180,7 +180,11 @@ const GeneralView = ({ localeProp, user, travelSchedules, revalidatePage }: Gene
const [isAllowDynamicBookingChecked, setIsAllowDynamicBookingChecked] = useState(
!!user.allowDynamicBooking
);
const [isAllowSEOIndexingChecked, setIsAllowSEOIndexingChecked] = useState(!!user.allowSEOIndexing);
const [isAllowSEOIndexingChecked, setIsAllowSEOIndexingChecked] = useState(
user.organizationSettings?.allowSEOIndexing === false
? !!user.organizationSettings?.allowSEOIndexing
: !!user.allowSEOIndexing
);
const [isReceiveMonthlyDigestEmailChecked, setIsReceiveMonthlyDigestEmailChecked] = useState(
!!user.receiveMonthlyDigestEmail
);
@@ -359,10 +363,11 @@ const GeneralView = ({ localeProp, user, travelSchedules, revalidatePage }: Gene
/>
<SettingsToggle
data-testid="my-seo-indexing-switch"
toggleSwitchAtTheEnd={true}
title={t("seo_indexing")}
description={t("allow_seo_indexing")}
disabled={mutation.isPending}
disabled={mutation.isPending || user.organizationSettings?.allowSEOIndexing === false}
checked={isAllowSEOIndexingChecked}
onCheckedChange={(checked) => {
setIsAllowSEOIndexingChecked(checked);
+5
View File
@@ -35,6 +35,7 @@ function TeamPage({
markdownStrippedBio,
isValidOrgDomain,
currentOrgDomain,
isSEOIndexable,
}: PageProps) {
useTheme(team.theme);
const routerQuery = useRouterQuery();
@@ -175,6 +176,10 @@ function TeamPage({
image: profileImageSrc,
},
}}
nextSeoProps={{
nofollow: !isSEOIndexable,
noindex: !isSEOIndexable,
}}
/>
<main className="dark:bg-darkgray-50 bg-subtle mx-auto max-w-3xl rounded-md px-4 pb-12 pt-12">
<div className="mx-auto mb-8 max-w-3xl text-center">
+2
View File
@@ -34,6 +34,7 @@ function Type({
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
isSEOIndexable,
}: PageProps) {
const searchParams = useSearchParams();
@@ -47,6 +48,7 @@ function Type({
isTeamEvent
entity={eventData.entity}
bookingData={booking}
isSEOIndexable={isSEOIndexable}
/>
<Booker
username={user}
@@ -47,6 +47,7 @@ function mockedUserPageComponentProps(props: Partial<React.ComponentProps<typeof
...(props.entity ?? null),
},
eventTypes: [],
isOrgSEOIndexable: false,
} satisfies React.ComponentProps<typeof UserPage>;
}
@@ -22,7 +22,7 @@ import type { getServerSideProps } from "@server/lib/[user]/getServerSideProps";
export type PageProps = InferGetServerSidePropsType<typeof getServerSideProps>;
export function UserPage(props: PageProps) {
const { users, profile, eventTypes, markdownStrippedBio, entity } = props;
const { users, profile, eventTypes, markdownStrippedBio, entity, isOrgSEOIndexable } = props;
const [user] = users; //To be used when we only have a single user, not dynamic group
useTheme(profile.theme);
@@ -59,6 +59,13 @@ export function UserPage(props: PageProps) {
const isEventListEmpty = eventTypes.length === 0;
const isOrg = !!user?.profile?.organization;
const allowSEOIndexing = isOrg
? isOrgSEOIndexable
? profile.allowSEOIndexing
: false
: profile.allowSEOIndexing;
return (
<>
<HeadSeo
@@ -71,8 +78,8 @@ export function UserPage(props: PageProps) {
users: [{ username: `${user.username}`, name: `${user.name}` }],
}}
nextSeoProps={{
noindex: !profile.allowSEOIndexing,
nofollow: !profile.allowSEOIndexing,
noindex: !allowSEOIndexing,
nofollow: !allowSEOIndexing,
}}
/>
@@ -0,0 +1,217 @@
import { expect } from "@playwright/test";
import { doOnOrgDomain } from "playwright/lib/testUtils";
import { SchedulingType } from "@calcom/prisma/enums";
import { test } from "../lib/fixtures";
test.afterEach(({ users }) => {
users.deleteAll();
});
test.describe("Organization Settings", () => {
test("Setting - 'Allow search engine indexing' inside Org profile settings", async ({ page, users }) => {
const orgMember = await users.create(undefined, {
hasTeam: true,
isOrg: true,
hasSubteam: true,
isOrgVerified: true,
isDnsSetup: true,
orgRequestedSlug: "example",
schedulingType: SchedulingType.ROUND_ROBIN,
});
const { team: org } = await orgMember.getOrgMembership();
const { team } = await orgMember.getFirstTeamMembership();
const teamEvent = await orgMember.getFirstTeamEvent(team.id);
const userEvent = orgMember.eventTypes[0];
await orgMember.apiLogin();
await test.step("Set to true, enables 'Allow search engine indexing' switch on my-account/general settings", async () => {
await page.goto(`/settings/organizations/profile`);
const orgSeoIndexingSwitch = await page.getByTestId(`${org.id}-seo-indexing-switch`);
await orgSeoIndexingSwitch.click();
await expect(orgSeoIndexingSwitch).toBeChecked();
await page.goto(`/settings/my-account/general`);
const mySeoIndexingSwitch = await page.getByTestId("my-seo-indexing-switch");
await expect(mySeoIndexingSwitch).toBeEnabled();
});
await test.step("Set to false, disables and sets to false 'Allow search engine indexing' switch on my-account/general settings", async () => {
await page.goto(`/settings/organizations/profile`);
const orgSeoIndexingSwitch = await page.getByTestId(`${org.id}-seo-indexing-switch`);
await orgSeoIndexingSwitch.click();
await expect(orgSeoIndexingSwitch).toBeChecked({ checked: false });
await page.goto(`/settings/my-account/general`);
const mySeoIndexingSwitch = await page.getByTestId("my-seo-indexing-switch");
await expect(mySeoIndexingSwitch).toBeChecked({ checked: false });
await expect(mySeoIndexingSwitch).toBeDisabled();
});
await test.step("Set to false, sets 'robots' meta on 'head' element to 'noindex,nofollow', for team page", async () => {
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async ({ page, goToUrlWithErrorHandling }) => {
const result = await goToUrlWithErrorHandling(`/team/${team.slug}`);
const metaTag = await page.locator('head > meta[name="robots"]');
const metaTagValue = await metaTag.getAttribute("content");
expect(metaTagValue).toEqual("noindex,nofollow"); //disabled seo-indexing
return { url: result.url };
}
);
});
await test.step("Set to false, sets 'robots' meta on 'head' element to 'noindex,nofollow', for team event page", async () => {
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async ({ page, goToUrlWithErrorHandling }) => {
const result = await goToUrlWithErrorHandling(`/team/${team.slug}/${teamEvent.slug}`);
const metaTag = await page.locator('head > meta[name="robots"]');
const metaTagValue = await metaTag.getAttribute("content");
expect(metaTagValue).toEqual("noindex,nofollow"); //disabled seo-indexing
return { url: result.url };
}
);
});
await test.step("Set to false, sets 'robots' meta on 'head' element to 'noindex,nofollow', for user page", async () => {
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async ({ page, goToUrlWithErrorHandling }) => {
const result = await goToUrlWithErrorHandling(`/${orgMember.username}`);
const metaTag = await page.locator('head > meta[name="robots"]');
const metaTagValue = await metaTag.getAttribute("content");
expect(metaTagValue).toEqual("noindex,nofollow"); //disabled seo-indexing
return { url: result.url };
}
);
});
await test.step("Set to false, sets 'robots' meta on 'head' element to 'noindex,nofollow', for user eventtype page", async () => {
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async ({ page, goToUrlWithErrorHandling }) => {
const result = await goToUrlWithErrorHandling(`/${orgMember.username}/${userEvent.slug}`);
const metaTag = await page.locator('head > meta[name="robots"]');
const metaTagValue = await metaTag.getAttribute("content");
expect(metaTagValue).toEqual("noindex,nofollow"); //disabled seo-indexing
return { url: result.url };
}
);
});
await test.step("Set to true, sets 'robots' meta on 'head' element to 'index,follow', for team page", async () => {
await page.goto(`/settings/organizations/profile`);
const orgSeoIndexingSwitch = await page.getByTestId(`${org.id}-seo-indexing-switch`);
await orgSeoIndexingSwitch.click();
await expect(orgSeoIndexingSwitch).toBeChecked();
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async ({ page, goToUrlWithErrorHandling }) => {
const result = await goToUrlWithErrorHandling(`/team/${team.slug}`);
await page.reload();
const metaTag = await page.locator('head > meta[name="robots"]');
const metaTagValue = await metaTag.getAttribute("content");
expect(metaTagValue).toEqual("index,follow"); //enabled seo-indexing
return { url: result.url };
}
);
});
await test.step("Set to true, sets 'robots' meta on 'head' element to 'index,follow', for team event page", async () => {
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async ({ page, goToUrlWithErrorHandling }) => {
const result = await goToUrlWithErrorHandling(`/team/${team.slug}/${teamEvent.slug}`);
await page.reload();
const metaTag = await page.locator('head > meta[name="robots"]');
const metaTagValue = await metaTag.getAttribute("content");
expect(metaTagValue).toEqual("index,follow"); //enabled seo-indexing
return { url: result.url };
}
);
});
await test.step("Set to true and user's 'Allow search engine indexing' setting also set to true, user's setting is respected, for user page", async () => {
await page.goto(`/settings/my-account/general`);
const mySeoIndexingSwitch = await page.getByTestId("my-seo-indexing-switch");
await expect(mySeoIndexingSwitch).toBeEnabled();
await expect(mySeoIndexingSwitch).toBeChecked();
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async ({ page, goToUrlWithErrorHandling }) => {
const result = await goToUrlWithErrorHandling(`/${orgMember.username}`);
await page.reload();
const metaTag = await page.locator('head > meta[name="robots"]');
const metaTagValue = await metaTag.getAttribute("content");
expect(metaTagValue).toEqual("index,follow"); //enabled seo-indexing
return { url: result.url };
}
);
});
await test.step("Set to true and user's 'Allow search engine indexing' setting also set to true, user's setting is respected, for user eventtype page", async () => {
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async ({ page, goToUrlWithErrorHandling }) => {
const result = await goToUrlWithErrorHandling(`/${orgMember.username}/${userEvent.slug}`);
await page.reload();
const metaTag = await page.locator('head > meta[name="robots"]');
const metaTagValue = await metaTag.getAttribute("content");
expect(metaTagValue).toEqual("index,follow"); //enabled seo-indexing
return { url: result.url };
}
);
});
await test.step("Set to false and user's 'Allow search engine indexing' setting was true, org's setting is prioritized, for user eventtype page", async () => {
await page.goto(`/settings/organizations/profile`);
const orgSeoIndexingSwitch = await page.getByTestId(`${org.id}-seo-indexing-switch`);
await orgSeoIndexingSwitch.click();
await expect(orgSeoIndexingSwitch).toBeChecked({ checked: false });
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async ({ page, goToUrlWithErrorHandling }) => {
const result = await goToUrlWithErrorHandling(`/${orgMember.username}/${userEvent.slug}`);
await page.reload();
const metaTag = await page.locator('head > meta[name="robots"]');
const metaTagValue = await metaTag.getAttribute("content");
expect(metaTagValue).toEqual("noindex,nofollow"); //disabled seo-indexing
return { url: result.url };
}
);
});
});
});
@@ -2819,7 +2819,9 @@
"rr_distribution_method_availability_description": "Allows bookers to book meetings whenever a host is available. Use this to maximize the number of potential meetings booked and when the even distribution of meetings across hosts is less important.",
"rr_distribution_method_balanced_title": "Load balancing",
"rr_distribution_method_balanced_description": "We will monitor how many bookings have been made with each host and compare this with others, disabling some hosts that are too far ahead so bookings are evenly distributed.",
"exclude_emails_that_contain" : "Exclude emails that contain ...",
"exclude_emails_match_found_error_message" : "Please enter a valid work email address",
"exclude_emails_that_contain": "Exclude emails that contain ...",
"exclude_emails_match_found_error_message": "Please enter a valid work email address",
"disable_org_url_label": "Disable public organization profile and redirect",
"disable_org_url_description": "Redirects {{orgSlug}}.cal.com to {{destination}}",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -219,6 +219,12 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
} as const;
}
const allowSEOIndexing = org
? user?.profile?.organization?.organizationSettings?.allowSEOIndexing
? user?.allowSEOIndexing
: false
: user?.allowSEOIndexing;
const props: Props = {
eventData: {
id: eventData.id,
@@ -230,7 +236,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
slug,
trpcState: ssr.dehydrate(),
isBrandingHidden: user?.hideBranding,
isSEOIndexable: user?.allowSEOIndexing,
isSEOIndexable: allowSEOIndexing,
themeBasis: username,
bookingUid: bookingUid ? `${bookingUid}` : null,
rescheduleUid: null,
@@ -69,6 +69,7 @@ type UserPageProps = {
| "currency"
| "recurringEvent"
>)[];
isOrgSEOIndexable: boolean | undefined;
} & EmbedProps;
export const getServerSideProps: GetServerSideProps<UserPageProps> = async (context) => {
@@ -194,6 +195,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
themeBasis: user.username,
trpcState: ssr.dehydrate(),
markdownStrippedBio,
isOrgSEOIndexable: org?.organizationSettings?.allowSEOIndexing,
},
};
};
@@ -43,6 +43,12 @@ const OrgAppearanceView = ({
} = themeForm;
const [hideBrandingValue, setHideBrandingValue] = useState(currentOrg?.hideBranding ?? false);
const [allowSEOIndexingValue, setAllowSEOIndexingValue] = useState(
currentOrg?.organizationSettings?.allowSEOIndexing ?? false
);
const [orgProfileRedirectsToVerifiedDomainValue, setOrgProfileRedirectsToVerifiedDomainValue] = useState(
currentOrg?.organizationSettings?.orgProfileRedirectsToVerifiedDomain ?? false
);
const brandColorsFormMethods = useForm<BrandColorsFormValues>({
defaultValues: {
@@ -153,6 +159,36 @@ const OrgAppearanceView = ({
}}
switchContainerClassName="mt-6"
/>
<SettingsToggle
data-testid={`${currentOrg?.id}-seo-indexing-switch`}
toggleSwitchAtTheEnd={true}
title={t("seo_indexing")}
description={t("allow_seo_indexing")}
disabled={mutation.isPending}
checked={allowSEOIndexingValue}
onCheckedChange={(checked) => {
setAllowSEOIndexingValue(checked);
mutation.mutate({ allowSEOIndexing: checked });
}}
switchContainerClassName="mt-6"
/>
<SettingsToggle
toggleSwitchAtTheEnd={true}
title={t("disable_org_url_label")}
description={t("disable_org_url_description", {
orgSlug: currentOrg?.slug,
destination: currentOrg?.organizationSettings?.orgAutoAcceptEmail,
})}
disabled={mutation.isPending}
checked={orgProfileRedirectsToVerifiedDomainValue}
onCheckedChange={(checked) => {
setOrgProfileRedirectsToVerifiedDomainValue(checked);
mutation.mutate({ orgProfileRedirectsToVerifiedDomain: checked });
}}
switchContainerClassName="mt-6"
/>
</div>
);
};
@@ -101,6 +101,13 @@ export async function getTeamWithMembers(args: {
isOrganization: true,
logoUrl: true,
metadata: true,
organizationSettings: {
select: {
allowSEOIndexing: true,
orgProfileRedirectsToVerifiedDomain: true,
orgAutoAcceptEmail: true,
},
},
},
},
parentId: true,
@@ -158,6 +165,13 @@ export async function getTeamWithMembers(args: {
identifier: true,
},
},
organizationSettings: {
select: {
allowSEOIndexing: true,
orgProfileRedirectsToVerifiedDomain: true,
orgAutoAcceptEmail: true,
},
},
},
} as const;
@@ -3,6 +3,7 @@ import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { prisma } from "@calcom/prisma";
import type { OrganizationSettings } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
@@ -10,6 +11,11 @@ import { createAProfileForAnExistingUser } from "../../createAProfileForAnExisti
import { getParsedTeam } from "./teamUtils";
import { UserRepository } from "./user";
type MinimumOrganizationSettings = Pick<
OrganizationSettings,
"orgAutoAcceptEmail" | "orgProfileRedirectsToVerifiedDomain" | "allowSEOIndexing"
>;
type SEOOrganizationSettings = Pick<OrganizationSettings, "allowSEOIndexing">;
const orgSelect = {
id: true,
name: true,
@@ -215,6 +221,9 @@ export class OrganizationRepository {
lockEventTypeCreationForUsers: true,
adminGetsNoSlotsNotification: true,
isAdminReviewed: true,
allowSEOIndexing: true,
orgProfileRedirectsToVerifiedDomain: true,
orgAutoAcceptEmail: true,
},
});
@@ -229,6 +238,9 @@ export class OrganizationRepository {
organizationSettings: {
lockEventTypeCreationForUsers: organizationSettings?.lockEventTypeCreationForUsers,
adminGetsNoSlotsNotification: organizationSettings?.adminGetsNoSlotsNotification,
allowSEOIndexing: organizationSettings?.allowSEOIndexing,
orgProfileRedirectsToVerifiedDomain: organizationSettings?.orgProfileRedirectsToVerifiedDomain,
orgAutoAcceptEmail: organizationSettings?.orgAutoAcceptEmail,
},
user: {
role: membership?.role,
@@ -311,4 +323,38 @@ export class OrganizationRepository {
return org?.calVideoLogo;
}
static utils = {
/**
* Gets the organization setting if the team is an organization.
* If not, it gets the organization setting of the parent organization.
*/
getOrganizationSettings: (team: {
isOrganization: boolean;
organizationSettings: MinimumOrganizationSettings | null;
parent: {
organizationSettings: MinimumOrganizationSettings | null;
} | null;
}) => {
if (!team) return null;
if (team.isOrganization) return team.organizationSettings ?? null;
if (!team.parent) return null;
return team.parent.organizationSettings ?? null;
},
getOrganizationSEOSettings: (team: {
isOrganization: boolean;
organizationSettings: SEOOrganizationSettings | null;
parent: {
organizationSettings: SEOOrganizationSettings | null;
} | null;
}) => {
if (!team) return null;
if (team.isOrganization) return team.organizationSettings ?? null;
if (!team.parent) return null;
return team.parent.organizationSettings ?? null;
},
getVerifiedDomain(settings: Pick<OrganizationSettings, "orgAutoAcceptEmail">) {
return settings.orgAutoAcceptEmail;
},
};
}
+12 -1
View File
@@ -36,6 +36,10 @@ const membershipSelect = Prisma.validator<Prisma.MembershipSelect>()({
});
const log = logger.getSubLogger({ prefix: ["repository/profile"] });
const organizationSettingsSelect = Prisma.validator<Prisma.OrganizationSettingsSelect>()({
allowSEOIndexing: true,
orgProfileRedirectsToVerifiedDomain: true,
});
const organizationSelect = {
id: true,
slug: true,
@@ -45,6 +49,12 @@ const organizationSelect = {
bannerUrl: true,
isPlatform: true,
};
const organizationWithSettingsSelect = {
...organizationSelect,
organizationSettings: {
select: organizationSettingsSelect,
},
};
export enum LookupTarget {
User,
@@ -373,6 +383,7 @@ export class ProfileRepository {
organizationSettings: {
select: {
lockEventTypeCreationForUsers: true,
allowSEOIndexing: true,
},
},
members: {
@@ -416,7 +427,7 @@ export class ProfileRepository {
select: userSelect,
},
organization: {
select: organizationSelect,
select: organizationWithSettingsSelect,
},
},
});
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "OrganizationSettings" ADD COLUMN "allowSEOIndexing" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "orgProfileRedirectsToVerifiedDomain" BOOLEAN NOT NULL DEFAULT false;
+15 -11
View File
@@ -469,20 +469,24 @@ model Team {
}
model OrganizationSettings {
id Int @id @default(autoincrement())
organization Team @relation(fields: [organizationId], references: [id], onDelete: Cascade)
organizationId Int @unique
isOrganizationConfigured Boolean @default(false)
id Int @id @default(autoincrement())
organization Team @relation(fields: [organizationId], references: [id], onDelete: Cascade)
organizationId Int @unique
isOrganizationConfigured Boolean @default(false)
// It decides if new organization members can be auto-accepted or not
isOrganizationVerified Boolean @default(false)
orgAutoAcceptEmail String
lockEventTypeCreationForUsers Boolean @default(false)
adminGetsNoSlotsNotification Boolean @default(false)
isOrganizationVerified Boolean @default(false)
// It is a domain e.g "acme.com". Any email with this domain might be auto-accepted
// Also, it is the domain to which the organization profile is redirected.
orgAutoAcceptEmail String
lockEventTypeCreationForUsers Boolean @default(false)
adminGetsNoSlotsNotification Boolean @default(false)
// It decides if instance ADMIN has reviewed the organization or not.
// It is used to allow super sensitive operations like 'impersonation of Org members by Org admin'
isAdminReviewed Boolean @default(false)
dSyncData DSyncData?
isAdminAPIEnabled Boolean @default(false)
isAdminReviewed Boolean @default(false)
dSyncData DSyncData?
isAdminAPIEnabled Boolean @default(false)
allowSEOIndexing Boolean @default(false)
orgProfileRedirectsToVerifiedDomain Boolean @default(false)
}
enum MembershipRole {
@@ -91,6 +91,7 @@ export const meHandler = async ({ ctx, input }: MeOptions) => {
username: user.profile?.username ?? user.username ?? null,
profile: user.profile ?? null,
profiles: allUserEnrichedProfiles,
organizationSettings: user?.profile?.organization?.organizationSettings,
};
return {
@@ -41,6 +41,14 @@ const updateOrganizationSettings = async ({
data.adminGetsNoSlotsNotification = input.adminGetsNoSlotsNotification;
}
if (input.hasOwnProperty("allowSEOIndexing")) {
data.allowSEOIndexing = input.allowSEOIndexing;
}
if (input.hasOwnProperty("orgProfileRedirectsToVerifiedDomain")) {
data.orgProfileRedirectsToVerifiedDomain = input.orgProfileRedirectsToVerifiedDomain;
}
// If no settings values have changed lets skip this update
if (Object.keys(data).length === 0) return;
@@ -131,7 +139,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
if (!prevOrganisation) throw new TRPCError({ code: "NOT_FOUND", message: "Organisation not found." });
const { mergeMetadata } = getMetadataHelpers(teamMetadataSchema.unwrap(), prevOrganisation.metadata);
const { mergeMetadata } = getMetadataHelpers(teamMetadataSchema.unwrap(), prevOrganisation.metadata ?? {});
const data: Prisma.TeamUpdateArgs["data"] = {
logoUrl: input.logoUrl,
@@ -31,6 +31,8 @@ export const ZUpdateInputSchema = z.object({
lockEventTypeCreation: z.boolean().optional(),
lockEventTypeCreationOptions: z.enum(["DELETE", "HIDE"]).optional(),
adminGetsNoSlotsNotification: z.boolean().optional(),
allowSEOIndexing: z.boolean().optional(),
orgProfileRedirectsToVerifiedDomain: z.boolean().optional(),
});
export type TUpdateInputSchema = z.infer<typeof ZUpdateInputSchema>;