From 69decafb5d970ad3f8f781f101ac88b94216de32 Mon Sep 17 00:00:00 2001 From: andrewwallacespeckle <139135120+andrewwallacespeckle@users.noreply.github.com> Date: Thu, 16 May 2024 12:55:53 +0200 Subject: [PATCH] feat(fe2): Hide settings tab for logged out users (#2261) * Hide settings for logged out users * Hide settings tab for non-logged in users * Add middleware to settings to login * Add middleware * Update to webhooks middleware * Updates to middleware * Changes to middleware * Update comments --- .../lib/common/generated/gql/gql.ts | 5 ++ .../lib/common/generated/gql/graphql.ts | 8 +++ .../lib/projects/graphql/queries.ts | 9 +++ .../frontend-2/middleware/canViewSettings.ts | 33 ++++++++++ .../frontend-2/middleware/canViewWebhooks.ts | 30 +++++++++ .../frontend-2/pages/projects/[id]/index.vue | 62 +++++++++++-------- .../pages/projects/[id]/index/settings.vue | 4 ++ .../projects/[id]/index/settings/webhooks.vue | 5 ++ 8 files changed, 130 insertions(+), 26 deletions(-) create mode 100644 packages/frontend-2/middleware/canViewSettings.ts create mode 100644 packages/frontend-2/middleware/canViewWebhooks.ts diff --git a/packages/frontend-2/lib/common/generated/gql/gql.ts b/packages/frontend-2/lib/common/generated/gql/gql.ts index bf2914090b..85a31a3e58 100644 --- a/packages/frontend-2/lib/common/generated/gql/gql.ts +++ b/packages/frontend-2/lib/common/generated/gql/gql.ts @@ -118,6 +118,7 @@ const documents = { "\n mutation createWebhook($webhook: WebhookCreateInput!) {\n webhookCreate(webhook: $webhook)\n }\n": types.CreateWebhookDocument, "\n mutation updateWebhook($webhook: WebhookUpdateInput!) {\n webhookUpdate(webhook: $webhook)\n }\n": types.UpdateWebhookDocument, "\n query ProjectAccessCheck($id: String!) {\n project(id: $id) {\n id\n }\n }\n": types.ProjectAccessCheckDocument, + "\n query ProjectRoleCheck($id: String!) {\n project(id: $id) {\n id\n role\n }\n }\n": types.ProjectRoleCheckDocument, "\n query ProjectsDashboardQuery($filter: UserProjectsFilter, $cursor: String) {\n activeUser {\n id\n projects(filter: $filter, limit: 6, cursor: $cursor) {\n cursor\n totalCount\n items {\n ...ProjectDashboardItem\n }\n }\n ...ProjectsInviteBanners\n }\n }\n": types.ProjectsDashboardQueryDocument, "\n query ProjectPageQuery($id: String!, $token: String) {\n project(id: $id) {\n ...ProjectPageProject\n }\n projectInvite(projectId: $id, token: $token) {\n ...ProjectsInviteBanner\n }\n }\n": types.ProjectPageQueryDocument, "\n query ProjectLatestModels($projectId: String!, $filter: ProjectModelsFilter) {\n project(id: $projectId) {\n id\n models(cursor: null, limit: 16, filter: $filter) {\n totalCount\n cursor\n items {\n ...ProjectPageLatestItemsModelItem\n }\n }\n pendingImportedModels {\n ...PendingFileUpload\n }\n }\n }\n": types.ProjectLatestModelsDocument, @@ -616,6 +617,10 @@ export function graphql(source: "\n mutation updateWebhook($webhook: WebhookUpd * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "\n query ProjectAccessCheck($id: String!) {\n project(id: $id) {\n id\n }\n }\n"): (typeof documents)["\n query ProjectAccessCheck($id: String!) {\n project(id: $id) {\n id\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n query ProjectRoleCheck($id: String!) {\n project(id: $id) {\n id\n role\n }\n }\n"): (typeof documents)["\n query ProjectRoleCheck($id: String!) {\n project(id: $id) {\n id\n role\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/packages/frontend-2/lib/common/generated/gql/graphql.ts b/packages/frontend-2/lib/common/generated/gql/graphql.ts index 690a42b7ae..c6947297aa 100644 --- a/packages/frontend-2/lib/common/generated/gql/graphql.ts +++ b/packages/frontend-2/lib/common/generated/gql/graphql.ts @@ -3385,6 +3385,13 @@ export type ProjectAccessCheckQueryVariables = Exact<{ export type ProjectAccessCheckQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string } }; +export type ProjectRoleCheckQueryVariables = Exact<{ + id: Scalars['String']; +}>; + + +export type ProjectRoleCheckQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, role?: string | null } }; + export type ProjectsDashboardQueryQueryVariables = Exact<{ filter?: InputMaybe; cursor?: InputMaybe; @@ -3926,6 +3933,7 @@ export const DeleteWebhookDocument = {"kind":"Document","definitions":[{"kind":" export const CreateWebhookDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"createWebhook"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"webhook"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WebhookCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"webhookCreate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"webhook"},"value":{"kind":"Variable","name":{"kind":"Name","value":"webhook"}}}]}]}}]} as unknown as DocumentNode; export const UpdateWebhookDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"updateWebhook"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"webhook"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WebhookUpdateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"webhookUpdate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"webhook"},"value":{"kind":"Variable","name":{"kind":"Name","value":"webhook"}}}]}]}}]} as unknown as DocumentNode; export const ProjectAccessCheckDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectAccessCheck"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; +export const ProjectRoleCheckDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectRoleCheck"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]} as unknown as DocumentNode; export const ProjectsDashboardQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectsDashboardQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UserProjectsFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"projects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"6"}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectDashboardItem"}}]}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectsInviteBanners"}}]}}]}},...ProjectDashboardItemFragmentDoc.definitions,...ProjectDashboardItemNoModelsFragmentDoc.definitions,...ProjectPageModelsCardProjectFragmentDoc.definitions,...ProjectPageModelsActions_ProjectFragmentDoc.definitions,...ProjectsModelPageEmbed_ProjectFragmentDoc.definitions,...ProjectsPageTeamDialogManagePermissions_ProjectFragmentDoc.definitions,...ProjectPageLatestItemsModelItemFragmentDoc.definitions,...PendingFileUploadFragmentDoc.definitions,...ProjectPageModelsCardRenameDialogFragmentDoc.definitions,...ProjectPageModelsCardDeleteDialogFragmentDoc.definitions,...ProjectPageModelsActionsFragmentDoc.definitions,...ModelCardAutomationStatus_ModelFragmentDoc.definitions,...ModelCardAutomationStatus_AutomationsStatusFragmentDoc.definitions,...ProjectsInviteBannersFragmentDoc.definitions,...ProjectsInviteBannerFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions]} as unknown as DocumentNode; export const ProjectPageQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectPageQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageProject"}}]}},{"kind":"Field","name":{"kind":"Name","value":"projectInvite"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"projectId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectsInviteBanner"}}]}}]}},...ProjectPageProjectFragmentDoc.definitions,...ProjectPageProjectHeaderFragmentDoc.definitions,...ProjectPageTeamDialogFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions,...ProjectsPageTeamDialogManagePermissions_ProjectFragmentDoc.definitions,...ProjectsInviteBannerFragmentDoc.definitions]} as unknown as DocumentNode; export const ProjectLatestModelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectLatestModels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectModelsFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"models"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"NullValue"}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"16"}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageLatestItemsModelItem"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pendingImportedModels"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PendingFileUpload"}}]}}]}}]}},...ProjectPageLatestItemsModelItemFragmentDoc.definitions,...PendingFileUploadFragmentDoc.definitions,...ProjectPageModelsCardRenameDialogFragmentDoc.definitions,...ProjectPageModelsCardDeleteDialogFragmentDoc.definitions,...ProjectPageModelsActionsFragmentDoc.definitions,...ModelCardAutomationStatus_ModelFragmentDoc.definitions,...ModelCardAutomationStatus_AutomationsStatusFragmentDoc.definitions]} as unknown as DocumentNode; diff --git a/packages/frontend-2/lib/projects/graphql/queries.ts b/packages/frontend-2/lib/projects/graphql/queries.ts index 6269566994..57271a1687 100644 --- a/packages/frontend-2/lib/projects/graphql/queries.ts +++ b/packages/frontend-2/lib/projects/graphql/queries.ts @@ -8,6 +8,15 @@ export const projectAccessCheckQuery = graphql(` } `) +export const projectRoleCheckQuery = graphql(` + query ProjectRoleCheck($id: String!) { + project(id: $id) { + id + role + } + } +`) + export const projectsDashboardQuery = graphql(` query ProjectsDashboardQuery($filter: UserProjectsFilter, $cursor: String) { activeUser { diff --git a/packages/frontend-2/middleware/canViewSettings.ts b/packages/frontend-2/middleware/canViewSettings.ts new file mode 100644 index 0000000000..7a64042e37 --- /dev/null +++ b/packages/frontend-2/middleware/canViewSettings.ts @@ -0,0 +1,33 @@ +import { useApolloClientFromNuxt } from '~~/lib/common/composables/graphql' +import { convertThrowIntoFetchResult } from '~~/lib/common/helpers/graphql' +import { projectRoute } from '~~/lib/common/helpers/route' +import { projectRoleCheckQuery } from '~~/lib/projects/graphql/queries' + +/** + * Apply this to a page to prevent unauthenticated access to settings ensuring the user is a collaborator + */ +export default defineNuxtRouteMiddleware(async (to) => { + const client = useApolloClientFromNuxt() + + // Fetch project role data to check if the user is a collaborator + const projectId = to.params.id as string + const { data } = await client + .query({ + query: projectRoleCheckQuery, + variables: { id: projectId } + }) + .catch(convertThrowIntoFetchResult) + + if (!data?.project) { + return navigateTo(projectRoute(projectId)) + } + + // Check if the user is a collaborator of the project + const hasRole = computed(() => data.project.role) + + if (!hasRole.value) { + return navigateTo(projectRoute(projectId)) + } + + return undefined +}) diff --git a/packages/frontend-2/middleware/canViewWebhooks.ts b/packages/frontend-2/middleware/canViewWebhooks.ts new file mode 100644 index 0000000000..08e0729716 --- /dev/null +++ b/packages/frontend-2/middleware/canViewWebhooks.ts @@ -0,0 +1,30 @@ +import { Roles } from '@speckle/shared' +import { useApolloClientFromNuxt } from '~~/lib/common/composables/graphql' +import { convertThrowIntoFetchResult } from '~~/lib/common/helpers/graphql' +import { projectSettingsRoute } from '~~/lib/common/helpers/route' +import { projectRoleCheckQuery } from '~~/lib/projects/graphql/queries' + +/** + * Apply this to a page to prevent unauthenticated access to webhooks and ensure the user is the owner + */ +export default defineNuxtRouteMiddleware(async (to) => { + const client = useApolloClientFromNuxt() + + // Fetch project role data to check if the user is the owner + const projectId = to.params.id as string + const { data } = await client + .query({ + query: projectRoleCheckQuery, + variables: { id: projectId } + }) + .catch(convertThrowIntoFetchResult) + + // Check if the user is the owner of the project + const isOwner = data?.project.role === Roles.Stream.Owner + + if (!isOwner) { + return navigateTo(projectSettingsRoute(projectId)) + } + + return undefined +}) diff --git a/packages/frontend-2/pages/projects/[id]/index.vue b/packages/frontend-2/pages/projects/[id]/index.vue index 416782d4bc..dac82b06f4 100644 --- a/packages/frontend-2/pages/projects/[id]/index.vue +++ b/packages/frontend-2/pages/projects/[id]/index.vue @@ -104,6 +104,7 @@ const projectName = computed(() => ) const modelCount = computed(() => project.value?.modelCount.totalCount) const commentCount = computed(() => project.value?.commentThreadCount.totalCount) +const hasRole = computed(() => project.value?.role) useHead({ title: projectName @@ -117,38 +118,45 @@ const onInviteAccepted = async (params: { accepted: boolean }) => { } } -const pageTabItems = computed((): LayoutPageTabItem[] => [ - { - title: 'Models', - id: 'models', - count: modelCount.value, - icon: CubeIcon - }, - { - title: 'Discussions', - id: 'discussions', - count: commentCount.value, - icon: ChatBubbleLeftRightIcon - }, - // { - // title: 'Automations', - // id: 'automations', - // tag: 'New', - // icon: BoltIcon - // }, - { - title: 'Settings', - id: 'settings', - icon: Cog6ToothIcon +const pageTabItems = computed((): LayoutPageTabItem[] => { + const items: LayoutPageTabItem[] = [ + { + title: 'Models', + id: 'models', + count: modelCount.value, + icon: CubeIcon + }, + { + title: 'Discussions', + id: 'discussions', + count: commentCount.value, + icon: ChatBubbleLeftRightIcon + } + // { + // title: 'Automations', + // id: 'automations', + // tag: 'New', + // icon: BoltIcon + // }, + ] + + if (hasRole.value) { + items.push({ + title: 'Settings', + id: 'settings', + icon: Cog6ToothIcon + }) } -]) + + return items +}) const activePageTab = computed({ get: () => { const path = router.currentRoute.value.path if (/\/discussions\/?$/i.test(path)) return pageTabItems.value[1] // if (/\/automations\/?$/i.test(path)) return pageTabItems.value[2] - if (/\/settings\/?/i.test(path)) return pageTabItems.value[2] + if (/\/settings\/?/i.test(path) && hasRole.value) return pageTabItems.value[2] return pageTabItems.value[0] }, set: (val: LayoutPageTabItem) => { @@ -163,7 +171,9 @@ const activePageTab = computed({ router.push({ path: projectRoute(projectId.value, 'automations') }) break case 'settings': - router.push({ path: projectRoute(projectId.value, 'settings') }) + if (hasRole.value) { + router.push({ path: projectRoute(projectId.value, 'settings') }) + } break } } diff --git a/packages/frontend-2/pages/projects/[id]/index/settings.vue b/packages/frontend-2/pages/projects/[id]/index/settings.vue index 3510ea4fbe..9d4d801d4a 100644 --- a/packages/frontend-2/pages/projects/[id]/index/settings.vue +++ b/packages/frontend-2/pages/projects/[id]/index/settings.vue @@ -20,6 +20,10 @@ import { graphql } from '~~/lib/common/generated/gql' import type { ProjectPageProjectFragment } from '~~/lib/common/generated/gql/graphql' import { Roles } from '@speckle/shared' +definePageMeta({ + middleware: ['can-view-settings'] +}) + graphql(` fragment ProjectPageSettingsTab_Project on Project { id diff --git a/packages/frontend-2/pages/projects/[id]/index/settings/webhooks.vue b/packages/frontend-2/pages/projects/[id]/index/settings/webhooks.vue index 56b94337a0..290b4b6149 100644 --- a/packages/frontend-2/pages/projects/[id]/index/settings/webhooks.vue +++ b/packages/frontend-2/pages/projects/[id]/index/settings/webhooks.vue @@ -1,3 +1,8 @@ +