Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fe2): Hide settings tab for logged out users #2261

Merged
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>