Skip to content

Commit a8a1338

Browse files
hobbescodescoopbri
andauthored
Remove Organization Settings Access for Non-Members (#127)
* refactor(organization-settings): disable route for users that are not members of the org * refactor: remove unnecessary join organization UI and mutations * docs(organization-management): update JSDoc --------- Co-authored-by: Brian Cooper <[email protected]>
1 parent 7c12900 commit a8a1338

File tree

11 files changed

+126
-145
lines changed

11 files changed

+126
-145
lines changed

src/app/organizations/[organizationSlug]/(manage)/settings/page.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,8 @@ import {
1111
} from "generated/graphql";
1212
import { getOrganization } from "lib/actions";
1313
import { app } from "lib/config";
14-
import {
15-
enableJoinOrganizationFlag,
16-
enableOwnershipTransferFlag,
17-
} from "lib/flags";
14+
import { enableOwnershipTransferFlag } from "lib/flags";
15+
import { getSdk } from "lib/graphql";
1816
import { getQueryClient } from "lib/util";
1917

2018
export const generateMetadata = async ({ params }: Props) => {
@@ -40,11 +38,7 @@ interface Props {
4038
const OrganizationSettingsPage = async ({ params }: Props) => {
4139
const { organizationSlug } = await params;
4240

43-
const [isJoinOrganizationEnabled, isOwnershipTransferEnabled] =
44-
await Promise.all([
45-
enableJoinOrganizationFlag(),
46-
enableOwnershipTransferFlag(),
47-
]);
41+
const isOwnershipTransferEnabled = await enableOwnershipTransferFlag();
4842

4943
const session = await auth();
5044

@@ -54,6 +48,15 @@ const OrganizationSettingsPage = async ({ params }: Props) => {
5448

5549
if (!organization) notFound();
5650

51+
const sdk = getSdk({ session });
52+
53+
const { memberByUserIdAndOrganizationId } = await sdk.OrganizationRole({
54+
userId: session.user.rowId!,
55+
organizationId: organization.rowId,
56+
});
57+
58+
if (!memberByUserIdAndOrganizationId) notFound();
59+
5760
const queryClient = getQueryClient();
5861

5962
await Promise.all([
@@ -90,7 +93,6 @@ const OrganizationSettingsPage = async ({ params }: Props) => {
9093
<OrganizationSettings
9194
userId={session.user.rowId!}
9295
organizationId={organization.rowId}
93-
isJoinOrganizationEnabled={isJoinOrganizationEnabled}
9496
isOwnershipTransferEnabled={isOwnershipTransferEnabled}
9597
/>
9698
</Page>

src/app/organizations/[organizationSlug]/page.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
Role,
1616
useOrganizationMetricsQuery,
1717
useOrganizationQuery,
18+
useOrganizationRoleQuery,
1819
} from "generated/graphql";
1920
import { Grid } from "generated/panda/jsx";
2021
import { getOrganization } from "lib/actions";
@@ -107,6 +108,16 @@ const OrganizationPage = async ({ params }: Props) => {
107108
organizationId: organization.rowId,
108109
}),
109110
}),
111+
queryClient.prefetchQuery({
112+
queryKey: useOrganizationRoleQuery.getKey({
113+
organizationId: organization.rowId,
114+
userId: session.user.rowId!,
115+
}),
116+
queryFn: useOrganizationRoleQuery.fetcher({
117+
organizationId: organization.rowId,
118+
userId: session.user.rowId!,
119+
}),
120+
}),
110121
]);
111122

112123
return (
@@ -154,7 +165,11 @@ const OrganizationPage = async ({ params }: Props) => {
154165
<Grid columns={{ base: 1, md: 2 }} gap={6}>
155166
<OrganizationMetrics organizationId={organization.rowId} />
156167

157-
<OrganizationManagement hasAdminPrivileges={hasAdminPrivileges} />
168+
<OrganizationManagement
169+
user={session.user}
170+
organizationId={organization.rowId}
171+
hasAdminPrivileges={hasAdminPrivileges}
172+
/>
158173
</Grid>
159174

160175
{/* dialogs */}

src/components/organization/ManagementNavigation/ManagementNavigation.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const ManagementNavigation = ({
5353
const router = useRouter(),
5454
segment = useSelectedLayoutSegment();
5555

56-
const { isAdmin } = useOrganizationMembership({
56+
const { isMember, isAdmin } = useOrganizationMembership({
5757
userId: user?.rowId,
5858
organizationId,
5959
});
@@ -83,6 +83,7 @@ const ManagementNavigation = ({
8383
onClose?.();
8484
router.push(`/organizations/${organizationSlug}/settings`);
8585
},
86+
disabled: !isMember,
8687
},
8788
];
8889

src/components/organization/ManagementSidebar/ManagementSidebar.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from "@omnidev/sigil";
1111
import { useParams, useSelectedLayoutSegment } from "next/navigation";
1212
import { LuPanelLeftClose, LuPanelLeftOpen } from "react-icons/lu";
13-
import { useLocalStorage } from "usehooks-ts";
13+
import { useIsClient, useLocalStorage } from "usehooks-ts";
1414

1515
import { Breadcrumb } from "components/core";
1616
import { ManagementNavigation } from "components/organization";
@@ -31,6 +31,8 @@ const ManagementSidebar = ({ children }: PropsWithChildren) => {
3131
minWidth: token("breakpoints.lg"),
3232
});
3333

34+
const isClient = useIsClient();
35+
3436
const segment = useSelectedLayoutSegment();
3537

3638
const { organizationSlug } = useParams<{ organizationSlug: string }>();
@@ -80,6 +82,8 @@ const ManagementSidebar = ({ children }: PropsWithChildren) => {
8082

8183
const isOpen = isLargeViewport ? isSidebarOpen : isDrawerOpen;
8284

85+
if (!isClient) return null;
86+
8387
return (
8488
<>
8589
{/* TODO: extract ternary part into a separate component. Use early returns there, and import above to separate logic from rendering. */}

src/components/organization/OrganizationListItem/OrganizationListItem.tsx

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
"use client";
22

3-
import { Button, Flex, HStack, Icon, Stack, Text } from "@omnidev/sigil";
3+
import { HStack, Icon, Stack, Text } from "@omnidev/sigil";
44
import dayjs from "dayjs";
55
import { HiOutlineFolder, HiOutlineUserGroup } from "react-icons/hi2";
6-
import { LuSettings } from "react-icons/lu";
76

87
import { Link, OverflowText } from "components/core";
98
import { setSingularOrPlural } from "lib/util";
@@ -70,14 +69,6 @@ const OrganizationListItem = ({ organization }: Props) => {
7069
</Stack>
7170
</Link>
7271
</Stack>
73-
74-
<Flex position="absolute" right={0} top={0} m={2}>
75-
<Link href={`${`/organizations/${organization.slug}/settings`}`}>
76-
<Button variant="ghost" p={0}>
77-
<Icon src={LuSettings} w={5} h={5} color="foreground.muted" />
78-
</Button>
79-
</Link>
80-
</Flex>
8172
</HStack>
8273

8374
<HStack gap={4} mt={4} justifySelf="flex-end" flexWrap="wrap">

src/components/organization/OrganizationManagement/OrganizationManagement.tsx

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
import { Button, Grid, Icon } from "@omnidev/sigil";
44
import { useParams, useRouter } from "next/navigation";
55
import { FiUserPlus } from "react-icons/fi";
6-
import { HiOutlineUserGroup } from "react-icons/hi2";
6+
import { HiOutlineFolder, HiOutlineUserGroup } from "react-icons/hi2";
77
import { LuSettings } from "react-icons/lu";
88

99
import { SectionContainer } from "components/layout";
1010
import { app } from "lib/config";
11+
import { useOrganizationMembership } from "lib/hooks";
1112

1213
import type { ButtonProps } from "@omnidev/sigil";
14+
import type { Organization } from "generated/graphql";
15+
import type { Session } from "next-auth";
1316
import type { IconType } from "react-icons";
1417

1518
interface Action extends ButtonProps {
@@ -20,41 +23,69 @@ interface Action extends ButtonProps {
2023
}
2124

2225
interface Props {
26+
/** Authenticated user. */
27+
user: Session["user"] | undefined;
28+
/** Organization ID. */
29+
organizationId: Organization["rowId"];
2330
/** Whether the user has admin privileges for the organization. */
2431
hasAdminPrivileges: boolean;
2532
}
2633

34+
const managementDetails = app.organizationPage.management;
35+
2736
/**
2837
* Organization management.
2938
*/
30-
const OrganizationManagement = ({ hasAdminPrivileges }: Props) => {
39+
const OrganizationManagement = ({
40+
user,
41+
organizationId,
42+
hasAdminPrivileges,
43+
}: Props) => {
3144
const { organizationSlug } = useParams<{ organizationSlug: string }>();
3245
const router = useRouter();
3346

47+
const { isMember } = useOrganizationMembership({
48+
userId: user?.rowId,
49+
organizationId,
50+
});
51+
3452
const ORGANIZATION_ACTIONS: Action[] = [
3553
{
36-
label: app.organizationPage.management.cta.manageTeam.label,
54+
label: managementDetails.cta.manageTeam.label,
3755
icon: HiOutlineUserGroup,
3856
onClick: () => router.push(`/organizations/${organizationSlug}/members`),
3957
},
4058
{
41-
label: app.organizationPage.management.cta.invitations.label,
59+
label: managementDetails.cta.invitations.label,
4260
icon: FiUserPlus,
4361
onClick: () =>
4462
router.push(`/organizations/${organizationSlug}/invitations`),
4563
disabled: !hasAdminPrivileges,
4664
},
4765
{
48-
label: app.organizationPage.management.cta.settings.label,
66+
label: managementDetails.cta.settings.label,
4967
icon: LuSettings,
5068
onClick: () => router.push(`/organizations/${organizationSlug}/settings`),
69+
disabled: !isMember,
70+
},
71+
{
72+
label: app.organizationPage.header.cta.viewProjects.label,
73+
icon: HiOutlineFolder,
74+
onClick: () => router.push(`/organizations/${organizationSlug}/projects`),
75+
disabled: isMember,
5176
},
5277
];
5378

5479
return (
5580
<SectionContainer
56-
title={app.organizationPage.management.title}
57-
description={app.organizationPage.management.description}
81+
title={
82+
isMember ? managementDetails.title.member : managementDetails.title.anon
83+
}
84+
description={
85+
isMember
86+
? managementDetails.description.member
87+
: managementDetails.description.anon
88+
}
5889
>
5990
<Grid gap={4}>
6091
{ORGANIZATION_ACTIONS.filter(({ disabled }) => !disabled).map(

0 commit comments

Comments
 (0)