Files
speckle-server/packages/frontend-2/components/settings/workspaces/Security.vue
T
Alessandro Magionami 20bf7181b9 Activitystream IoC 2 addStreamAccessRequestDeclinedActivity (#3231)
* chore(activitystream): addStreamUpdatedActivity refactor multi region

* chore(activitystream): addStreamAccessRequestedActivity refactor multiregion

* chore(activitystream): addStreamAccessRequestDeclinedActivity refactor multiregion
2024-10-11 11:37:41 +02:00

345 lines
11 KiB
Vue

<template>
<section>
<div class="md:max-w-xl md:mx-auto pb-6 md:pb-0">
<SettingsSectionHeader
title="Security"
text="Manage verified workspace domains and associated features."
/>
<template v-if="isSsoEnabled">
<SettingsWorkspacesSecuritySso
v-if="result?.workspace"
:workspace="result.workspace"
/>
<hr class="my-6 md:my-8 border-outline-2" />
</template>
<section>
<SettingsSectionHeader title="Your domains" class="pb-4 md:pb-6" subheading />
<ul v-if="hasWorkspaceDomains">
<li
v-for="domain in workspaceDomains"
:key="domain.id"
class="border-x border-b first:border-t first:rounded-t-lg last:rounded-b-lg p-6 py-4 flex items-center"
>
<p class="text-body-xs font-medium flex-1">@{{ domain.domain }}</p>
<FormButton
:disabled="workspaceDomains.length === 1 && isDomainProtectionEnabled"
color="outline"
size="sm"
@click="openRemoveDialog(domain)"
>
Delete
</FormButton>
</li>
</ul>
<p
v-else
class="text-body-xs text-foreground-2 border border-outline-2 p-6 rounded-lg"
>
No verified domains yet
</p>
</section>
<hr class="my-6 md:my-8 border-outline-2" />
<section>
<SettingsSectionHeader title="Add new domain" subheading class="pb-4 md:pb-6" />
<div class="grid grid-cols-2 gap-x-6 items-center">
<div class="flex flex-col gap-y-1">
<p class="text-body-xs font-medium text-foreground">New domain</p>
<p class="text-body-xs text-foreground-2 leading-5">
Add a domain from a list of email domains for your active account.
</p>
</div>
<div class="flex gap-x-3">
<FormSelectBase
v-model="selectedDomain"
:items="verifiedUserDomains"
:disabled-item-predicate="disabledItemPredicate"
disabled-item-tooltip="This domain can't be used for verified workspace domains"
name="workspaceDomains"
label="Verified domains"
class="w-full"
>
<template #nothing-selected>Select domain</template>
<template #something-selected="{ value }">@{{ value }}</template>
<template #option="{ item }">
<div class="flex items-center">@{{ item }}</div>
</template>
</FormSelectBase>
<FormButton :disabled="!selectedDomain" @click="addDomain">Add</FormButton>
</div>
</div>
</section>
<hr class="my-6 md:my-8 border-outline-2" />
<section class="flex flex-col space-y-3">
<SettingsSectionHeader title="Domain features" subheading class="mb-3" />
<div class="flex flex-col space-y-8">
<div class="flex items-center">
<div class="flex-1 flex-col pr-6 gap-y-1">
<p class="text-body-xs font-medium text-foreground">Domain protection</p>
<p class="text-body-xs text-foreground-2 leading-5 max-w-md">
Admins won't be able to add users as members (or admins) to a workspace
unless the one of the users email matches one of the workspace's
verified email domains.
</p>
</div>
<FormSwitch
v-model="isDomainProtectionEnabled"
:show-label="false"
:disabled="!hasWorkspaceDomains"
name="domain-protection"
/>
</div>
<div class="flex items-center">
<div class="flex-1 flex-col pr-6 gap-y-1">
<p class="text-body-xs font-medium text-foreground">
Domain discoverability
</p>
<p class="text-body-xs text-foreground-2 leading-5 max-w-md">
Makes your workspace discoverable by users who have a verified email
address matching one of the workspace's verified domains.
</p>
</div>
<FormSwitch
v-model="isDomainDiscoverabilityEnabled"
name="domain-discoverability"
:disabled="!hasWorkspaceDomains"
:show-label="false"
/>
</div>
</div>
</section>
</div>
<SettingsWorkspacesSecurityDomainRemoveDialog
v-if="removeDialogDomain"
v-model:open="showRemoveDomainDialog"
:workspace-id="workspaceId"
:domain="removeDialogDomain"
/>
</section>
</template>
<script setup lang="ts">
import type { ShallowRef } from 'vue'
import { useApolloClient, useQuery } from '@vue/apollo-composable'
import { graphql } from '~/lib/common/generated/gql'
import type {
Workspace,
SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomainFragment
} from '~/lib/common/generated/gql/graphql'
import { SettingsUpdateWorkspaceSecurityDocument } from '~/lib/common/generated/gql/graphql'
import { getCacheId, getFirstErrorMessage } from '~/lib/common/helpers/graphql'
import { settingsWorkspacesSecurityQuery } from '~/lib/settings/graphql/queries'
import { useAddWorkspaceDomain } from '~/lib/settings/composables/management'
import { useMixpanel } from '~/lib/core/composables/mp'
import { blockedDomains } from '@speckle/shared'
import { useIsWorkspacesSsoEnabled } from '~/composables/globals'
graphql(`
fragment SettingsWorkspacesSecurity_Workspace on Workspace {
id
domains {
id
domain
...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain
}
domainBasedMembershipProtectionEnabled
discoverabilityEnabled
...SettingsWorkspacesSecuritySso_Workspace
}
fragment SettingsWorkspacesSecurity_User on User {
id
emails {
id
email
verified
}
}
`)
const props = defineProps<{
workspaceId: string
}>()
const addWorkspaceDomain = useAddWorkspaceDomain()
const { triggerNotification } = useGlobalToast()
const isSsoEnabled = useIsWorkspacesSsoEnabled()
const apollo = useApolloClient().client
const mixpanel = useMixpanel()
const selectedDomain = ref<string>()
const showRemoveDomainDialog = ref(false)
const removeDialogDomain =
ref<SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomainFragment>()
const blockedDomainItems: ShallowRef<string[]> = shallowRef(blockedDomains)
const { result } = useQuery(settingsWorkspacesSecurityQuery, {
workspaceId: props.workspaceId
})
const workspaceDomains = computed(() => {
return result.value?.workspace.domains || []
})
const hasWorkspaceDomains = computed(() => workspaceDomains.value.length > 0)
const verifiedUserDomains = computed(() => {
const workspaceDomainSet = new Set(workspaceDomains.value.map((item) => item.domain))
return [
...new Set(
(result.value?.activeUser?.emails ?? [])
.filter((email) => email.verified)
.map((email) => email.email.split('@')[1])
.filter((domain) => !workspaceDomainSet.has(domain))
)
]
})
const isDomainProtectionEnabled = computed({
get: () => result.value?.workspace.domainBasedMembershipProtectionEnabled || false,
set: async (newVal) => {
const mutationResult = await apollo
.mutate({
mutation: SettingsUpdateWorkspaceSecurityDocument,
variables: {
input: {
id: props.workspaceId,
domainBasedMembershipProtectionEnabled: newVal
}
},
optimisticResponse: {
workspaceMutations: {
update: {
__typename: 'Workspace',
id: props.workspaceId,
domainBasedMembershipProtectionEnabled: newVal,
discoverabilityEnabled:
result.value?.workspace.discoverabilityEnabled || false
}
}
},
update: (cache, res) => {
const { data } = res
if (!data?.workspaceMutations) return
cache.modify<Workspace>({
id: getCacheId('Workspace', props.workspaceId),
fields: {
domainBasedMembershipProtectionEnabled: () =>
res.data?.workspaceMutations.update
.domainBasedMembershipProtectionEnabled || false
}
})
}
})
.catch(convertThrowIntoFetchResult)
if (mutationResult?.data) {
mixpanel.track('Workspace Domain Protection Toggled', {
value: newVal,
// eslint-disable-next-line camelcase
workspace_id: props.workspaceId
})
} else {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Failed to update',
description: getFirstErrorMessage(mutationResult?.errors)
})
}
}
})
const isDomainDiscoverabilityEnabled = computed({
get: () => result.value?.workspace.discoverabilityEnabled || false,
set: async (newVal) => {
const mutationResult = await apollo.mutate({
mutation: SettingsUpdateWorkspaceSecurityDocument,
variables: {
input: {
id: props.workspaceId,
discoverabilityEnabled: newVal
}
},
optimisticResponse: {
workspaceMutations: {
update: {
__typename: 'Workspace',
id: props.workspaceId,
domainBasedMembershipProtectionEnabled:
result.value?.workspace.domainBasedMembershipProtectionEnabled || false,
discoverabilityEnabled: newVal
}
}
},
update: (cache, res) => {
const { data } = res
if (!data?.workspaceMutations) return
cache.modify<Workspace>({
id: getCacheId('Workspace', props.workspaceId),
fields: {
discoverabilityEnabled: () =>
res.data?.workspaceMutations.update.discoverabilityEnabled || false
}
})
}
})
if (mutationResult?.data) {
mixpanel.track('Workspace Discoverability Toggled', {
value: newVal,
// eslint-disable-next-line camelcase
workspace_id: props.workspaceId
})
} else {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Failed to update',
description: getFirstErrorMessage(mutationResult?.errors)
})
}
}
})
const addDomain = async () => {
if (!selectedDomain.value || !result.value?.workspace) return
await addWorkspaceDomain.mutate(
{
domain: selectedDomain.value,
workspaceId: props.workspaceId
},
result.value?.workspace.domains ?? [],
result.value?.workspace.discoverabilityEnabled,
result.value?.workspace.domainBasedMembershipProtectionEnabled
)
mixpanel.track('Workspace Domain Added', {
// eslint-disable-next-line camelcase
workspace_id: props.workspaceId
})
selectedDomain.value = undefined
}
const openRemoveDialog = (
domain: SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomainFragment
) => {
removeDialogDomain.value = domain
showRemoveDomainDialog.value = true
}
const disabledItemPredicate = (item: string) => {
return blockedDomainItems.value.includes(item)
}
watch(
() => workspaceDomains.value,
() => {
if (!hasWorkspaceDomains.value) {
isDomainDiscoverabilityEnabled.value = false
}
}
)
</script>