Files
speckle-server/packages/frontend-2/components/settings/workspaces/security/DomainManagement.vue
T
2025-06-13 12:14:43 +02:00

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>