diff --git a/.changeset/rich-actors-cross.md b/.changeset/rich-actors-cross.md new file mode 100644 index 0000000000..137f4ac986 --- /dev/null +++ b/.changeset/rich-actors-cross.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Hide members page of if user doesn't have any membership related permissions. diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembers.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembers.tsx index c25477d9db..b8f7a62f16 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembers.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembers.tsx @@ -22,12 +22,13 @@ export const OrganizationMembers = withCardStateProvider(() => { const { organizationSettings } = useEnvironment(); const card = useCardState(); const { isAuthorizedUser: canManageMemberships } = useGate({ permission: 'org:sys_memberships:manage' }); + const { isAuthorizedUser: canReadMemberships } = useGate({ permission: 'org:sys_memberships:read' }); const isDomainsEnabled = organizationSettings?.domains?.enabled; const { membershipRequests } = useCoreOrganization({ membershipRequests: isDomainsEnabled || undefined, }); - // @ts-expect-error + // @ts-expect-error This property is not typed. It is used by our dashboard in order to render a billing widget. const { __unstable_manageBillingUrl } = useOrganizationProfileContext(); if (canManageMemberships === null) { @@ -55,7 +56,9 @@ export const OrganizationMembers = withCardStateProvider(() => { - + {canReadMemberships && ( + + )} {canManageMemberships && ( { )} - - - {canManageMemberships && __unstable_manageBillingUrl && } - - - + {canReadMemberships && ( + + + {canManageMemberships && __unstable_manageBillingUrl && } + + + + )} {canManageMemberships && ( diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileNavbar.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileNavbar.tsx index 7832af2bce..a4aa8e013d 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileNavbar.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileNavbar.tsx @@ -1,5 +1,7 @@ import React from 'react'; +import { useGate } from '../../../ui/common'; +import { ORGANIZATION_PROFILE_NAVBAR_ROUTE_ID } from '../../../ui/constants'; import { useCoreOrganization, useOrganizationProfileContext } from '../../contexts'; import { Breadcrumbs, NavBar, NavbarContextProvider, OrganizationPreview } from '../../elements'; import type { PropsOfComponent } from '../../styledSystem'; @@ -10,6 +12,17 @@ export const OrganizationProfileNavbar = ( const { organization } = useCoreOrganization(); const { pages } = useOrganizationProfileContext(); + const { isAuthorizedUser: allowMembersRoute } = useGate({ + some: [ + { + permission: 'org:sys_memberships:read', + }, + { + permission: 'org:sys_memberships:manage', + }, + ], + }); + if (!organization) { return null; } @@ -24,7 +37,11 @@ export const OrganizationProfileNavbar = ( sx={t => ({ margin: `0 0 ${t.space.$4} ${t.space.$2}` })} /> } - routes={pages.routes} + routes={pages.routes.filter( + r => + r.id !== ORGANIZATION_PROFILE_NAVBAR_ROUTE_ID.MEMBERS || + (r.id === ORGANIZATION_PROFILE_NAVBAR_ROUTE_ID.MEMBERS && allowMembersRoute), + )} contentRef={props.contentRef} /> {props.children} diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx index 0af1a3cd55..7f2baece00 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx @@ -129,7 +129,12 @@ export const OrganizationProfileRoutes = (props: PropsOfComponent - + + + diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx index 85c05bf4df..1163112072 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx @@ -70,7 +70,7 @@ describe('OrganizationMembers', () => { f.withOrganizations(); f.withUser({ email_addresses: ['test@clerk.dev'], - organization_memberships: [{ name: 'Org1', permissions: [] }], + organization_memberships: [{ name: 'Org1', permissions: ['org:sys_memberships:read'] }], }); }); @@ -85,6 +85,26 @@ describe('OrganizationMembers', () => { }); }); + it('does not show members tab or navbar route if user is lacking permissions', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withOrganizations(); + f.withUser({ + email_addresses: ['test@clerk.dev'], + organization_memberships: [{ name: 'Org1', permissions: [] }], + }); + }); + + fixtures.clerk.organization?.getRoles.mockRejectedValue(null); + + const { queryByRole } = render(, { wrapper }); + + await waitFor(() => { + expect(queryByRole('tab', { name: 'Members' })).not.toBeInTheDocument(); + expect(queryByRole('tab', { name: 'Invitations' })).not.toBeInTheDocument(); + expect(queryByRole('tab', { name: 'Requests' })).not.toBeInTheDocument(); + }); + }); + it('navigates to invite screen when user clicks on Invite button', async () => { const { wrapper, fixtures } = await createFixtures(f => { f.withOrganizations(); diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationProfile.test.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationProfile.test.tsx index 3b654a948e..05a4aaa2e5 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationProfile.test.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationProfile.test.tsx @@ -53,4 +53,24 @@ describe('OrganizationProfile', () => { expect(getByText('Custom1')).toBeDefined(); expect(getByText('ExternalLink')).toBeDefined(); }); + + it('removes member nav item if user is lacking permissions', async () => { + const { wrapper } = await createFixtures(f => { + f.withOrganizations(); + f.withUser({ + email_addresses: ['test@clerk.com'], + organization_memberships: [ + { + name: 'Org1', + permissions: [], + }, + ], + }); + }); + + const { queryByText } = render(, { wrapper }); + expect(queryByText('Org1')).toBeInTheDocument(); + expect(queryByText('Members')).not.toBeInTheDocument(); + expect(queryByText('Settings')).toBeInTheDocument(); + }); });