Skip to content

Commit eb96c6f

Browse files
panteliselefdesiprisg
authored andcommitted
fix(clerk-js): Hide Members page of OrgProfile if user doesn't have any member related permissions (#2138)
* fix(clerk-js): Hide Members page of OrgProfile if user doesn't have any member related permissions * fix(clerk-js): Address pr comments
1 parent f0fffce commit eb96c6f

File tree

11 files changed

+173
-34
lines changed

11 files changed

+173
-34
lines changed

.changeset/rich-actors-cross.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Hide members page of <OrganizationProfile/> if user doesn't have any membership related permissions.

packages/clerk-js/src/ui.retheme/components/OrganizationProfile/OrganizationMembers.tsx

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ export const OrganizationMembers = withCardStateProvider(() => {
2222
const { organizationSettings } = useEnvironment();
2323
const card = useCardState();
2424
const { isAuthorizedUser: canManageMemberships } = useGate({ permission: 'org:sys_memberships:manage' });
25+
const { isAuthorizedUser: canReadMemberships } = useGate({ permission: 'org:sys_memberships:read' });
2526
const isDomainsEnabled = organizationSettings?.domains?.enabled;
2627
const { membershipRequests } = useCoreOrganization({
2728
membershipRequests: isDomainsEnabled || undefined,
2829
});
2930

30-
// @ts-expect-error
31+
// @ts-expect-error This property is not typed. It is used by our dashboard in order to render a billing widget.
3132
const { __unstable_manageBillingUrl } = useOrganizationProfileContext();
3233

3334
if (canManageMemberships === null) {
@@ -55,7 +56,9 @@ export const OrganizationMembers = withCardStateProvider(() => {
5556
</Header.Root>
5657
<Tabs>
5758
<TabsList>
58-
<Tab localizationKey={localizationKeys('organizationProfile.membersPage.start.headerTitle__members')} />
59+
{canReadMemberships && (
60+
<Tab localizationKey={localizationKeys('organizationProfile.membersPage.start.headerTitle__members')} />
61+
)}
5962
{canManageMemberships && (
6063
<Tab
6164
localizationKey={localizationKeys('organizationProfile.membersPage.start.headerTitle__invitations')}
@@ -68,18 +71,20 @@ export const OrganizationMembers = withCardStateProvider(() => {
6871
)}
6972
</TabsList>
7073
<TabPanels>
71-
<TabPanel sx={{ width: '100%' }}>
72-
<Flex
73-
gap={4}
74-
direction='col'
75-
sx={{
76-
width: '100%',
77-
}}
78-
>
79-
{canManageMemberships && __unstable_manageBillingUrl && <MembershipWidget />}
80-
<ActiveMembersList />
81-
</Flex>
82-
</TabPanel>
74+
{canReadMemberships && (
75+
<TabPanel sx={{ width: '100%' }}>
76+
<Flex
77+
gap={4}
78+
direction='col'
79+
sx={{
80+
width: '100%',
81+
}}
82+
>
83+
{canManageMemberships && __unstable_manageBillingUrl && <MembershipWidget />}
84+
<ActiveMembersList />
85+
</Flex>
86+
</TabPanel>
87+
)}
8388
{canManageMemberships && (
8489
<TabPanel sx={{ width: '100%' }}>
8590
<OrganizationMembersTabInvitations />

packages/clerk-js/src/ui.retheme/components/OrganizationProfile/OrganizationProfileNavbar.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import React from 'react';
22

3+
import { useGate } from '../../common';
4+
import { ORGANIZATION_PROFILE_NAVBAR_ROUTE_ID } from '../../constants';
35
import { useCoreOrganization, useOrganizationProfileContext } from '../../contexts';
46
import { Breadcrumbs, NavBar, NavbarContextProvider, OrganizationPreview } from '../../elements';
57
import type { PropsOfComponent } from '../../styledSystem';
@@ -10,6 +12,17 @@ export const OrganizationProfileNavbar = (
1012
const { organization } = useCoreOrganization();
1113
const { pages } = useOrganizationProfileContext();
1214

15+
const { isAuthorizedUser: allowMembersRoute } = useGate({
16+
some: [
17+
{
18+
permission: 'org:sys_memberships:read',
19+
},
20+
{
21+
permission: 'org:sys_memberships:manage',
22+
},
23+
],
24+
});
25+
1326
if (!organization) {
1427
return null;
1528
}
@@ -24,7 +37,11 @@ export const OrganizationProfileNavbar = (
2437
sx={t => ({ margin: `0 0 ${t.space.$4} ${t.space.$2}` })}
2538
/>
2639
}
27-
routes={pages.routes}
40+
routes={pages.routes.filter(
41+
r =>
42+
r.id !== ORGANIZATION_PROFILE_NAVBAR_ROUTE_ID.MEMBERS ||
43+
(r.id === ORGANIZATION_PROFILE_NAVBAR_ROUTE_ID.MEMBERS && allowMembersRoute),
44+
)}
2845
contentRef={props.contentRef}
2946
/>
3047
{props.children}

packages/clerk-js/src/ui.retheme/components/OrganizationProfile/OrganizationProfileRoutes.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,12 @@ export const OrganizationProfileRoutes = (props: PropsOfComponent<typeof Profile
129129
</Gate>
130130
</Route>
131131
<Route index>
132-
<OrganizationMembers />
132+
<Gate
133+
some={[{ permission: 'org:sys_memberships:read' }, { permission: 'org:sys_memberships:manage' }]}
134+
redirectTo='./organization-settings'
135+
>
136+
<OrganizationMembers />
137+
</Gate>
133138
</Route>
134139
</Switch>
135140
</Route>

packages/clerk-js/src/ui.retheme/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ describe('OrganizationMembers', () => {
7070
f.withOrganizations();
7171
f.withUser({
7272
email_addresses: ['test@clerk.com'],
73-
organization_memberships: [{ name: 'Org1', permissions: [] }],
73+
organization_memberships: [{ name: 'Org1', permissions: ['org:sys_memberships:read'] }],
7474
});
7575
});
7676

@@ -85,6 +85,26 @@ describe('OrganizationMembers', () => {
8585
});
8686
});
8787

88+
it('does not show members tab or navbar route if user is lacking permissions', async () => {
89+
const { wrapper, fixtures } = await createFixtures(f => {
90+
f.withOrganizations();
91+
f.withUser({
92+
email_addresses: ['test@clerk.com'],
93+
organization_memberships: [{ name: 'Org1', permissions: [] }],
94+
});
95+
});
96+
97+
fixtures.clerk.organization?.getRoles.mockRejectedValue(null);
98+
99+
const { queryByRole } = render(<OrganizationMembers />, { wrapper });
100+
101+
await waitFor(() => {
102+
expect(queryByRole('tab', { name: 'Members' })).not.toBeInTheDocument();
103+
expect(queryByRole('tab', { name: 'Invitations' })).not.toBeInTheDocument();
104+
expect(queryByRole('tab', { name: 'Requests' })).not.toBeInTheDocument();
105+
});
106+
});
107+
88108
it('navigates to invite screen when user clicks on Invite button', async () => {
89109
const { wrapper, fixtures } = await createFixtures(f => {
90110
f.withOrganizations();

packages/clerk-js/src/ui.retheme/components/OrganizationProfile/__tests__/OrganizationProfile.test.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,24 @@ describe('OrganizationProfile', () => {
5353
expect(getByText('Custom1')).toBeDefined();
5454
expect(getByText('ExternalLink')).toBeDefined();
5555
});
56+
57+
it('removes member nav item if user is lacking permissions', async () => {
58+
const { wrapper } = await createFixtures(f => {
59+
f.withOrganizations();
60+
f.withUser({
61+
email_addresses: ['test@clerk.com'],
62+
organization_memberships: [
63+
{
64+
name: 'Org1',
65+
permissions: [],
66+
},
67+
],
68+
});
69+
});
70+
71+
const { queryByText } = render(<OrganizationProfile />, { wrapper });
72+
expect(queryByText('Org1')).toBeInTheDocument();
73+
expect(queryByText('Members')).not.toBeInTheDocument();
74+
expect(queryByText('Settings')).toBeInTheDocument();
75+
});
5676
});

packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembers.tsx

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ export const OrganizationMembers = withCardStateProvider(() => {
2222
const { organizationSettings } = useEnvironment();
2323
const card = useCardState();
2424
const { isAuthorizedUser: canManageMemberships } = useGate({ permission: 'org:sys_memberships:manage' });
25+
const { isAuthorizedUser: canReadMemberships } = useGate({ permission: 'org:sys_memberships:read' });
2526
const isDomainsEnabled = organizationSettings?.domains?.enabled;
2627
const { membershipRequests } = useCoreOrganization({
2728
membershipRequests: isDomainsEnabled || undefined,
2829
});
2930

30-
// @ts-expect-error
31+
// @ts-expect-error This property is not typed. It is used by our dashboard in order to render a billing widget.
3132
const { __unstable_manageBillingUrl } = useOrganizationProfileContext();
3233

3334
if (canManageMemberships === null) {
@@ -55,7 +56,9 @@ export const OrganizationMembers = withCardStateProvider(() => {
5556
</Header.Root>
5657
<Tabs>
5758
<TabsList>
58-
<Tab localizationKey={localizationKeys('organizationProfile.membersPage.start.headerTitle__members')} />
59+
{canReadMemberships && (
60+
<Tab localizationKey={localizationKeys('organizationProfile.membersPage.start.headerTitle__members')} />
61+
)}
5962
{canManageMemberships && (
6063
<Tab
6164
localizationKey={localizationKeys('organizationProfile.membersPage.start.headerTitle__invitations')}
@@ -68,18 +71,20 @@ export const OrganizationMembers = withCardStateProvider(() => {
6871
)}
6972
</TabsList>
7073
<TabPanels>
71-
<TabPanel sx={{ width: '100%' }}>
72-
<Flex
73-
gap={4}
74-
direction='col'
75-
sx={{
76-
width: '100%',
77-
}}
78-
>
79-
{canManageMemberships && __unstable_manageBillingUrl && <MembershipWidget />}
80-
<ActiveMembersList />
81-
</Flex>
82-
</TabPanel>
74+
{canReadMemberships && (
75+
<TabPanel sx={{ width: '100%' }}>
76+
<Flex
77+
gap={4}
78+
direction='col'
79+
sx={{
80+
width: '100%',
81+
}}
82+
>
83+
{canManageMemberships && __unstable_manageBillingUrl && <MembershipWidget />}
84+
<ActiveMembersList />
85+
</Flex>
86+
</TabPanel>
87+
)}
8388
{canManageMemberships && (
8489
<TabPanel sx={{ width: '100%' }}>
8590
<OrganizationMembersTabInvitations />

packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileNavbar.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import React from 'react';
22

3+
import { useGate } from '../../../ui/common';
4+
import { ORGANIZATION_PROFILE_NAVBAR_ROUTE_ID } from '../../../ui/constants';
35
import { useCoreOrganization, useOrganizationProfileContext } from '../../contexts';
46
import { Breadcrumbs, NavBar, NavbarContextProvider, OrganizationPreview } from '../../elements';
57
import type { PropsOfComponent } from '../../styledSystem';
@@ -10,6 +12,17 @@ export const OrganizationProfileNavbar = (
1012
const { organization } = useCoreOrganization();
1113
const { pages } = useOrganizationProfileContext();
1214

15+
const { isAuthorizedUser: allowMembersRoute } = useGate({
16+
some: [
17+
{
18+
permission: 'org:sys_memberships:read',
19+
},
20+
{
21+
permission: 'org:sys_memberships:manage',
22+
},
23+
],
24+
});
25+
1326
if (!organization) {
1427
return null;
1528
}
@@ -24,7 +37,11 @@ export const OrganizationProfileNavbar = (
2437
sx={t => ({ margin: `0 0 ${t.space.$4} ${t.space.$2}` })}
2538
/>
2639
}
27-
routes={pages.routes}
40+
routes={pages.routes.filter(
41+
r =>
42+
r.id !== ORGANIZATION_PROFILE_NAVBAR_ROUTE_ID.MEMBERS ||
43+
(r.id === ORGANIZATION_PROFILE_NAVBAR_ROUTE_ID.MEMBERS && allowMembersRoute),
44+
)}
2845
contentRef={props.contentRef}
2946
/>
3047
{props.children}

packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,12 @@ export const OrganizationProfileRoutes = (props: PropsOfComponent<typeof Profile
129129
</Gate>
130130
</Route>
131131
<Route index>
132-
<OrganizationMembers />
132+
<Gate
133+
some={[{ permission: 'org:sys_memberships:read' }, { permission: 'org:sys_memberships:manage' }]}
134+
redirectTo={isSettingsPageRoot ? '../' : './organization-settings'}
135+
>
136+
<OrganizationMembers />
137+
</Gate>
133138
</Route>
134139
</Switch>
135140
</Route>

packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ describe('OrganizationMembers', () => {
7070
f.withOrganizations();
7171
f.withUser({
7272
email_addresses: ['test@clerk.com'],
73-
organization_memberships: [{ name: 'Org1', permissions: [] }],
73+
organization_memberships: [{ name: 'Org1', permissions: ['org:sys_memberships:read'] }],
7474
});
7575
});
7676

@@ -85,6 +85,26 @@ describe('OrganizationMembers', () => {
8585
});
8686
});
8787

88+
it('does not show members tab or navbar route if user is lacking permissions', async () => {
89+
const { wrapper, fixtures } = await createFixtures(f => {
90+
f.withOrganizations();
91+
f.withUser({
92+
email_addresses: ['test@clerk.com'],
93+
organization_memberships: [{ name: 'Org1', permissions: [] }],
94+
});
95+
});
96+
97+
fixtures.clerk.organization?.getRoles.mockRejectedValue(null);
98+
99+
const { queryByRole } = render(<OrganizationMembers />, { wrapper });
100+
101+
await waitFor(() => {
102+
expect(queryByRole('tab', { name: 'Members' })).not.toBeInTheDocument();
103+
expect(queryByRole('tab', { name: 'Invitations' })).not.toBeInTheDocument();
104+
expect(queryByRole('tab', { name: 'Requests' })).not.toBeInTheDocument();
105+
});
106+
});
107+
88108
it('navigates to invite screen when user clicks on Invite button', async () => {
89109
const { wrapper, fixtures } = await createFixtures(f => {
90110
f.withOrganizations();

0 commit comments

Comments
 (0)