Skip to content

Commit

Permalink
feat: new privacy settings for orgs (calcom#13693)
Browse files Browse the repository at this point in the history
* moved privacy setting to new tab

* i18n

* added more privacy switches

* fix: hide org teams and members

* fix: use i18n

* fix: privacy page

* feat: also hide team mebers when org is private

* chore: remove log

* fix type error

* chore: type err

* chore: feedback

* chore: type err

---------

Co-authored-by: Udit Takkar <[email protected]>
Co-authored-by: Udit Takkar <[email protected]>
Co-authored-by: CarinaWolli <[email protected]>
  • Loading branch information
4 people authored Mar 12, 2024
1 parent 0e11002 commit f2478ea
Show file tree
Hide file tree
Showing 10 changed files with 133 additions and 26 deletions.
33 changes: 27 additions & 6 deletions apps/web/lib/team/[slug]/getServerSideProps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,17 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
},
}),
},
include: {
parent: {
select: {
id: true,
slug: true,
name: true,
isPrivate: true,
isOrganization: true,
},
},
},
});

if (!unpublishedTeam) return { notFound: true } as const;
Expand All @@ -103,19 +114,23 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
} as const;
}

const isTeamOrParentOrgPrivate = team.isPrivate || (team.parent?.isOrganization && team.parent?.isPrivate);

team.eventTypes =
team.eventTypes?.map((type) => ({
...type,
users: type.users.map((user) => ({
...user,
avatar: `/${user.username}/avatar.png`,
})),
users: !isTeamOrParentOrgPrivate
? type.users.map((user) => ({
...user,
avatar: `/${user.username}/avatar.png`,
}))
: [],
descriptionAsSafeHTML: markdownToSafeHTML(type.description),
})) ?? null;

const safeBio = markdownToSafeHTML(team.bio) || "";

const members = !team.isPrivate
const members = !isTeamOrParentOrgPrivate
? team.members.map((member) => {
return {
name: member.name,
Expand All @@ -139,7 +154,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>

return {
props: {
team: { ...serializableTeam, safeBio, members, metadata },
team: {
...serializableTeam,
safeBio,
members,
metadata,
children: isTeamOrParentOrgPrivate ? [] : team.children,
},
themeBasis: serializableTeam.slug,
trpcState: ssr.dehydrate(),
markdownStrippedBio,
Expand Down
9 changes: 9 additions & 0 deletions apps/web/pages/settings/organizations/privacy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import PrivacyView from "@calcom/features/ee/organizations/pages/settings/privacy";

import type { CalPageWrapper } from "@components/PageWrapper";
import PageWrapper from "@components/PageWrapper";

const Page = PrivacyView as CalPageWrapper;
Page.PageWrapper = PageWrapper;

export default Page;
14 changes: 11 additions & 3 deletions apps/web/pages/team/[slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ function TeamPage({
const isBioEmpty = !team.bio || !team.bio.replace("<p><br></p>", "").length;
const metadata = teamMetadataSchema.parse(team.metadata);

const teamOrOrgIsPrivate = team.isPrivate || (team?.parent?.isOrganization && team.parent?.isPrivate);

useEffect(() => {
telemetry.event(
telemetryEventTypes.pageView,
Expand Down Expand Up @@ -194,11 +196,17 @@ function TeamPage({
)}
</div>
{team.isOrganization ? (
<SubTeams />
!teamOrOrgIsPrivate ? (
<SubTeams />
) : (
<div className="w-full text-center">
<h2 className="text-emphasis font-semibold">{t("you_cannot_see_teams_of_org")}</h2>
</div>
)
) : (
<>
{(showMembers.isOn || !team.eventTypes?.length) &&
(team.isPrivate ? (
(teamOrOrgIsPrivate ? (
<div className="w-full text-center">
<h2 data-testid="you-cannot-see-team-members" className="text-emphasis font-semibold">
{t("you_cannot_see_team_members")}
Expand All @@ -212,7 +220,7 @@ function TeamPage({
<EventTypes eventTypes={team.eventTypes} />

{/* Hide "Book a team member button when team is private or hideBookATeamMember is true" */}
{!team.hideBookATeamMember && !team.isPrivate && (
{!team.hideBookATeamMember && !teamOrOrgIsPrivate && (
<div>
<div className="relative mt-12">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
Expand Down
4 changes: 4 additions & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,7 @@
"make_team_private": "Make team private",
"make_team_private_description": "Your team members won't be able to see other team members when this is turned on.",
"you_cannot_see_team_members": "You cannot see all the team members of a private team.",
"you_cannot_see_teams_of_org":"You cannot see teams of a private organization.",
"allow_booker_to_select_duration": "Allow booker to select duration",
"impersonate_user_tip": "All uses of this feature is audited.",
"impersonating_user_warning": "Impersonating username \"{{user}}\".",
Expand Down Expand Up @@ -1620,6 +1621,7 @@
"test_routing": "Test Routing",
"payment_app_disabled": "An admin has disabled a payment app",
"edit_event_type": "Edit event type",
"only_admin_can_see_members_of_org": "This Organization is private, and only the organization's admin or owner can view its members.",
"collective_scheduling": "Collective Scheduling",
"make_it_easy_to_book": "Make it easy to book your team when everyone is available.",
"find_the_best_person": "Find the best person available and cycle through your team.",
Expand Down Expand Up @@ -2297,6 +2299,8 @@
"field_identifiers_as_variables": "Use field identifiers as variables for your custom event redirect",
"field_identifiers_as_variables_with_example": "Use field identifiers as variables for your custom event redirect (e.g. {{variable}})",
"account_already_linked": "Account is already linked",
"privacy_organization_description": "Manage privacy settings for your organization",
"privacy": "Privacy",
"team_will_be_under_org": "New teams will be under your organization",
"add_group_name": "Add group name",
"group_name": "Group Name",
Expand Down
26 changes: 10 additions & 16 deletions packages/features/ee/organizations/pages/settings/members.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import MakeTeamPrivateSwitch from "@calcom/features/ee/teams/components/MakeTeamPrivateSwitch";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { UserListTable } from "@calcom/features/users/components/UserTable/UserListTable";
import { useLocale } from "@calcom/lib/hooks/useLocale";
Expand All @@ -11,29 +10,24 @@ import { Meta } from "@calcom/ui";

const MembersView = () => {
const { t } = useLocale();
const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery();
const { data: currentOrg, isPending } = trpc.viewer.organizations.listCurrent.useQuery();

const isInviteOpen = !currentOrg?.user.accepted;
const isOrgAdminOrOwner =
currentOrg &&
(currentOrg.user.role === MembershipRole.OWNER || currentOrg.user.role === MembershipRole.ADMIN);

const canLoggedInUserSeeMembers =
(currentOrg?.isPrivate && isOrgAdminOrOwner) || isOrgAdminOrOwner || !currentOrg?.isPrivate;

return (
<LicenseRequired>
<Meta title={t("organization_members")} description={t("organization_description")} />
<div>
{((currentOrg?.isPrivate && isOrgAdminOrOwner) || isOrgAdminOrOwner || !currentOrg?.isPrivate) && (
<UserListTable />
)}
{currentOrg && isOrgAdminOrOwner && (
<MakeTeamPrivateSwitch
isOrg={true}
teamId={currentOrg.id}
isPrivate={currentOrg.isPrivate}
disabled={isInviteOpen}
/>
)}
</div>
<div>{!isPending && canLoggedInUserSeeMembers && <UserListTable />}</div>
{!canLoggedInUserSeeMembers && (
<div className="border-subtle rounded-xl border p-6">
<h2 className="text-default">{t("only_admin_can_see_members_of_org")}</h2>
</div>
)}
</LicenseRequired>
);
};
Expand Down
43 changes: 43 additions & 0 deletions packages/features/ee/organizations/pages/settings/privacy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"use client";

import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import MakeTeamPrivateSwitch from "@calcom/features/ee/teams/components/MakeTeamPrivateSwitch";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { MembershipRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import { Meta } from "@calcom/ui";

const PrivacyView = () => {
const { t } = useLocale();
const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery();
const isOrgAdminOrOwner =
currentOrg &&
(currentOrg.user.role === MembershipRole.OWNER || currentOrg.user.role === MembershipRole.ADMIN);
const isInviteOpen = !currentOrg?.user.accepted;

const isDisabled = isInviteOpen || !isOrgAdminOrOwner;

if (!currentOrg) return null;

return (
<LicenseRequired>
<Meta
borderInShellHeader={false}
title={t("privacy")}
description={t("privacy_organization_description")}
/>
<div>
<MakeTeamPrivateSwitch
isOrg={true}
teamId={currentOrg.id}
isPrivate={currentOrg.isPrivate}
disabled={isDisabled}
/>
</div>
</LicenseRequired>
);
};
PrivacyView.getLayout = getLayout;

export default PrivacyView;
4 changes: 4 additions & 0 deletions packages/features/settings/layouts/SettingsLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ const tabs: VerticalTabItemProps[] = [
name: "members",
href: "/settings/organizations/members",
},
{
name: "privacy",
href: "/settings/organizations/privacy",
},
{
name: "appearance",
href: "/settings/organizations/appearance",
Expand Down
2 changes: 2 additions & 0 deletions packages/lib/server/queries/teams/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ export async function getTeamWithMembers(args: {
id: true,
slug: true,
name: true,
isPrivate: true,
isOrganization: true,
},
},
children: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => {
throw new TRPCError({ code: "NOT_FOUND", message: "User is not part of any organization." });
}

if (ctx.user.organization.isPrivate && !ctx.user.organization.isOrgAdmin) {
return {
canUserGetMembers: false,
rows: [],
meta: {
totalRowCount: 0,
},
};
}

const { cursor, limit } = input;

const getTotalMembers = await prisma.membership.count({
Expand Down
14 changes: 13 additions & 1 deletion packages/trpc/server/routers/viewer/teams/get.handler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
import { MembershipRole } from "@calcom/prisma/enums";

import { TRPCError } from "@trpc/server";

Expand Down Expand Up @@ -31,9 +32,20 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
if (!membership) {
throw new TRPCError({ code: "NOT_FOUND", message: "Not a member of this team." });
}
const { members, ...restTeam } = team;

// Hide Members of team when 1) Org is private and logged in user is not admin or owner
// OR
// 2)Team is private and logged in user is not admin or owner of team or Organization's admin or owner
const hideMembers =
(ctx.user.profile?.organization?.isPrivate && !ctx.user.organization?.isOrgAdmin) ||
(team.isPrivate &&
!(membership.role === MembershipRole.OWNER || membership.role === MembershipRole.ADMIN) &&
!ctx.user.organization?.isOrgAdmin);

return {
...team,
...restTeam,
members: hideMembers ? [] : members,
safeBio: markdownToSafeHTML(team.bio),
membership: {
role: membership.role,
Expand Down

0 comments on commit f2478ea

Please sign in to comment.