refactor(fe): security page

This commit is contained in:
andrewwallacespeckle
2025-05-22 14:30:00 +02:00
parent fd061d1159
commit 15bfeea48e
2 changed files with 138 additions and 118 deletions
@@ -1,38 +1,36 @@
<template>
<section class="flex flex-col gap-3">
<SettingsSectionHeader title="Authentication" subheading class="mb-3" />
<section class="flex flex-col gap-2 border-t border-outline-2 py-8 mt-8">
<div class="flex justify-between items-start">
<div class="flex flex-col">
<h4 class="text-heading-lg">SSO</h4>
<p class="text-body-xs text-foreground-2">
Allow logins through your OpenID identity provider.
</p>
</div>
<div v-if="workspace.hasAccessToSSO">
<FormButton
v-if="isWorkspaceAdmin"
:disabled="isFormVisible || !!provider"
@click="handleConfigureClick"
>
Configure
</FormButton>
<div v-else v-tippy="`You must be a workspace admin`">
<FormButton disabled>Configure</FormButton>
</div>
</div>
<FormButton v-else :to="settingsWorkspaceRoutes.billing.route(workspace.slug)">
Upgrade to Business
</FormButton>
</div>
<div v-if="loading" class="flex justify-center">
<CommonLoadingIcon />
</div>
<template v-else>
<div class="flex items-center mb-4">
<div class="flex-1 flex-col pr-6 gap-y-1">
<p class="text-body-xs font-medium text-foreground">Enable SSO</p>
<p class="text-body-2xs text-foreground-2 leading-5 max-w-md">
Allow logins through your OpenID identity provider.
</p>
</div>
<div v-if="workspace.hasAccessToSSO">
<FormButton
v-if="isWorkspaceAdmin"
:disabled="isFormVisible || !!provider"
@click="handleConfigureClick"
>
Configure
</FormButton>
<div v-else v-tippy="`You must be a workspace admin`">
<FormButton disabled>Configure</FormButton>
</div>
</div>
<FormButton v-else :to="settingsWorkspaceRoutes.billing.route(workspace.slug)">
Upgrade to Business
</FormButton>
</div>
<CommonCard
v-if="!workspace.hasAccessToSSO && workspace.sso?.provider?.id"
class="bg-foundation"
@@ -5,81 +5,97 @@
title="Security"
text="Manage verified workspace domains and associated features."
/>
<template v-if="isSsoEnabled">
<SettingsWorkspacesSecuritySsoWrapper v-if="workspace" :workspace="workspace" />
<hr class="my-6 md:my-8 border-outline-2" />
</template>
<section>
<SettingsSectionHeader
title="Allowed email domains"
class="pb-4 md:pb-6"
subheading
/>
<CommonCard v-if="workspace?.sso?.provider?.id" class="bg-foundation mb-4">
With SSO enabled, allowed domains are configured on your identity provider's
side.
</CommonCard>
<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 border-outline-2 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"
@click="openRemoveDialog(domain)"
<div class="flex flex-col divide-y divide-outline-2">
<section>
<SettingsSectionHeader title="Workspace discoverability" subheading />
<p class="text-body-xs text-foreground-2 mt-2 mb-6">
Let users discover the workspace if they sign up with a matching email.
</p>
<CommonCard v-if="workspace?.sso?.provider?.id" class="bg-foundation mb-4">
With SSO enabled, allowed domains are configured on your identity provider's
side.
</CommonCard>
<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 border-outline-2 last:rounded-b-lg p-6 py-4 flex items-center"
>
Delete
</FormButton>
</li>
</ul>
<p class="text-body-xs font-medium flex-1">@{{ domain.domain }}</p>
<FormButton 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>
<section class="mt-8">
<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-2xs text-foreground-2 leading-5">
Add a domain from a list of email domains for your active account.
</p>
<p
v-else
class="text-body-xs text-center text-foreground-2 border border-outline-2 p-6 rounded-lg"
>
No verified domains yet
</p>
<div class="grid grid-cols-2 gap-x-6 mt-6">
<div class="flex flex-col gap-y-1">
<p class="text-body-xs font-medium text-foreground">New domain</p>
<p class="text-body-2xs 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>
<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 class="mt-6 flex flex-col gap-2">
<p class="text-body-xs font-medium text-foreground">New user policy</p>
<FormRadio
v-for="option in radioOptions"
:key="option.value"
:label="option.title"
:description="option.description"
:value="option.value"
name="measurementType"
:checked="joinPolicy === option.value"
size="sm"
@change="joinPolicy = option.value"
/>
</div>
</div>
</section>
<section class="flex flex-col space-y-3 mt-8">
<div class="flex flex-col space-y-8">
<div class="flex items-center">
</section>
<template v-if="isSsoEnabled">
<SettingsWorkspacesSecuritySsoWrapper
v-if="workspace"
:workspace="workspace"
/>
</template>
<section class="py-8">
<SettingsSectionHeader subheading title="Allowed email domains" />
<p class="text-body-xs text-foreground-2 mt-2 mb-6">
Only users with email addresses from your verified domains can be added as
workspace members or administrators.
</p>
<div class="flex">
<div class="flex-1 flex-col pr-6 gap-y-1">
<div class="flex items-center">
<p class="text-body-xs font-medium text-foreground">
Domain protection
</p>
</div>
<p class="text-body-xs font-medium text-foreground">Domain protection</p>
<p class="text-body-2xs text-foreground-2 leading-5 max-w-md">
Only users with email addresses from your verified domains can be added
as workspace members or administrators.
@@ -96,25 +112,8 @@
/>
</div>
</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-based discoverability
</p>
<p class="text-body-2xs text-foreground-2 leading-5 max-w-md">
When enabled, users with a verified email address from your verified
domain list will be able to request to join this workspace.
</p>
</div>
<FormSwitch
v-model="isDomainDiscoverabilityEnabled"
name="domain-discoverability"
:disabled="!hasWorkspaceDomains"
:show-label="false"
/>
</div>
</div>
</section>
</section>
</div>
</div>
<SettingsWorkspacesSecurityDomainRemoveDialog
@@ -141,6 +140,7 @@ import {
workspaceUpdateDiscoverabilityMutation
} from '~/lib/workspaces/graphql/mutations'
import { useVerifiedUserEmailDomains } from '~/lib/workspaces/composables/security'
import { FormRadio } from '@speckle/ui-components'
graphql(`
fragment SettingsWorkspacesSecurity_Workspace on Workspace {
@@ -172,6 +172,11 @@ useHead({
title: 'Settings | Workspace - Security'
})
enum JoinPolicy {
AdminApproval = 'admin-approval',
AutoJoin = 'auto-join'
}
const slug = computed(() => (route.params.slug as string) || '')
const { domains: userEmailDomains } = useVerifiedUserEmailDomains({
@@ -193,6 +198,7 @@ const showRemoveDomainDialog = ref(false)
const removeDialogDomain =
ref<SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomainFragment>()
const blockedDomainItems: ShallowRef<string[]> = shallowRef(blockedDomains)
const joinPolicy = ref<JoinPolicy>(JoinPolicy.AdminApproval)
const { result } = useQuery(settingsWorkspacesSecurityQuery, {
slug: slug.value
@@ -278,13 +284,15 @@ const tooltipText = computed(() => {
const addDomain = async () => {
if (!selectedDomain.value || !workspace.value) return
const isFirstDomain = !hasWorkspaceDomains.value
await addWorkspaceDomain.mutate(
{
domain: selectedDomain.value,
workspaceId: workspace.value.id
},
workspace.value.domains ?? [],
workspace.value.discoverabilityEnabled,
isFirstDomain,
workspace.value.domainBasedMembershipProtectionEnabled,
workspace.value.hasAccessToSSO,
workspace.value.hasAccessToDomainBasedSecurityPolicies
@@ -308,6 +316,20 @@ const disabledItemPredicate = (item: string) => {
return blockedDomainItems.value.includes(item)
}
const radioOptions = [
{
title: 'Admin approval required',
description: 'Users must be approved by an admin to join the workspace.',
value: JoinPolicy.AdminApproval
},
{
title: 'Auto-join',
description:
'Users with a verified email address can join the workspace without admin approval.',
value: JoinPolicy.AutoJoin
}
] as const
watch(
() => workspaceDomains.value,
() => {