184 lines
6.0 KiB
Vue
184 lines
6.0 KiB
Vue
<template>
|
|
<section class="py-8">
|
|
<SettingsSectionHeader subheading title="Verified domains" />
|
|
<p class="text-body-xs text-foreground-2 mt-2 mb-6">
|
|
Connect verified domains to the workspace to enable security features below.
|
|
</p>
|
|
|
|
<div>
|
|
<div class="border border-outline-2 rounded-lg">
|
|
<ul v-if="workspaceDomains.length > 0" class="divide-y divide-outline-3">
|
|
<li
|
|
v-for="domain in workspaceDomains"
|
|
:key="domain.id"
|
|
class="px-6 py-3 flex items-center"
|
|
>
|
|
<p class="text-body-xs font-medium flex-1">@{{ domain.domain }}</p>
|
|
<div
|
|
v-tippy="!isWorkspaceAdmin ? 'You must be a workspace admin' : undefined"
|
|
>
|
|
<FormButton
|
|
:disabled="!isWorkspaceAdmin"
|
|
color="outline"
|
|
size="sm"
|
|
@click="handleRemoveDomain(domain)"
|
|
>
|
|
Delete
|
|
</FormButton>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
|
|
<p
|
|
v-else
|
|
class="text-body-2xs text-center text-foreground-2 px-6 py-6 rounded-lg"
|
|
>
|
|
No domains connected yet
|
|
</p>
|
|
<div
|
|
class="flex justify-between items-center gap-8 border-t border-outline-2 rounded-b-lg px-6 py-3"
|
|
>
|
|
<div class="flex items-center gap-1">
|
|
<p class="text-body-2xs text-foreground-2">Connect a verified domain</p>
|
|
<InformationCircleIcon
|
|
v-tippy="
|
|
'To connect a domain, you first need to verify an email address with that domain in your personal account settings. For example, if you verify example@company.com, you can then connect the company.com domain here.'
|
|
"
|
|
class="w-4 h-4 text-foreground-disabled"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex gap-1 min-w-[210px]">
|
|
<div
|
|
v-tippy="!isWorkspaceAdmin ? 'You must be a workspace admin' : undefined"
|
|
class="w-full"
|
|
>
|
|
<FormSelectBase
|
|
v-model="selectedDomain"
|
|
:items="verifiedUserDomains"
|
|
:disabled="!isWorkspaceAdmin"
|
|
: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"
|
|
size="sm"
|
|
>
|
|
<template #nothing-selected>Select domain</template>
|
|
<template #something-selected="{ value }">@{{ value }}</template>
|
|
<template #option="{ item }">
|
|
<div class="flex items-center">@{{ item }}</div>
|
|
</template>
|
|
</FormSelectBase>
|
|
</div>
|
|
<FormButton
|
|
:disabled="!selectedDomain"
|
|
size="sm"
|
|
color="outline"
|
|
@click="handleAddDomain"
|
|
>
|
|
Add
|
|
</FormButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useMutation } from '@vue/apollo-composable'
|
|
import { graphql } from '~/lib/common/generated/gql'
|
|
import type { ShallowRef } from 'vue'
|
|
import { blockedDomains, Roles } from '@speckle/shared'
|
|
import { useVerifiedUserEmailDomains } from '~/lib/workspaces/composables/security'
|
|
import { InformationCircleIcon } from '@heroicons/vue/20/solid'
|
|
import { useAddWorkspaceDomain } from '~/lib/settings/composables/management'
|
|
import { settingsDeleteWorkspaceDomainMutation } from '~/lib/settings/graphql/mutations'
|
|
import type { SettingsWorkspacesSecurityDomainManagement_WorkspaceFragment } from '~/lib/common/generated/gql/graphql'
|
|
|
|
graphql(`
|
|
fragment SettingsWorkspacesSecurityDomainManagement_Workspace on Workspace {
|
|
id
|
|
role
|
|
discoverabilityEnabled
|
|
domainBasedMembershipProtectionEnabled
|
|
hasAccessToDomainBasedSecurityPolicies: hasAccessToFeature(
|
|
featureName: domainBasedSecurityPolicies
|
|
)
|
|
hasAccessToSSO: hasAccessToFeature(featureName: oidcSso)
|
|
domains {
|
|
id
|
|
domain
|
|
}
|
|
}
|
|
`)
|
|
|
|
const props = defineProps<{
|
|
workspace: SettingsWorkspacesSecurityDomainManagement_WorkspaceFragment
|
|
}>()
|
|
|
|
const { mutate: deleteDomain } = useMutation(settingsDeleteWorkspaceDomainMutation)
|
|
const addWorkspaceDomain = useAddWorkspaceDomain()
|
|
const { domains: userEmailDomains } = useVerifiedUserEmailDomains({
|
|
filterBlocked: false
|
|
})
|
|
const { triggerNotification } = useGlobalToast()
|
|
|
|
const selectedDomain = ref<string>()
|
|
const blockedDomainItems: ShallowRef<string[]> = shallowRef(blockedDomains)
|
|
|
|
const workspaceDomains = computed(() => props.workspace?.domains || [])
|
|
const isWorkspaceAdmin = computed(() => props.workspace.role === Roles.Workspace.Admin)
|
|
|
|
const verifiedUserDomains = computed(() => {
|
|
const workspaceDomainSet = new Set(workspaceDomains.value.map((item) => item.domain))
|
|
|
|
return [
|
|
...new Set(
|
|
userEmailDomains.value.filter((domain) => !workspaceDomainSet.has(domain))
|
|
)
|
|
]
|
|
})
|
|
|
|
const disabledItemPredicate = (item: string) => {
|
|
return blockedDomainItems.value.includes(item)
|
|
}
|
|
|
|
const handleAddDomain = async () => {
|
|
if (!selectedDomain.value || !props.workspace?.id) return
|
|
|
|
await addWorkspaceDomain.mutate({
|
|
domain: selectedDomain.value,
|
|
workspaceId: props.workspace.id
|
|
})
|
|
|
|
selectedDomain.value = undefined
|
|
}
|
|
|
|
const handleRemoveDomain = async (domain: { id: string; domain: string }) => {
|
|
if (!props.workspace?.id) return
|
|
|
|
const result = await deleteDomain({
|
|
input: {
|
|
workspaceId: props.workspace.id,
|
|
id: domain.id
|
|
}
|
|
}).catch(convertThrowIntoFetchResult)
|
|
|
|
if (result?.data) {
|
|
triggerNotification({
|
|
type: ToastNotificationType.Success,
|
|
title: 'Domain removed',
|
|
description: `Successfully removed @${domain.domain} from workspace`
|
|
})
|
|
} else {
|
|
triggerNotification({
|
|
type: ToastNotificationType.Danger,
|
|
title: 'Failed to remove domain',
|
|
description: 'Please try again later'
|
|
})
|
|
}
|
|
}
|
|
</script>
|