Skip to content

Commit

Permalink
feat(fe2): Hide settings tab for logged out users (#2261)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
andrewwallacespeckle authored May 16, 2024
1 parent 7a1d4ba commit 69decaf
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 26 deletions.
5 changes: 5 additions & 0 deletions packages/frontend-2/lib/common/generated/gql/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
*/
Expand Down
8 changes: 8 additions & 0 deletions packages/frontend-2/lib/common/generated/gql/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<UserProjectsFilter>;
cursor?: InputMaybe<Scalars['String']>;
Expand Down Expand Up @@ -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<CreateWebhookMutation, CreateWebhookMutationVariables>;
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<UpdateWebhookMutation, UpdateWebhookMutationVariables>;
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<ProjectAccessCheckQuery, ProjectAccessCheckQueryVariables>;
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<ProjectRoleCheckQuery, ProjectRoleCheckQueryVariables>;
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<ProjectsDashboardQueryQuery, ProjectsDashboardQueryQueryVariables>;
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<ProjectPageQueryQuery, ProjectPageQueryQueryVariables>;
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<ProjectLatestModelsQuery, ProjectLatestModelsQueryVariables>;
Expand Down
9 changes: 9 additions & 0 deletions packages/frontend-2/lib/projects/graphql/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
33 changes: 33 additions & 0 deletions packages/frontend-2/middleware/canViewSettings.ts
Original file line number Diff line number Diff line change
@@ -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
})
30 changes: 30 additions & 0 deletions packages/frontend-2/middleware/canViewWebhooks.ts
Original file line number Diff line number Diff line change
@@ -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
})
62 changes: 36 additions & 26 deletions packages/frontend-2/pages/projects/[id]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) => {
Expand All @@ -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
}
}
Expand Down
4 changes: 4 additions & 0 deletions packages/frontend-2/pages/projects/[id]/index/settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
<template>
<ProjectPageSettingsWebhooks />
</template>
<script setup lang="ts">
definePageMeta({
middleware: ['can-view-webhooks']
})
</script>

0 comments on commit 69decaf

Please sign in to comment.