diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/attributes/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/attributes/page.tsx index 55cbeac844c307..cfa0900043cfb7 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/attributes/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/attributes/page.tsx @@ -1,7 +1,15 @@ import { _generateMetadata, getTranslate } from "app/_utils"; +import { headers, cookies } from "next/headers"; +import { redirect } from "next/navigation"; import OrgSettingsAttributesPage from "@calcom/ee/organizations/pages/settings/attributes/attributes-list-view"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; +import { Resource } from "@calcom/features/pbac/domain/types/permission-registry"; +import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions"; import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; +import { MembershipRole } from "@calcom/prisma/enums"; + +import { buildLegacyRequest } from "@lib/buildLegacyCtx"; export const generateMetadata = async () => await _generateMetadata( @@ -14,10 +22,40 @@ export const generateMetadata = async () => const Page = async () => { const t = await getTranslate(); + const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) }); + + if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) { + return redirect("/settings/profile"); + } + + const { canRead, canEdit, canDelete, canCreate } = await getResourcePermissions({ + userId: session.user.id, + teamId: session.user.profile.organizationId, + resource: Resource.Attributes, + userRole: session.user.org.role, + fallbackRoles: { + read: { + roles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER], + }, + update: { + roles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }, + delete: { + roles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }, + create: { + roles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }, + }, + }); + + if (!canRead) { + return redirect("/settings/profile"); + } return ( - + ); }; diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/dsync/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/dsync/page.tsx index a53e48fd1ec599..161ce65275a78c 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/dsync/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/dsync/page.tsx @@ -1,7 +1,15 @@ import { _generateMetadata, getTranslate } from "app/_utils"; +import { headers, cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import DirectorySyncTeamView from "@calcom/features/ee/dsync/page/team-dsync-view"; +import { Resource } from "@calcom/features/pbac/domain/types/permission-registry"; +import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions"; import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; +import { MembershipRole } from "@calcom/prisma/enums"; + +import { buildLegacyRequest } from "@lib/buildLegacyCtx"; export const generateMetadata = async () => await _generateMetadata( @@ -14,10 +22,27 @@ export const generateMetadata = async () => const Page = async () => { const t = await getTranslate(); + const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) }); + + if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) { + return redirect("/settings/organizations/general"); + } + + const { canEdit } = await getResourcePermissions({ + userId: session.user.id, + teamId: session.user.profile.organizationId, + resource: Resource.Organization, + userRole: session.user.org.role, + fallbackRoles: { + update: { + roles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }, + }, + }); return ( - + ); }; diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/privacy/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/privacy/page.tsx index 8900c77c70038b..7a4e75a2f43e0a 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/privacy/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/privacy/page.tsx @@ -1,7 +1,15 @@ import { _generateMetadata, getTranslate } from "app/_utils"; +import { headers, cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import PrivacyView from "@calcom/features/ee/organizations/pages/settings/privacy"; +import { Resource } from "@calcom/features/pbac/domain/types/permission-registry"; +import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions"; import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; +import { MembershipRole } from "@calcom/prisma/enums"; + +import { buildLegacyRequest } from "@lib/buildLegacyCtx"; export const generateMetadata = async () => await _generateMetadata( @@ -15,9 +23,34 @@ export const generateMetadata = async () => const Page = async () => { const t = await getTranslate(); + const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) }); + + if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) { + return redirect("/settings/profile"); + } + + const { canRead, canEdit } = await getResourcePermissions({ + userId: session.user.id, + teamId: session.user.profile.organizationId, + resource: Resource.Organization, + userRole: session.user.org.role, + fallbackRoles: { + read: { + roles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER], + }, + update: { + roles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }, + }, + }); + + if (!canRead) { + return redirect("/settings/profile"); + } + return ( - + ); }; diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/sso/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/sso/page.tsx index 93750ac2fdadfe..a3fcf936b1cb6a 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/sso/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/(org-admin-only)/sso/page.tsx @@ -1,7 +1,15 @@ import { _generateMetadata, getTranslate } from "app/_utils"; +import { headers, cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import OrgSSOView from "@calcom/features/ee/sso/page/orgs-sso-view"; +import { Resource } from "@calcom/features/pbac/domain/types/permission-registry"; +import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions"; import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; +import { MembershipRole } from "@calcom/prisma/enums"; + +import { buildLegacyRequest } from "@lib/buildLegacyCtx"; export const generateMetadata = async () => await _generateMetadata( @@ -14,10 +22,27 @@ export const generateMetadata = async () => const Page = async () => { const t = await getTranslate(); + const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) }); + + if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) { + return redirect("/settings/organizations/general"); + } + + const { canEdit } = await getResourcePermissions({ + userId: session.user.id, + teamId: session.user.profile.organizationId, + resource: Resource.Organization, + userRole: session.user.org.role, + fallbackRoles: { + update: { + roles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }, + }, + }); return ( - + ); }; diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/general/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/general/page.tsx index 75aeaf2f6e8b36..5ce47fad0ba9b1 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/general/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/general/page.tsx @@ -1,7 +1,15 @@ import { _generateMetadata, getTranslate } from "app/_utils"; +import { headers, cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import LegacyPage from "@calcom/features/ee/organizations/pages/settings/general"; +import { Resource } from "@calcom/features/pbac/domain/types/permission-registry"; +import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions"; import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; +import { MembershipRole } from "@calcom/prisma/enums"; + +import { buildLegacyRequest } from "@lib/buildLegacyCtx"; export const generateMetadata = async () => await _generateMetadata( @@ -15,9 +23,30 @@ export const generateMetadata = async () => const Page = async () => { const t = await getTranslate(); + const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) }); + + if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) { + return redirect("/settings/profile"); + } + + const { canRead, canEdit } = await getResourcePermissions({ + userId: session.user.id, + teamId: session.user.profile.organizationId, + resource: Resource.Organization, + userRole: session.user.org.role, + fallbackRoles: { + read: { + roles: [MembershipRole.MEMBER, MembershipRole.ADMIN, MembershipRole.OWNER], + }, + update: { + roles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }, + }, + }); + return ( - + ); }; diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx index 3849a833050756..4b58650b45d306 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx @@ -1,7 +1,16 @@ import { _generateMetadata, getTranslate } from "app/_utils"; +import { cookies, headers } from "next/headers"; +import { redirect } from "next/navigation"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import LegacyPage from "@calcom/features/ee/organizations/pages/settings/profile"; +import { Resource } from "@calcom/features/pbac/domain/types/permission-registry"; +import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions"; import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; +import type { Membership } from "@calcom/prisma/client"; +import { MembershipRole } from "@calcom/prisma/enums"; + +import { buildLegacyRequest } from "@lib/buildLegacyCtx"; export const generateMetadata = async () => await _generateMetadata( @@ -13,14 +22,47 @@ export const generateMetadata = async () => ); const Page = async () => { + const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) }); const t = await getTranslate(); + const orgRole = session?.user.profile?.organization.members?.find( + (member: Membership) => member.userId === session?.user.id + )?.role; + + if (!session?.user.id || !session?.user.profile?.organizationId || !orgRole) { + return redirect("/settings/profile"); + } + + const { canRead, canEdit, canDelete } = await getResourcePermissions({ + userId: session.user.id, + teamId: session?.user.profile?.organizationId, + resource: Resource.Organization, + userRole: orgRole, + fallbackRoles: { + read: { + roles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }, + update: { + roles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }, + delete: { + roles: [MembershipRole.OWNER], + }, + }, + }); + return ( - + ); }; diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 11a55d364c781e..fe8f9ffdd56610 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -871,6 +871,10 @@ "create_team": "Create Team", "name": "Name", "nameless_team": "Nameless Team", + "oauth_clients": "OAuth Clients", + "oauth_clients_description": "Manage OAuth clients for your organization", + "create_oauth_client": "Create OAuth Client", + "create_oauth_client_description": "Create a new OAuth client for third-party integrations", "oauth_client_deletion_message": "OAuth client deleted successfully", "create_new_team_description": "Create a new team to collaborate with users.", "create_new_team": "Create a new team", @@ -1836,6 +1840,8 @@ "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.", "only_admin_can_manage_sso_org": "Only the organization's admin or owner can manage SSO settings", + "only_admin_can_manage_directory_sync": "Only the organization's admin or owner can manage directory sync settings", + "only_admin_can_manage_oauth_clients": "Only the organization's admin or owner can manage OAuth clients", "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.", @@ -3302,6 +3308,11 @@ "error_creating_role": "Error creating role", "error_updating_role": "Error updating role", "pbac_desc_create_roles": "Create roles", + "pbac_resource_attributes": "Attributes", + "pbac_desc_view_organization_attributes": "View organization attributes", + "pbac_desc_update_organization_attributes": "Update organization attributes", + "pbac_desc_delete_organization_attributes": "Delete organization attributes", + "pbac_desc_create_organization_attributes": "Create organization attributes", "pbac_desc_view_roles": "View roles", "pbac_desc_update_roles": "Update roles", "pbac_desc_delete_roles": "Delete roles", diff --git a/packages/features/ee/dsync/page/team-dsync-view.tsx b/packages/features/ee/dsync/page/team-dsync-view.tsx index c8a453ba2e1a8c..bf8202f3af2e25 100644 --- a/packages/features/ee/dsync/page/team-dsync-view.tsx +++ b/packages/features/ee/dsync/page/team-dsync-view.tsx @@ -12,7 +12,7 @@ import { showToast } from "@calcom/ui/components/toast"; import ConfigureDirectorySync from "../components/ConfigureDirectorySync"; // For Hosted Cal - Team view -const DirectorySync = () => { +const DirectorySync = ({ permissions }: { permissions?: { canEdit: boolean } }) => { const { t } = useLocale(); const router = useRouter(); @@ -36,6 +36,10 @@ const DirectorySync = () => { showToast(error.message, "error"); } + if (!permissions?.canEdit) { + router.push("/404"); + } + return (
{HOSTED_CAL_FEATURES && } diff --git a/packages/features/ee/organizations/pages/components/DisablePhoneOnlySMSNotificationsSwitch.tsx b/packages/features/ee/organizations/pages/components/DisablePhoneOnlySMSNotificationsSwitch.tsx index 9e499828e8debd..b1852f86857098 100644 --- a/packages/features/ee/organizations/pages/components/DisablePhoneOnlySMSNotificationsSwitch.tsx +++ b/packages/features/ee/organizations/pages/components/DisablePhoneOnlySMSNotificationsSwitch.tsx @@ -10,10 +10,9 @@ import { showToast } from "@calcom/ui/components/toast"; interface GeneralViewProps { currentOrg: RouterOutputs["viewer"]["organizations"]["listCurrent"]; - isAdminOrOwner: boolean; } -export const DisablePhoneOnlySMSNotificationsSwitch = ({ currentOrg, isAdminOrOwner }: GeneralViewProps) => { +export const DisablePhoneOnlySMSNotificationsSwitch = ({ currentOrg }: GeneralViewProps) => { const { t } = useLocale(); const utils = trpc.useUtils(); const [disablePhoneOnlySMSNotificationsActive, setDisablePhoneOnlySMSNotificationsActive] = useState( @@ -32,8 +31,6 @@ export const DisablePhoneOnlySMSNotificationsSwitch = ({ currentOrg, isAdminOrOw }, }); - if (!isAdminOrOwner) return null; - return ( <> { +export const LockEventTypeSwitch = ({ currentOrg }: GeneralViewProps) => { const [lockEventTypeCreationForUsers, setLockEventTypeCreationForUsers] = useState( !!currentOrg.organizationSettings.lockEventTypeCreationForUsers ); @@ -50,8 +49,6 @@ export const LockEventTypeSwitch = ({ currentOrg, isAdminOrOwner }: GeneralViewP }, }); - if (!isAdminOrOwner) return null; - const currentLockedOption = formMethods.watch("currentEventTypeOptions"); const { reset, getValues } = formMethods; @@ -69,7 +66,7 @@ export const LockEventTypeSwitch = ({ currentOrg, isAdminOrOwner }: GeneralViewP { @@ -124,9 +121,7 @@ export const LockEventTypeSwitch = ({ currentOrg, isAdminOrOwner }: GeneralViewP - +
diff --git a/packages/features/ee/organizations/pages/components/NoSlotsNotificationSwitch.tsx b/packages/features/ee/organizations/pages/components/NoSlotsNotificationSwitch.tsx index 4494251490179d..4d714409df8416 100644 --- a/packages/features/ee/organizations/pages/components/NoSlotsNotificationSwitch.tsx +++ b/packages/features/ee/organizations/pages/components/NoSlotsNotificationSwitch.tsx @@ -8,10 +8,9 @@ import { showToast } from "@calcom/ui/components/toast"; interface GeneralViewProps { currentOrg: RouterOutputs["viewer"]["organizations"]["listCurrent"]; - isAdminOrOwner: boolean; } -export const NoSlotsNotificationSwitch = ({ currentOrg, isAdminOrOwner }: GeneralViewProps) => { +export const NoSlotsNotificationSwitch = ({ currentOrg }: GeneralViewProps) => { const { t } = useLocale(); const utils = trpc.useUtils(); const [notificationActive, setNotificationActive] = useState( @@ -30,8 +29,6 @@ export const NoSlotsNotificationSwitch = ({ currentOrg, isAdminOrOwner }: Genera }, }); - if (!isAdminOrOwner) return null; - return ( <> >; + permissions: { canEdit: boolean; canDelete: boolean }; }) { const { t } = useLocale(); const [isEnabled, setIsEnabled] = useState(attribute.enabled); @@ -87,44 +89,54 @@ function AttributeItem({

- - - -
); } -function OrganizationAttributesPage() { +function OrganizationAttributesPage({ + permissions, +}: { + permissions: { canEdit: boolean; canDelete: boolean; canCreate: boolean }; +}) { const { t } = useLocale(); const { data, isLoading } = trpc.viewer.attributes.list.useQuery(); const [attributeToDelete, setAttributeToDelete] = useState(); @@ -149,17 +161,20 @@ function OrganizationAttributesPage() { ))} - + {permissions.canCreate && ( + + )} ) : (
@@ -173,13 +188,15 @@ function OrganizationAttributesPage() {

{t("add_attributes_description")}

- + {permissions.canCreate && ( + + )}
)} diff --git a/packages/features/ee/organizations/pages/settings/general.tsx b/packages/features/ee/organizations/pages/settings/general.tsx index b4862548c67395..98141bab962bdf 100644 --- a/packages/features/ee/organizations/pages/settings/general.tsx +++ b/packages/features/ee/organizations/pages/settings/general.tsx @@ -5,7 +5,6 @@ import { useRouter } from "next/navigation"; import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; -import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner"; import { TimezoneSelect } from "@calcom/features/components/timezone-select"; import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired"; import SectionBottomActions from "@calcom/features/settings/SectionBottomActions"; @@ -42,22 +41,31 @@ const SkeletonLoader = () => { interface GeneralViewProps { currentOrg: RouterOutputs["viewer"]["organizations"]["listCurrent"]; - isAdminOrOwner: boolean; localeProp: string; + + permissions: { + canRead: boolean; + canEdit: boolean; + }; } -const OrgGeneralView = () => { +const OrgGeneralView = ({ + permissions, +}: { + permissions: { + canRead: boolean; + canEdit: boolean; + }; +}) => { const { t } = useLocale(); const router = useRouter(); const session = useSession(); - const isAdminOrOwner = checkAdminOrOwner(session.data?.user?.org?.role); const { data: currentOrg, isPending, error, } = trpc.viewer.organizations.listCurrent.useQuery(undefined, {}); - const { data: user } = trpc.viewer.me.get.useQuery(); useEffect( function refactorMeWithoutEffect() { @@ -77,18 +85,22 @@ const OrgGeneralView = () => { - - - + {permissions.canEdit && ( + <> + + + + + )} ); }; -const GeneralView = ({ currentOrg, isAdminOrOwner, localeProp }: GeneralViewProps) => { +const GeneralView = ({ currentOrg, permissions, localeProp }: GeneralViewProps) => { const { t } = useLocale(); const mutation = trpc.viewer.organizations.update.useMutation({ @@ -136,7 +148,7 @@ const GeneralView = ({ currentOrg, isAdminOrOwner, localeProp }: GeneralViewProp reset, getValues, } = formMethods; - const isDisabled = isSubmitting || !isDirty || !isAdminOrOwner; + const isDisabled = isSubmitting || !isDirty || !permissions.canEdit; return (
- {isAdminOrOwner && ( + {permissions?.canEdit && (