Poor man's SSO (#2641)
* Implemented workspace general page * Added notifications to user input * Allowed non-admins to view but not edit * Added redirect to homeroute * Fixed validation * Squashed commit of the following: commit 7bf14ab8af0f76b4c9d0aa87fc08085af7c34959 Author: Chuck Driesler <chuck@speckle.systems> Date: Tue Aug 6 19:40:50 2024 +0200 mob next [ci-skip] [ci skip] [skip ci] lastFile:packages/server/modules/workspacesCore/migrations/20240806160740_workspace_domains.ts commit 8aa3fb0cb052c10eeeb83bf9874ae0d1c065e480 Author: Alessandro Magionami <alessandro.magionami@gmail.com> Date: Tue Aug 6 18:54:15 2024 +0200 mob next [ci-skip] [ci skip] [skip ci] lastFile:packages/server/modules/core/domain/userEmails/operations.ts commit 66dfd0cf6c15a789c8f96a65a3168323e83a7b9e Author: Chuck Driesler <chuck@speckle.systems> Date: Tue Aug 6 18:30:22 2024 +0200 mob next [ci-skip] [ci skip] [skip ci] lastFile:packages/server/modules/workspacesCore/domain/types.ts Co-authored-by: Alessandro Magionami <alessandro.magionami@gmail.com> * Move General to workspaces folder * feat(workspaces): inputs on security section * feat(workspaces): add domain to workspace mutation * chore(workspaces): add blocked domains list * fix(workspaces): modals with buttons * feat(workspaceDomains): delete domain * fix(workspaces): use mutation * fix(workspaces): present user verified domains as options * Moved sidebar menu to a composable * Added coming soon tag back * feat(workspaces): create domains resolver for workspace * chore(workspaces): fix tests * chore(workspaces): fix types * chore(workspaces): fix linter * fix(workspaces): do some delete I think * chore(workspaces): add domainBasedMembershipProtectionEnabled field to workspace * chore(workspaces): improve validation for email domain * fix(workspace): query and do the thing * chore(workspaces): add graphql schema for domainBasedMembershipProtection * chore(workspaces): lint and test failures * fix(workspaces): test issues w new field * feat(workspaces): add discoverability flag * chore(workspaces): they made me do it * feat(workspaces): enable toggling domain protection * feat(workspaces): add discoverability toggle to workspace settings * feat(workspace): auto enable discoverability on first domain registration * feat(workspace): discoverability toggle fixes * fix(eventBus): fix tests * feat(workspaces): user discoverable workspaces (#2620) * feat(workspaces): it works just trust me * fix(workspaces): don't worry about it * fix(workspaces); happy path success * fix(workspaces): almost there * fix(workspaces): successful tests! * fix(workspaces): we have DISCOVERED (#2621) * Fixed linting issue * Updated query * Updated validation rules * Updated validation rules * Fix unsaved file with type export * Addressed PR comments * Updated cache * Updated item classes, add fragment back * Gergo/web 1574 join workspaces via discovery (#2623) * chore(useremails): add find verified emails by user function * chore(workspace): table helper for workspace domains * chore(workspace): get workspace with domains function * chore(workspace): test get workspace with domains function * feat(workspace): restrict workspace membership when updating workspace role * chore(workspaces): fix types * feat(workspaces): WIP join * feat(workspaces): join button makes u join * chore(useremails): fix type for find verified emails function * feat(workspaces): join * feat(workspace): prevent inviting user without email matching domain * chore(workspaces): fix linter * fix(workspaces): invoke join (gergo wrote this) * fuck * fix(workspaces): properly get discoverable workspaces * fix(workspaces): test --------- Co-authored-by: Gergő Jedlicska <gergo@jedlicska.com> Co-authored-by: Chuck Driesler <chuck@speckle.systems> * fix(workspaces): some query stuff * fix(workspaces): mutate cache instead of refetch * fix(workspaces): more adjustments to gql query and fragment structure * fix(workspaces): queries, style, structure * fix(workspaces): match discoverability with current styles * chore(workspaces): lint lint lint * fix(workspaces): got it twisted * chore(workspaces): fix test * fix(workspaces): route to joined workspace on join --------- Co-authored-by: Mike Tasset <mike.tasset@gmail.com> Co-authored-by: Chuck Driesler <chuck@speckle.systems> Co-authored-by: Alessandro Magionami <alessandro.magionami@gmail.com>
This commit is contained in:
@@ -29,6 +29,7 @@ export type ActiveUserMutations = {
|
||||
finishOnboarding: Scalars['Boolean']['output'];
|
||||
/** Edit a user's profile */
|
||||
update: User;
|
||||
workspaceMutations?: Maybe<UserWorkspaceMutations>;
|
||||
};
|
||||
|
||||
|
||||
@@ -56,6 +57,11 @@ export type ActivityCollection = {
|
||||
totalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type AddDomainToWorkspaceInput = {
|
||||
domain: Scalars['String']['input'];
|
||||
workspaceId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type AdminInviteList = {
|
||||
__typename?: 'AdminInviteList';
|
||||
cursor?: Maybe<Scalars['String']['output']>;
|
||||
@@ -843,6 +849,13 @@ export type DiscoverableStreamsSortingInput = {
|
||||
type: DiscoverableStreamsSortType;
|
||||
};
|
||||
|
||||
export type DiscoverableWorkspace = {
|
||||
__typename?: 'DiscoverableWorkspace';
|
||||
description?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
name: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type EditCommentInput = {
|
||||
commentId: Scalars['String']['input'];
|
||||
content: CommentContentInput;
|
||||
@@ -3365,6 +3378,8 @@ export type User = {
|
||||
/** Returns the apps you have created. */
|
||||
createdApps?: Maybe<Array<ServerApp>>;
|
||||
createdAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
/** Get discoverable workspaces with verified domains that match the active user's */
|
||||
discoverableWorkspaces: Array<DiscoverableWorkspace>;
|
||||
/** Only returned if API user is the user being requested or an admin */
|
||||
email?: Maybe<Scalars['String']['output']>;
|
||||
emails: Array<UserEmail>;
|
||||
@@ -4030,4 +4045,4 @@ export type AcccountTestQueryQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
export type AcccountTestQueryQuery = { __typename?: 'Query', serverInfo: { __typename?: 'ServerInfo', version?: string | null, name: string, company?: string | null } };
|
||||
|
||||
|
||||
export const AcccountTestQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AcccountTestQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"company"}}]}}]}}]} as unknown as DocumentNode<AcccountTestQueryQuery, AcccountTestQueryQueryVariables>;
|
||||
export const AcccountTestQueryDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "AcccountTestQuery" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "serverInfo" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "version" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "company" } }] } }] } }] } as unknown as DocumentNode<AcccountTestQueryQuery, AcccountTestQueryQueryVariables>;
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<FormSelectBase
|
||||
v-model="selectedValue"
|
||||
:items="domains"
|
||||
name="workspaceDomains"
|
||||
label="Verified domains"
|
||||
class="min-w-[110px]"
|
||||
size="sm"
|
||||
>
|
||||
<template #nothing-selected>Select domain</template>
|
||||
<template #something-selected="{ value }">
|
||||
<template v-if="isMultiItemArrayValue(value)">
|
||||
<div v-for="v in value" :key="v">@{{ v }}</div>
|
||||
</template>
|
||||
<template v-else>@{{ value }}</template>
|
||||
</template>
|
||||
<template #option="{ item }">
|
||||
<div class="flex items-center">@{{ item }}</div>
|
||||
</template>
|
||||
</FormSelectBase>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useFormSelectChildInternals } from '@speckle/ui-components'
|
||||
|
||||
type ItemType = string
|
||||
type ValueType = ItemType | ItemType[] | undefined
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: ValueType): void
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
domains: ItemType[]
|
||||
modelValue: ValueType
|
||||
}>()
|
||||
|
||||
const { selectedValue, isMultiItemArrayValue } = useFormSelectChildInternals<ItemType>({
|
||||
props: toRefs(props),
|
||||
emit
|
||||
})
|
||||
</script>
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<div :class="mainClasses">
|
||||
<div :class="mainInfoBlockClasses">
|
||||
<UserAvatar :user="invite.invitedBy" :size="avatarSize" />
|
||||
<UserAvatar v-if="invite.invitedBy" :user="invite.invitedBy" :size="avatarSize" />
|
||||
<WorkspaceAvatar
|
||||
v-if="invite.workspace"
|
||||
:logo="invite.workspace.logo"
|
||||
:default-logo-index="invite.workspace.defaultLogoIndex"
|
||||
/>
|
||||
<div class="text-foreground">
|
||||
<slot name="message" />
|
||||
</div>
|
||||
@@ -9,6 +14,7 @@
|
||||
<div class="flex space-x-2 w-full sm:w-auto shrink-0">
|
||||
<div v-if="isLoggedIn" class="flex items-center justify-end w-full space-x-2">
|
||||
<FormButton
|
||||
v-if="!invite.workspace"
|
||||
:size="buttonSize"
|
||||
color="subtle"
|
||||
text
|
||||
@@ -59,7 +65,12 @@ defineEmits<{
|
||||
}>()
|
||||
|
||||
type GenericInviteItem = {
|
||||
invitedBy: AvatarUserType
|
||||
invitedBy?: AvatarUserType
|
||||
workspace?: {
|
||||
id: string
|
||||
logo?: string
|
||||
defaultLogoIndex: number
|
||||
}
|
||||
user?: MaybeNullOrUndefined<{
|
||||
id: string
|
||||
}>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<Portal to="primary-actions"></Portal>
|
||||
<ProjectsDashboardHeader
|
||||
:projects-invites="projectsPanelResult?.activeUser || undefined"
|
||||
:workspaces-invites="workspacesInvitesResult?.activeUser || undefined"
|
||||
:workspaces-invites="workspacesResult?.activeUser || undefined"
|
||||
class="mb-10"
|
||||
/>
|
||||
|
||||
<div v-if="!showEmptyState" class="flex flex-col gap-4">
|
||||
@@ -64,7 +65,7 @@ import {
|
||||
} from '@vue/apollo-composable'
|
||||
import {
|
||||
projectsDashboardQuery,
|
||||
projectsDashboardWorkspaceInvitesQuery
|
||||
projectsDashboardWorkspaceQuery
|
||||
} from '~~/lib/projects/graphql/queries'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import {
|
||||
@@ -114,8 +115,8 @@ const {
|
||||
}
|
||||
}))
|
||||
|
||||
const { result: workspacesInvitesResult } = useQuery(
|
||||
projectsDashboardWorkspaceInvitesQuery,
|
||||
const { result: workspacesResult } = useQuery(
|
||||
projectsDashboardWorkspaceQuery,
|
||||
undefined,
|
||||
() => ({
|
||||
enabled: isWorkspacesEnabled.value
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
:invites="projectsInvites"
|
||||
/>
|
||||
<WorkspaceInviteBanners
|
||||
v-if="workspacesInvites?.workspaceInvites?.length"
|
||||
v-if="
|
||||
workspacesInvites?.workspaceInvites?.length ||
|
||||
workspacesInvites?.discoverableWorkspaces?.length
|
||||
"
|
||||
:invites="workspacesInvites"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
<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."
|
||||
/>
|
||||
<section>
|
||||
<SettingsSectionHeader title="Workspace domains" subheading />
|
||||
<div class="pt-4">
|
||||
<FormButton @click="showAddDomainDialog = true">Add domain</FormButton>
|
||||
<LayoutTable
|
||||
class="mt-2 md:mt-4 w-full"
|
||||
:columns="[
|
||||
{ id: 'domain', header: 'Domain', classes: 'col-span-6' },
|
||||
{ id: 'delete', header: 'Delete', classes: 'col-span-6' }
|
||||
]"
|
||||
:items="workspaceDomains"
|
||||
>
|
||||
<template #domain="{ item }">
|
||||
<span class="text-body-xs text-foreground">
|
||||
{{ `@${item.domain}` }}
|
||||
</span>
|
||||
</template>
|
||||
<template #delete="{ item }">
|
||||
<FormButton color="danger" @click="() => openRemoveDialog(item)">
|
||||
Delete
|
||||
</FormButton>
|
||||
</template>
|
||||
</LayoutTable>
|
||||
</div>
|
||||
</section>
|
||||
<hr class="my-6 md:my-10" />
|
||||
<div class="flex flex-col space-y-3">
|
||||
<SettingsSectionHeader title="Domain features" subheading class="mb-3" />
|
||||
<FormSwitch
|
||||
v-model="isDomainProtectionEnabled"
|
||||
name="domain-protection"
|
||||
label="Enable domain protection"
|
||||
/>
|
||||
<FormSwitch
|
||||
v-model="isDomainDiscoverabilityEnabled"
|
||||
name="domain-discoverability"
|
||||
label="Enable domain discoverability"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SettingsWorkspacesSecurityDomainAddDialog
|
||||
v-if="verifiedUser && workspace"
|
||||
v-model:open="showAddDomainDialog"
|
||||
:workspace="workspace"
|
||||
:verified-user="verifiedUser"
|
||||
/>
|
||||
<SettingsWorkspacesSecurityDomainRemoveDialog
|
||||
v-if="removeDialogDomain"
|
||||
v-model:open="showRemoveDomainDialog"
|
||||
:workspace-id="workspaceId"
|
||||
:domain="removeDialogDomain"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
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'
|
||||
|
||||
graphql(`
|
||||
fragment SettingsWorkspacesSecurity_Workspace on Workspace {
|
||||
id
|
||||
domains {
|
||||
...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain
|
||||
}
|
||||
domainBasedMembershipProtectionEnabled
|
||||
discoverabilityEnabled
|
||||
}
|
||||
fragment SettingsWorkspacesSecurity_User on User {
|
||||
...SettingsWorkspacesSecurityDomainAddDialog_User
|
||||
}
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
workspaceId: string
|
||||
}>()
|
||||
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
const apollo = useApolloClient().client
|
||||
|
||||
const showAddDomainDialog = ref(false)
|
||||
|
||||
const showRemoveDomainDialog = ref(false)
|
||||
const removeDialogDomain =
|
||||
ref<SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomainFragment>()
|
||||
|
||||
const { result: workspaceSecuritySettings } = useQuery(
|
||||
settingsWorkspacesSecurityQuery,
|
||||
{
|
||||
workspaceId: props.workspaceId
|
||||
}
|
||||
)
|
||||
|
||||
const workspace = computed(() => {
|
||||
return workspaceSecuritySettings.value?.workspace
|
||||
})
|
||||
const verifiedUser = computed(() => {
|
||||
return workspaceSecuritySettings.value?.activeUser
|
||||
})
|
||||
|
||||
const workspaceDomains = computed(() => {
|
||||
return workspaceSecuritySettings.value?.workspace.domains || []
|
||||
})
|
||||
|
||||
const isDomainProtectionEnabled = computed({
|
||||
get: () =>
|
||||
workspaceSecuritySettings.value?.workspace.domainBasedMembershipProtectionEnabled ||
|
||||
false,
|
||||
set: async (newVal) => {
|
||||
const result = await apollo
|
||||
.mutate({
|
||||
mutation: SettingsUpdateWorkspaceSecurityDocument,
|
||||
variables: {
|
||||
input: {
|
||||
id: props.workspaceId,
|
||||
domainBasedMembershipProtectionEnabled: newVal
|
||||
}
|
||||
},
|
||||
optimisticResponse: {
|
||||
workspaceMutations: {
|
||||
update: {
|
||||
__typename: 'Workspace',
|
||||
id: props.workspaceId,
|
||||
domainBasedMembershipProtectionEnabled: newVal,
|
||||
discoverabilityEnabled:
|
||||
workspaceSecuritySettings.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 (!result?.data) {
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Failed to update',
|
||||
description: getFirstErrorMessage(result?.errors)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const isDomainDiscoverabilityEnabled = computed({
|
||||
get: () => workspaceSecuritySettings.value?.workspace.discoverabilityEnabled || false,
|
||||
set: async (newVal) => {
|
||||
const result = await apollo.mutate({
|
||||
mutation: SettingsUpdateWorkspaceSecurityDocument,
|
||||
variables: {
|
||||
input: {
|
||||
id: props.workspaceId,
|
||||
discoverabilityEnabled: newVal
|
||||
}
|
||||
},
|
||||
optimisticResponse: {
|
||||
workspaceMutations: {
|
||||
update: {
|
||||
__typename: 'Workspace',
|
||||
id: props.workspaceId,
|
||||
domainBasedMembershipProtectionEnabled:
|
||||
workspaceSecuritySettings.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 (!result?.data) {
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Failed to update',
|
||||
description: getFirstErrorMessage(result?.errors)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const openRemoveDialog = (
|
||||
domain: SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomainFragment
|
||||
) => {
|
||||
removeDialogDomain.value = domain
|
||||
showRemoveDomainDialog.value = true
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<LayoutDialog
|
||||
v-model:open="isOpen"
|
||||
title="Add domain"
|
||||
max-width="sm"
|
||||
:buttons="dialogButtons"
|
||||
>
|
||||
<div class="h-24">
|
||||
<FormSelectWorkspaceDomains
|
||||
:domains="verifiedUserDomains"
|
||||
:model-value="selectedDomain"
|
||||
@update:model-value="onSelectedDomainUpdate"
|
||||
/>
|
||||
</div>
|
||||
</LayoutDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useApolloClient } from '@vue/apollo-composable'
|
||||
import type { LayoutDialogButton } from '@speckle/ui-components'
|
||||
import { settingsAddWorkspaceDomainMutation } from '~/lib/settings/graphql/mutations'
|
||||
import { getCacheId, getFirstErrorMessage } from '~/lib/common/helpers/graphql'
|
||||
import type {
|
||||
SettingsWorkspacesSecurityDomainAddDialog_UserFragment,
|
||||
SettingsWorkspacesSecurityDomainAddDialog_WorkspaceFragment,
|
||||
Workspace
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import { isString } from 'lodash-es'
|
||||
|
||||
graphql(`
|
||||
fragment SettingsWorkspacesSecurityDomainAddDialog_Workspace on Workspace {
|
||||
id
|
||||
domains {
|
||||
id
|
||||
domain
|
||||
}
|
||||
discoverabilityEnabled
|
||||
}
|
||||
fragment SettingsWorkspacesSecurityDomainAddDialog_User on User {
|
||||
id
|
||||
emails {
|
||||
id
|
||||
email
|
||||
verified
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
workspace: SettingsWorkspacesSecurityDomainAddDialog_WorkspaceFragment
|
||||
verifiedUser: SettingsWorkspacesSecurityDomainAddDialog_UserFragment
|
||||
}>()
|
||||
|
||||
const { workspace } = toRefs(props)
|
||||
|
||||
const isOpen = defineModel<boolean>('open', { required: true })
|
||||
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
|
||||
const apollo = useApolloClient().client
|
||||
|
||||
const selectedDomain = ref<string>('')
|
||||
const onSelectedDomainUpdate = (e?: string | string[]) => {
|
||||
if (!isString(e)) {
|
||||
return
|
||||
}
|
||||
selectedDomain.value = e
|
||||
}
|
||||
|
||||
const verifiedUserDomains = computed(() => [
|
||||
...new Set(
|
||||
(props.verifiedUser.emails ?? [])
|
||||
.filter((email) => email.verified)
|
||||
.map((email) => email.email.split('@')[1])
|
||||
)
|
||||
])
|
||||
|
||||
const onAdd = async () => {
|
||||
const result = await apollo
|
||||
.mutate({
|
||||
mutation: settingsAddWorkspaceDomainMutation,
|
||||
variables: {
|
||||
input: {
|
||||
domain: selectedDomain.value,
|
||||
workspaceId: workspace.value.id
|
||||
}
|
||||
},
|
||||
optimisticResponse: {
|
||||
workspaceMutations: {
|
||||
addDomain: {
|
||||
__typename: 'Workspace',
|
||||
id: workspace.value.id,
|
||||
domains: [
|
||||
...workspace.value.domains,
|
||||
{
|
||||
__typename: 'WorkspaceDomain',
|
||||
id: '',
|
||||
domain: selectedDomain.value
|
||||
}
|
||||
],
|
||||
discoverabilityEnabled:
|
||||
workspace.value.domains.length === 0
|
||||
? true
|
||||
: workspace.value.discoverabilityEnabled
|
||||
}
|
||||
}
|
||||
},
|
||||
update: (cache, res) => {
|
||||
const { data } = res
|
||||
if (!data?.workspaceMutations) return
|
||||
|
||||
cache.modify<Workspace>({
|
||||
id: getCacheId('Workspace', props.workspace.id),
|
||||
fields: {
|
||||
discoverabilityEnabled() {
|
||||
return data?.workspaceMutations.addDomain.discoverabilityEnabled || false
|
||||
},
|
||||
domains() {
|
||||
return [...(data?.workspaceMutations.addDomain.domains || [])]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(convertThrowIntoFetchResult)
|
||||
|
||||
if (result?.data) {
|
||||
isOpen.value = false
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Success,
|
||||
title: 'Domain added',
|
||||
description: `The verified domain ${selectedDomain.value} has been added to your workspace`
|
||||
})
|
||||
} else {
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Failed to add verified domain',
|
||||
description: getFirstErrorMessage(result?.errors)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const dialogButtons = computed((): LayoutDialogButton[] => [
|
||||
{
|
||||
text: 'Cancel',
|
||||
props: { color: 'outline', fullWidth: true },
|
||||
onClick: () => {
|
||||
isOpen.value = false
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Add',
|
||||
props: {
|
||||
fullWidth: true,
|
||||
color: 'primary'
|
||||
},
|
||||
onClick: onAdd
|
||||
}
|
||||
])
|
||||
</script>
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<LayoutDialog
|
||||
v-model:open="isOpen"
|
||||
title="Remove domain"
|
||||
max-width="sm"
|
||||
:buttons="dialogButtons"
|
||||
>
|
||||
<p class="text-body-xs text-foreground">
|
||||
Are you sure you want to remove
|
||||
<span class="font-semibold">@{{ domain.domain }}</span>
|
||||
from your workspace's verified domains?
|
||||
</p>
|
||||
</LayoutDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { LayoutDialogButton } from '@speckle/ui-components'
|
||||
import { useApolloClient } from '@vue/apollo-composable'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import {
|
||||
type Workspace,
|
||||
type SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomainFragment
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import { getCacheId, getFirstErrorMessage } from '~/lib/common/helpers/graphql'
|
||||
import { settingsDeleteWorkspaceDomainMutation } from '~/lib/settings/graphql/mutations'
|
||||
|
||||
graphql(`
|
||||
fragment SettingsWorkspacesSecurityDomainRemoveDialog_Workspace on Workspace {
|
||||
id
|
||||
domains {
|
||||
...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain
|
||||
}
|
||||
}
|
||||
fragment SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain on WorkspaceDomain {
|
||||
id
|
||||
domain
|
||||
}
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
workspaceId: string
|
||||
domain: SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomainFragment
|
||||
}>()
|
||||
|
||||
const apollo = useApolloClient().client
|
||||
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
|
||||
const isOpen = defineModel<boolean>('open', { required: true })
|
||||
|
||||
const handleRemove = async () => {
|
||||
const result = await apollo
|
||||
.mutate({
|
||||
mutation: settingsDeleteWorkspaceDomainMutation,
|
||||
variables: {
|
||||
input: {
|
||||
workspaceId: props.workspaceId,
|
||||
id: props.domain.id
|
||||
}
|
||||
},
|
||||
update: (cache, res) => {
|
||||
const { data } = res
|
||||
if (!data?.workspaceMutations) return
|
||||
|
||||
cache.modify<Workspace>({
|
||||
id: getCacheId('Workspace', props.workspaceId),
|
||||
fields: {
|
||||
domains(currentDomains, { isReference }) {
|
||||
return [...currentDomains].filter((domain) =>
|
||||
isReference(domain) ? false : domain.id !== props.domain.id
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(convertThrowIntoFetchResult)
|
||||
|
||||
if (result?.data) {
|
||||
isOpen.value = false
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Success,
|
||||
title: 'Domain removed',
|
||||
description: `Removed domain successfully`
|
||||
})
|
||||
} else {
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Failed to remove domain',
|
||||
description: getFirstErrorMessage(result?.errors)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const dialogButtons = computed((): LayoutDialogButton[] => [
|
||||
{
|
||||
text: 'Cancel',
|
||||
props: { color: 'outline', fullWidth: true },
|
||||
onClick: () => {
|
||||
isOpen.value = false
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Remove',
|
||||
props: {
|
||||
fullWidth: true,
|
||||
color: 'danger'
|
||||
},
|
||||
onClick: handleRemove
|
||||
}
|
||||
])
|
||||
</script>
|
||||
@@ -1,6 +1,15 @@
|
||||
<template>
|
||||
<div class="flex flex-col divide-y divide-outline-3">
|
||||
<WorkspaceInviteBanner v-for="item in items" :key="item.id" :invite="item" />
|
||||
<WorkspaceInviteBanner
|
||||
v-for="invite in invites"
|
||||
:key="invite.id"
|
||||
:invite="invite"
|
||||
/>
|
||||
<WorkspaceInviteDiscoverableWorkspaceBanner
|
||||
v-for="workspace in discoverableWorkspaces"
|
||||
:key="workspace.id"
|
||||
:workspace="workspace"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
@@ -13,6 +22,9 @@ import type { WorkspaceInviteBanners_UserFragment } from '~~/lib/common/generate
|
||||
|
||||
graphql(`
|
||||
fragment WorkspaceInviteBanners_User on User {
|
||||
discoverableWorkspaces {
|
||||
...WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace
|
||||
}
|
||||
workspaceInvites {
|
||||
...WorkspaceInviteBanner_PendingWorkspaceCollaborator
|
||||
}
|
||||
@@ -23,5 +35,8 @@ const props = defineProps<{
|
||||
invites: WorkspaceInviteBanners_UserFragment
|
||||
}>()
|
||||
|
||||
const items = computed(() => props.invites.workspaceInvites || [])
|
||||
const invites = computed(() => props.invites.workspaceInvites || [])
|
||||
const discoverableWorkspaces = computed(
|
||||
() => props.invites.discoverableWorkspaces || []
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<InviteBanner :invite="invite" @processed="processJoin">
|
||||
<template #message>
|
||||
You may join the workspace
|
||||
<span class="font-medium">{{ workspace.name }}</span>
|
||||
because it is part of your organization
|
||||
</template>
|
||||
</InviteBanner>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useApolloClient } from '@vue/apollo-composable'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import {
|
||||
DashboardJoinWorkspaceDocument,
|
||||
type WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspaceFragment
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import { getCacheId, getFirstErrorMessage } from '~/lib/common/helpers/graphql'
|
||||
|
||||
graphql(`
|
||||
fragment WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace on DiscoverableWorkspace {
|
||||
id
|
||||
name
|
||||
description
|
||||
logo
|
||||
defaultLogoIndex
|
||||
}
|
||||
fragment WorkspaceInviteDiscoverableWorkspaceBanner_Workspace on Workspace {
|
||||
id
|
||||
name
|
||||
description
|
||||
createdAt
|
||||
updatedAt
|
||||
logo
|
||||
defaultLogoIndex
|
||||
domainBasedMembershipProtectionEnabled
|
||||
discoverabilityEnabled
|
||||
}
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
workspace: WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspaceFragment
|
||||
}>()
|
||||
|
||||
const { client: apollo } = useApolloClient()
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
const router = useRouter()
|
||||
|
||||
const invite = computed(() => ({
|
||||
workspace: {
|
||||
id: props.workspace.id,
|
||||
logo: props.workspace.logo || undefined,
|
||||
defaultLogoIndex: props.workspace.defaultLogoIndex
|
||||
}
|
||||
}))
|
||||
|
||||
const processJoin = async (accept: boolean) => {
|
||||
if (!accept) {
|
||||
// TODO: Use cookies to enable dismissing the discoverable workspace invite
|
||||
return
|
||||
}
|
||||
|
||||
const result = await apollo
|
||||
.mutate({
|
||||
mutation: DashboardJoinWorkspaceDocument,
|
||||
variables: {
|
||||
input: {
|
||||
workspaceId: props.workspace.id
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(convertThrowIntoFetchResult)
|
||||
|
||||
if (result?.data) {
|
||||
apollo.cache.evict({
|
||||
id: getCacheId('DiscoverableWorkspace', props.workspace.id)
|
||||
})
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Success,
|
||||
title: 'Joined workspace',
|
||||
description: 'Successfully joined workspace'
|
||||
})
|
||||
router.push(`/workspaces/${props.workspace.id}`)
|
||||
} else {
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Failed to join workspace',
|
||||
description: getFirstErrorMessage(result?.errors)
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -105,6 +105,7 @@ const documents = {
|
||||
"\n fragment SettingsWorkspacesGeneralEditAvatar_Workspace on Workspace {\n id\n logo\n name\n }\n": types.SettingsWorkspacesGeneralEditAvatar_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesMembers_Workspace on Workspace {\n id\n role\n }\n": types.SettingsWorkspacesMembers_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": types.SettingsWorkspacesProjects_ProjectCollectionFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n domains {\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n fragment SettingsWorkspacesSecurity_User on User {\n ...SettingsWorkspacesSecurityDomainAddDialog_User\n }\n": types.SettingsWorkspacesSecurity_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n user {\n id\n avatar\n name\n company\n verified\n }\n }\n": types.SettingsWorkspacesMembersGuestsTable_WorkspaceCollaboratorFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesMembersGuestsTable_Workspace on Workspace {\n id\n ...SettingsWorkspacesMembersTableHeader_Workspace\n team {\n id\n ...SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator\n }\n }\n": types.SettingsWorkspacesMembersGuestsTable_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n inviteId\n role\n title\n updatedAt\n user {\n id\n ...LimitedUserAvatar\n }\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n }\n": types.SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaboratorFragmentDoc,
|
||||
@@ -112,6 +113,8 @@ const documents = {
|
||||
"\n fragment SettingsWorkspacesMembersMembersTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n user {\n id\n avatar\n name\n company\n verified\n }\n }\n": types.SettingsWorkspacesMembersMembersTable_WorkspaceCollaboratorFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesMembersMembersTable_Workspace on Workspace {\n id\n ...SettingsWorkspacesMembersTableHeader_Workspace\n team {\n id\n ...SettingsWorkspacesMembersMembersTable_WorkspaceCollaborator\n }\n }\n": types.SettingsWorkspacesMembersMembersTable_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesMembersTableHeader_Workspace on Workspace {\n id\n role\n ...WorkspaceInviteDialog_Workspace\n }\n": types.SettingsWorkspacesMembersTableHeader_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecurityDomainAddDialog_Workspace on Workspace {\n id\n domains {\n id\n domain\n }\n discoverabilityEnabled\n }\n fragment SettingsWorkspacesSecurityDomainAddDialog_User on User {\n id\n emails {\n id\n email\n verified\n }\n }\n": types.SettingsWorkspacesSecurityDomainAddDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecurityDomainRemoveDialog_Workspace on Workspace {\n id\n domains {\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n }\n fragment SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain on WorkspaceDomain {\n id\n domain\n }\n": types.SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment ModelPageProject on Project {\n id\n createdAt\n name\n visibility\n }\n": types.ModelPageProjectFragmentDoc,
|
||||
"\n fragment ThreadCommentAttachment on Comment {\n text {\n attachments {\n id\n fileName\n fileType\n fileSize\n }\n }\n }\n": types.ThreadCommentAttachmentFragmentDoc,
|
||||
"\n fragment ViewerCommentsListItem on Comment {\n id\n rawText\n archived\n author {\n ...LimitedUserAvatar\n }\n createdAt\n viewedAt\n replies {\n totalCount\n cursor\n items {\n ...ViewerCommentsReplyItem\n }\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n resources {\n resourceId\n resourceType\n }\n }\n": types.ViewerCommentsListItemFragmentDoc,
|
||||
@@ -121,8 +124,9 @@ const documents = {
|
||||
"\n fragment WorkspaceProjectList_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...ProjectDashboardItem\n }\n cursor\n }\n": types.WorkspaceProjectList_ProjectCollectionFragmentDoc,
|
||||
"\n fragment WorkspaceHeader_Workspace on Workspace {\n ...WorkspaceAvatar_Workspace\n id\n role\n name\n logo\n description\n totalProjects: projects {\n totalCount\n }\n team {\n id\n user {\n id\n name\n ...LimitedUserAvatar\n }\n }\n ...WorkspaceInviteDialog_Workspace\n }\n": types.WorkspaceHeader_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceInviteBanner_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspaceId\n workspaceName\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n": types.WorkspaceInviteBanner_PendingWorkspaceCollaboratorFragmentDoc,
|
||||
"\n fragment WorkspaceInviteBanners_User on User {\n workspaceInvites {\n ...WorkspaceInviteBanner_PendingWorkspaceCollaborator\n }\n }\n": types.WorkspaceInviteBanners_UserFragmentDoc,
|
||||
"\n fragment WorkspaceInviteBanners_User on User {\n discoverableWorkspaces {\n ...WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace\n }\n workspaceInvites {\n ...WorkspaceInviteBanner_PendingWorkspaceCollaborator\n }\n }\n": types.WorkspaceInviteBanners_UserFragmentDoc,
|
||||
"\n fragment WorkspaceInviteBlock_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n workspaceId\n workspaceName\n token\n user {\n id\n name\n ...LimitedUserAvatar\n }\n title\n email\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n": types.WorkspaceInviteBlock_PendingWorkspaceCollaboratorFragmentDoc,
|
||||
"\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace on DiscoverableWorkspace {\n id\n name\n description\n logo\n defaultLogoIndex\n }\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_Workspace on Workspace {\n id\n name\n description\n createdAt\n updatedAt\n logo\n defaultLogoIndex\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n": types.WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspaceFragmentDoc,
|
||||
"\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n company\n bio\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n notificationPreferences\n versions(limit: 0) {\n totalCount\n }\n }\n }\n": types.ActiveUserMainMetadataDocument,
|
||||
"\n mutation CreateOnboardingProject {\n projectMutations {\n createForOnboarding {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n ": types.CreateOnboardingProjectDocument,
|
||||
"\n mutation FinishOnboarding {\n activeUserMutations {\n finishOnboarding\n }\n }\n": types.FinishOnboardingDocument,
|
||||
@@ -147,8 +151,9 @@ const documents = {
|
||||
"\n query ServerInfoAllScopes {\n serverInfo {\n scopes {\n name\n description\n }\n }\n }\n": types.ServerInfoAllScopesDocument,
|
||||
"\n query ProjectModelsSelectorValues($projectId: String!, $cursor: String) {\n project(id: $projectId) {\n id\n models(limit: 100, cursor: $cursor) {\n cursor\n totalCount\n items {\n ...CommonModelSelectorModel\n }\n }\n }\n }\n": types.ProjectModelsSelectorValuesDocument,
|
||||
"\n query MainServerInfoData {\n serverInfo {\n adminContact\n blobSizeLimitBytes\n canonicalUrl\n company\n description\n guestModeEnabled\n inviteOnly\n name\n termsOfService\n version\n automateUrl\n }\n }\n": types.MainServerInfoDataDocument,
|
||||
"\n mutation DashboardJoinWorkspace($input: JoinWorkspaceInput!) {\n workspaceMutations {\n join(input: $input) {\n ...WorkspaceInviteDiscoverableWorkspaceBanner_Workspace\n }\n }\n }\n": types.DashboardJoinWorkspaceDocument,
|
||||
"\n query DashboardProjectsPageQuery {\n activeUser {\n id\n projects(limit: 3) {\n items {\n ...DashboardProjectCard_Project\n }\n }\n ...ProjectsDashboardHeaderProjects_User\n }\n }\n": types.DashboardProjectsPageQueryDocument,
|
||||
"\n query DashboardProjectsPageWorkspaceInvitesQuery {\n activeUser {\n id\n ...ProjectsDashboardHeaderWorkspaces_User\n }\n }\n": types.DashboardProjectsPageWorkspaceInvitesQueryDocument,
|
||||
"\n query DashboardProjectsPageWorkspaceQuery {\n activeUser {\n id\n ...ProjectsDashboardHeaderWorkspaces_User\n }\n }\n": types.DashboardProjectsPageWorkspaceQueryDocument,
|
||||
"\n mutation DeleteAccessToken($token: String!) {\n apiTokenRevoke(token: $token)\n }\n": types.DeleteAccessTokenDocument,
|
||||
"\n mutation CreateAccessToken($token: ApiTokenCreateInput!) {\n apiTokenCreate(token: $token)\n }\n": types.CreateAccessTokenDocument,
|
||||
"\n mutation DeleteApplication($appId: String!) {\n appDelete(appId: $appId)\n }\n": types.DeleteApplicationDocument,
|
||||
@@ -199,8 +204,8 @@ const documents = {
|
||||
"\n mutation CreateTestAutomation(\n $projectId: ID!\n $input: ProjectTestAutomationCreateInput!\n ) {\n projectMutations {\n automationMutations(projectId: $projectId) {\n createTestAutomation(input: $input) {\n id\n ...ProjectPageAutomationsRow_Automation\n }\n }\n }\n }\n": types.CreateTestAutomationDocument,
|
||||
"\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 ...ProjectsDashboardHeaderProjects_User\n }\n }\n": types.ProjectsDashboardQueryDocument,
|
||||
"\n query ProjectsDashboardWorkspaceInvitesQuery {\n activeUser {\n id\n ...ProjectsDashboardHeaderWorkspaces_User\n }\n }\n": types.ProjectsDashboardWorkspaceInvitesQueryDocument,
|
||||
"\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 ...ProjectsDashboardHeaderProjects_User\n }\n }\n": types.ProjectsDashboardQueryDocument,
|
||||
"\n query ProjectsDashboardWorkspaceQuery {\n activeUser {\n id\n ...ProjectsDashboardHeaderWorkspaces_User\n }\n }\n": types.ProjectsDashboardWorkspaceQueryDocument,
|
||||
"\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,
|
||||
"\n query ProjectLatestModelsPagination(\n $projectId: String!\n $filter: ProjectModelsFilter\n $cursor: String = null\n ) {\n project(id: $projectId) {\n id\n models(cursor: $cursor, limit: 16, filter: $filter) {\n totalCount\n cursor\n items {\n ...ProjectPageLatestItemsModelItem\n }\n }\n }\n }\n": types.ProjectLatestModelsPaginationDocument,
|
||||
@@ -246,9 +251,12 @@ const documents = {
|
||||
"\n mutation SettingsDeleteUserEmail($input: DeleteUserEmailInput!) {\n activeUserMutations {\n emailMutations {\n delete(input: $input) {\n ...SettingsUserEmails_User\n }\n }\n }\n }\n": types.SettingsDeleteUserEmailDocument,
|
||||
"\n mutation SettingsSetPrimaryUserEmail($input: SetPrimaryUserEmailInput!) {\n activeUserMutations {\n emailMutations {\n setPrimary(input: $input) {\n ...SettingsUserEmails_User\n }\n }\n }\n }\n": types.SettingsSetPrimaryUserEmailDocument,
|
||||
"\n mutation SettingsNewEmailVerification($input: EmailVerificationRequestInput!) {\n activeUserMutations {\n emailMutations {\n requestNewEmailVerification(input: $input)\n }\n }\n }\n": types.SettingsNewEmailVerificationDocument,
|
||||
"\n mutation SettingsUpdateWorkspaceSecurity($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n }\n }\n": types.SettingsUpdateWorkspaceSecurityDocument,
|
||||
"\n mutation SettingsDeleteWorkspace($workspaceId: String!) {\n workspaceMutations {\n delete(workspaceId: $workspaceId)\n }\n }\n": types.SettingsDeleteWorkspaceDocument,
|
||||
"\n mutation SettingsResendWorkspaceInvite($input: WorkspaceInviteResendInput!) {\n workspaceMutations {\n invites {\n resend(input: $input)\n }\n }\n }\n": types.SettingsResendWorkspaceInviteDocument,
|
||||
"\n mutation SettingsCancelWorkspaceInvite($workspaceId: String!, $inviteId: String!) {\n workspaceMutations {\n invites {\n cancel(workspaceId: $workspaceId, inviteId: $inviteId) {\n id\n }\n }\n }\n }\n": types.SettingsCancelWorkspaceInviteDocument,
|
||||
"\n mutation AddWorkspaceDomain($input: AddDomainToWorkspaceInput!) {\n workspaceMutations {\n addDomain(input: $input) {\n ...SettingsWorkspacesSecurityDomainAddDialog_Workspace\n }\n }\n }\n": types.AddWorkspaceDomainDocument,
|
||||
"\n mutation DeleteWorkspaceDomain($input: WorkspaceDomainDeleteInput!) {\n workspaceMutations {\n deleteDomain(input: $input) {\n ...SettingsWorkspacesSecurityDomainRemoveDialog_Workspace\n }\n }\n }\n": types.DeleteWorkspaceDomainDocument,
|
||||
"\n mutation SettingsLeaveWorkspace($leaveId: ID!) {\n workspaceMutations {\n leave(id: $leaveId)\n }\n }\n": types.SettingsLeaveWorkspaceDocument,
|
||||
"\n query SettingsSidebar {\n activeUser {\n ...SettingsDialog_User\n }\n }\n": types.SettingsSidebarDocument,
|
||||
"\n query SettingsWorkspaceGeneral($id: String!) {\n workspace(id: $id) {\n ...SettingsWorkspacesGeneral_Workspace\n }\n }\n": types.SettingsWorkspaceGeneralDocument,
|
||||
@@ -256,6 +264,7 @@ const documents = {
|
||||
"\n query SettingsWorkspacesInvitesSearch(\n $workspaceId: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspace(id: $workspaceId) {\n ...SettingsWorkspacesMembersInvitesTable_Workspace\n }\n }\n": types.SettingsWorkspacesInvitesSearchDocument,
|
||||
"\n query SettingsUserEmailsQuery {\n activeUser {\n ...SettingsUserEmails_User\n }\n }\n": types.SettingsUserEmailsQueryDocument,
|
||||
"\n query SettingsWorkspacesProjects(\n $workspaceId: String!\n $limit: Int!\n $cursor: String\n $filter: WorkspaceProjectsFilter\n ) {\n workspace(id: $workspaceId) {\n id\n projects(limit: $limit, cursor: $cursor, filter: $filter) {\n cursor\n ...SettingsWorkspacesProjects_ProjectCollection\n }\n }\n }\n": types.SettingsWorkspacesProjectsDocument,
|
||||
"\n query SettingsWorkspaceSecurity($workspaceId: String!) {\n workspace(id: $workspaceId) {\n ...SettingsWorkspacesSecurity_Workspace\n }\n activeUser {\n ...SettingsWorkspacesSecurity_User\n }\n }\n": types.SettingsWorkspaceSecurityDocument,
|
||||
"\n fragment AppAuthorAvatar on AppAuthor {\n id\n name\n avatar\n }\n": types.AppAuthorAvatarFragmentDoc,
|
||||
"\n fragment LimitedUserAvatar on LimitedUser {\n id\n name\n avatar\n }\n": types.LimitedUserAvatarFragmentDoc,
|
||||
"\n fragment ActiveUserAvatar on User {\n id\n name\n avatar\n }\n": types.ActiveUserAvatarFragmentDoc,
|
||||
@@ -685,6 +694,10 @@ export function graphql(source: "\n fragment SettingsWorkspacesMembers_Workspac
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment SettingsWorkspacesProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\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 fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n domains {\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n fragment SettingsWorkspacesSecurity_User on User {\n ...SettingsWorkspacesSecurityDomainAddDialog_User\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n domains {\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n fragment SettingsWorkspacesSecurity_User on User {\n ...SettingsWorkspacesSecurityDomainAddDialog_User\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -713,6 +726,14 @@ export function graphql(source: "\n fragment SettingsWorkspacesMembersMembersTa
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment SettingsWorkspacesMembersTableHeader_Workspace on Workspace {\n id\n role\n ...WorkspaceInviteDialog_Workspace\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesMembersTableHeader_Workspace on Workspace {\n id\n role\n ...WorkspaceInviteDialog_Workspace\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 fragment SettingsWorkspacesSecurityDomainAddDialog_Workspace on Workspace {\n id\n domains {\n id\n domain\n }\n discoverabilityEnabled\n }\n fragment SettingsWorkspacesSecurityDomainAddDialog_User on User {\n id\n emails {\n id\n email\n verified\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesSecurityDomainAddDialog_Workspace on Workspace {\n id\n domains {\n id\n domain\n }\n discoverabilityEnabled\n }\n fragment SettingsWorkspacesSecurityDomainAddDialog_User on User {\n id\n emails {\n id\n email\n verified\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 fragment SettingsWorkspacesSecurityDomainRemoveDialog_Workspace on Workspace {\n id\n domains {\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n }\n fragment SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain on WorkspaceDomain {\n id\n domain\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesSecurityDomainRemoveDialog_Workspace on Workspace {\n id\n domains {\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n }\n fragment SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain on WorkspaceDomain {\n id\n domain\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -752,11 +773,15 @@ export function graphql(source: "\n fragment WorkspaceInviteBanner_PendingWorks
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment WorkspaceInviteBanners_User on User {\n workspaceInvites {\n ...WorkspaceInviteBanner_PendingWorkspaceCollaborator\n }\n }\n"): (typeof documents)["\n fragment WorkspaceInviteBanners_User on User {\n workspaceInvites {\n ...WorkspaceInviteBanner_PendingWorkspaceCollaborator\n }\n }\n"];
|
||||
export function graphql(source: "\n fragment WorkspaceInviteBanners_User on User {\n discoverableWorkspaces {\n ...WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace\n }\n workspaceInvites {\n ...WorkspaceInviteBanner_PendingWorkspaceCollaborator\n }\n }\n"): (typeof documents)["\n fragment WorkspaceInviteBanners_User on User {\n discoverableWorkspaces {\n ...WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace\n }\n workspaceInvites {\n ...WorkspaceInviteBanner_PendingWorkspaceCollaborator\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 fragment WorkspaceInviteBlock_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n workspaceId\n workspaceName\n token\n user {\n id\n name\n ...LimitedUserAvatar\n }\n title\n email\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n"): (typeof documents)["\n fragment WorkspaceInviteBlock_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n workspaceId\n workspaceName\n token\n user {\n id\n name\n ...LimitedUserAvatar\n }\n title\n email\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\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 fragment WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace on DiscoverableWorkspace {\n id\n name\n description\n logo\n defaultLogoIndex\n }\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_Workspace on Workspace {\n id\n name\n description\n createdAt\n updatedAt\n logo\n defaultLogoIndex\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n"): (typeof documents)["\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace on DiscoverableWorkspace {\n id\n name\n description\n logo\n defaultLogoIndex\n }\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_Workspace on Workspace {\n id\n name\n description\n createdAt\n updatedAt\n logo\n defaultLogoIndex\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -853,6 +878,10 @@ export function graphql(source: "\n query ProjectModelsSelectorValues($projectI
|
||||
* 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 MainServerInfoData {\n serverInfo {\n adminContact\n blobSizeLimitBytes\n canonicalUrl\n company\n description\n guestModeEnabled\n inviteOnly\n name\n termsOfService\n version\n automateUrl\n }\n }\n"): (typeof documents)["\n query MainServerInfoData {\n serverInfo {\n adminContact\n blobSizeLimitBytes\n canonicalUrl\n company\n description\n guestModeEnabled\n inviteOnly\n name\n termsOfService\n version\n automateUrl\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 mutation DashboardJoinWorkspace($input: JoinWorkspaceInput!) {\n workspaceMutations {\n join(input: $input) {\n ...WorkspaceInviteDiscoverableWorkspaceBanner_Workspace\n }\n }\n }\n"): (typeof documents)["\n mutation DashboardJoinWorkspace($input: JoinWorkspaceInput!) {\n workspaceMutations {\n join(input: $input) {\n ...WorkspaceInviteDiscoverableWorkspaceBanner_Workspace\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -860,7 +889,7 @@ export function graphql(source: "\n query DashboardProjectsPageQuery {\n act
|
||||
/**
|
||||
* 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 DashboardProjectsPageWorkspaceInvitesQuery {\n activeUser {\n id\n ...ProjectsDashboardHeaderWorkspaces_User\n }\n }\n"): (typeof documents)["\n query DashboardProjectsPageWorkspaceInvitesQuery {\n activeUser {\n id\n ...ProjectsDashboardHeaderWorkspaces_User\n }\n }\n"];
|
||||
export function graphql(source: "\n query DashboardProjectsPageWorkspaceQuery {\n activeUser {\n id\n ...ProjectsDashboardHeaderWorkspaces_User\n }\n }\n"): (typeof documents)["\n query DashboardProjectsPageWorkspaceQuery {\n activeUser {\n id\n ...ProjectsDashboardHeaderWorkspaces_User\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -1064,11 +1093,11 @@ export function graphql(source: "\n query ProjectRoleCheck($id: String!) {\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 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 ...ProjectsDashboardHeaderProjects_User\n }\n }\n"): (typeof documents)["\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 ...ProjectsDashboardHeaderProjects_User\n }\n }\n"];
|
||||
export function graphql(source: "\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 ...ProjectsDashboardHeaderProjects_User\n }\n }\n"): (typeof documents)["\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 ...ProjectsDashboardHeaderProjects_User\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 ProjectsDashboardWorkspaceInvitesQuery {\n activeUser {\n id\n ...ProjectsDashboardHeaderWorkspaces_User\n }\n }\n"): (typeof documents)["\n query ProjectsDashboardWorkspaceInvitesQuery {\n activeUser {\n id\n ...ProjectsDashboardHeaderWorkspaces_User\n }\n }\n"];
|
||||
export function graphql(source: "\n query ProjectsDashboardWorkspaceQuery {\n activeUser {\n id\n ...ProjectsDashboardHeaderWorkspaces_User\n }\n }\n"): (typeof documents)["\n query ProjectsDashboardWorkspaceQuery {\n activeUser {\n id\n ...ProjectsDashboardHeaderWorkspaces_User\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -1249,6 +1278,10 @@ export function graphql(source: "\n mutation SettingsSetPrimaryUserEmail($input
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation SettingsNewEmailVerification($input: EmailVerificationRequestInput!) {\n activeUserMutations {\n emailMutations {\n requestNewEmailVerification(input: $input)\n }\n }\n }\n"): (typeof documents)["\n mutation SettingsNewEmailVerification($input: EmailVerificationRequestInput!) {\n activeUserMutations {\n emailMutations {\n requestNewEmailVerification(input: $input)\n }\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 mutation SettingsUpdateWorkspaceSecurity($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n }\n }\n"): (typeof documents)["\n mutation SettingsUpdateWorkspaceSecurity($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -1261,6 +1294,14 @@ export function graphql(source: "\n mutation SettingsResendWorkspaceInvite($inp
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation SettingsCancelWorkspaceInvite($workspaceId: String!, $inviteId: String!) {\n workspaceMutations {\n invites {\n cancel(workspaceId: $workspaceId, inviteId: $inviteId) {\n id\n }\n }\n }\n }\n"): (typeof documents)["\n mutation SettingsCancelWorkspaceInvite($workspaceId: String!, $inviteId: String!) {\n workspaceMutations {\n invites {\n cancel(workspaceId: $workspaceId, inviteId: $inviteId) {\n id\n }\n }\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 mutation AddWorkspaceDomain($input: AddDomainToWorkspaceInput!) {\n workspaceMutations {\n addDomain(input: $input) {\n ...SettingsWorkspacesSecurityDomainAddDialog_Workspace\n }\n }\n }\n"): (typeof documents)["\n mutation AddWorkspaceDomain($input: AddDomainToWorkspaceInput!) {\n workspaceMutations {\n addDomain(input: $input) {\n ...SettingsWorkspacesSecurityDomainAddDialog_Workspace\n }\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 mutation DeleteWorkspaceDomain($input: WorkspaceDomainDeleteInput!) {\n workspaceMutations {\n deleteDomain(input: $input) {\n ...SettingsWorkspacesSecurityDomainRemoveDialog_Workspace\n }\n }\n }\n"): (typeof documents)["\n mutation DeleteWorkspaceDomain($input: WorkspaceDomainDeleteInput!) {\n workspaceMutations {\n deleteDomain(input: $input) {\n ...SettingsWorkspacesSecurityDomainRemoveDialog_Workspace\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -1289,6 +1330,10 @@ export function graphql(source: "\n query SettingsUserEmailsQuery {\n active
|
||||
* 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 SettingsWorkspacesProjects(\n $workspaceId: String!\n $limit: Int!\n $cursor: String\n $filter: WorkspaceProjectsFilter\n ) {\n workspace(id: $workspaceId) {\n id\n projects(limit: $limit, cursor: $cursor, filter: $filter) {\n cursor\n ...SettingsWorkspacesProjects_ProjectCollection\n }\n }\n }\n"): (typeof documents)["\n query SettingsWorkspacesProjects(\n $workspaceId: String!\n $limit: Int!\n $cursor: String\n $filter: WorkspaceProjectsFilter\n ) {\n workspace(id: $workspaceId) {\n id\n projects(limit: $limit, cursor: $cursor, filter: $filter) {\n cursor\n ...SettingsWorkspacesProjects_ProjectCollection\n }\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 SettingsWorkspaceSecurity($workspaceId: String!) {\n workspace(id: $workspaceId) {\n ...SettingsWorkspacesSecurity_Workspace\n }\n activeUser {\n ...SettingsWorkspacesSecurity_User\n }\n }\n"): (typeof documents)["\n query SettingsWorkspaceSecurity($workspaceId: String!) {\n workspace(id: $workspaceId) {\n ...SettingsWorkspacesSecurity_Workspace\n }\n activeUser {\n ...SettingsWorkspacesSecurity_User\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
|
||||
export const dashboardJoinWorkspaceMutation = graphql(`
|
||||
mutation DashboardJoinWorkspace($input: JoinWorkspaceInput!) {
|
||||
workspaceMutations {
|
||||
join(input: $input) {
|
||||
...WorkspaceInviteDiscoverableWorkspaceBanner_Workspace
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
@@ -14,8 +14,8 @@ export const dashboardProjectsPageQuery = graphql(`
|
||||
}
|
||||
`)
|
||||
|
||||
export const dashboardProjectsPageWorkspaceInvitesQuery = graphql(`
|
||||
query DashboardProjectsPageWorkspaceInvitesQuery {
|
||||
export const dashboardProjectsPageWorkspacesQuery = graphql(`
|
||||
query DashboardProjectsPageWorkspaceQuery {
|
||||
activeUser {
|
||||
id
|
||||
...ProjectsDashboardHeaderWorkspaces_User
|
||||
|
||||
@@ -28,13 +28,14 @@ export const projectsDashboardQuery = graphql(`
|
||||
...ProjectDashboardItem
|
||||
}
|
||||
}
|
||||
...ProjectsInviteBanners
|
||||
...ProjectsDashboardHeaderProjects_User
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const projectsDashboardWorkspaceInvitesQuery = graphql(`
|
||||
query ProjectsDashboardWorkspaceInvitesQuery {
|
||||
export const projectsDashboardWorkspaceQuery = graphql(`
|
||||
query ProjectsDashboardWorkspaceQuery {
|
||||
activeUser {
|
||||
id
|
||||
...ProjectsDashboardHeaderWorkspaces_User
|
||||
|
||||
@@ -10,6 +10,7 @@ import SettingsServerActiveUsers from '~/components/settings/server/ActiveUsers.
|
||||
import SettingsServerPendingInvitations from '~/components/settings/server/PendingInvitations.vue'
|
||||
import SettingsWorkspaceGeneral from '~/components/settings/workspaces/General.vue'
|
||||
import SettingsWorkspacesMembers from '~/components/settings/workspaces/Members.vue'
|
||||
import SettingsWorkspacesSecurity from '~/components/settings/workspaces/Security.vue'
|
||||
import SettingsWorkspacesProjects from '~/components/settings/workspaces/Projects.vue'
|
||||
import { useIsMultipleEmailsEnabled } from '~/composables/globals'
|
||||
|
||||
@@ -27,16 +28,15 @@ export const useSettingsMenu = () => {
|
||||
title: 'Projects',
|
||||
component: SettingsWorkspacesProjects
|
||||
},
|
||||
security: {
|
||||
title: 'Security',
|
||||
component: SettingsWorkspacesSecurity
|
||||
},
|
||||
billing: {
|
||||
title: 'Billing',
|
||||
disabled: true,
|
||||
tooltipText: 'Manage billing for your workspace'
|
||||
},
|
||||
security: {
|
||||
title: 'Security',
|
||||
disabled: true,
|
||||
tooltipText: 'SSO, manage permissions, restrict domain access'
|
||||
},
|
||||
regions: {
|
||||
title: 'Regions',
|
||||
disabled: true,
|
||||
|
||||
@@ -56,6 +56,18 @@ export const settingsNewEmailVerificationMutation = graphql(`
|
||||
}
|
||||
`)
|
||||
|
||||
export const settingsUpdateWorkspaceSecurity = graphql(`
|
||||
mutation SettingsUpdateWorkspaceSecurity($input: WorkspaceUpdateInput!) {
|
||||
workspaceMutations {
|
||||
update(input: $input) {
|
||||
id
|
||||
domainBasedMembershipProtectionEnabled
|
||||
discoverabilityEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const deleteWorkspaceMutation = graphql(`
|
||||
mutation SettingsDeleteWorkspace($workspaceId: String!) {
|
||||
workspaceMutations {
|
||||
@@ -86,6 +98,25 @@ export const settingsCancelWorkspaceInviteMutation = graphql(`
|
||||
}
|
||||
`)
|
||||
|
||||
export const settingsAddWorkspaceDomainMutation = graphql(`
|
||||
mutation AddWorkspaceDomain($input: AddDomainToWorkspaceInput!) {
|
||||
workspaceMutations {
|
||||
addDomain(input: $input) {
|
||||
...SettingsWorkspacesSecurityDomainAddDialog_Workspace
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const settingsDeleteWorkspaceDomainMutation = graphql(`
|
||||
mutation DeleteWorkspaceDomain($input: WorkspaceDomainDeleteInput!) {
|
||||
workspaceMutations {
|
||||
deleteDomain(input: $input) {
|
||||
...SettingsWorkspacesSecurityDomainRemoveDialog_Workspace
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
export const settingsLeaveWorkspaceMutation = graphql(`
|
||||
mutation SettingsLeaveWorkspace($leaveId: ID!) {
|
||||
workspaceMutations {
|
||||
|
||||
@@ -65,3 +65,14 @@ export const settingsWorkspacesProjectsQuery = graphql(`
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const settingsWorkspacesSecurityQuery = graphql(`
|
||||
query SettingsWorkspaceSecurity($workspaceId: String!) {
|
||||
workspace(id: $workspaceId) {
|
||||
...SettingsWorkspacesSecurity_Workspace
|
||||
}
|
||||
activeUser {
|
||||
...SettingsWorkspacesSecurity_User
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</Portal>
|
||||
<ProjectsDashboardHeader
|
||||
:projects-invites="projectsResult?.activeUser || undefined"
|
||||
:workspaces-invites="workspaceInvitesResult?.activeUser || undefined"
|
||||
:workspaces-invites="workspacesResult?.activeUser || undefined"
|
||||
/>
|
||||
<div class="flex flex-col gap-y-12">
|
||||
<div class="flex flex-col-reverse lg:flex-col gap-y-12">
|
||||
@@ -56,7 +56,7 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
dashboardProjectsPageQuery,
|
||||
dashboardProjectsPageWorkspaceInvitesQuery
|
||||
dashboardProjectsPageWorkspacesQuery
|
||||
} from '~~/lib/dashboard/graphql/queries'
|
||||
import type { QuickStartItem } from '~~/lib/dashboard/helpers/types'
|
||||
import { getResizedGhostImage } from '~~/lib/dashboard/helpers/utils'
|
||||
@@ -80,8 +80,8 @@ const config = useRuntimeConfig()
|
||||
const mixpanel = useMixpanel()
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
const { result: projectsResult } = useQuery(dashboardProjectsPageQuery)
|
||||
const { result: workspaceInvitesResult } = useQuery(
|
||||
dashboardProjectsPageWorkspaceInvitesQuery,
|
||||
const { result: workspacesResult } = useQuery(
|
||||
dashboardProjectsPageWorkspacesQuery,
|
||||
undefined,
|
||||
() => ({
|
||||
enabled: isWorkspacesEnabled.value
|
||||
|
||||
@@ -28,6 +28,8 @@ input WorkspaceUpdateInput {
|
||||
"""
|
||||
logo: String
|
||||
defaultLogoIndex: Int
|
||||
domainBasedMembershipProtectionEnabled: Boolean
|
||||
discoverabilityEnabled: Boolean
|
||||
}
|
||||
|
||||
input WorkspaceRoleUpdateInput {
|
||||
@@ -67,6 +69,15 @@ input WorkspaceProjectInviteCreateInput {
|
||||
workspaceRole: String
|
||||
}
|
||||
|
||||
input AddDomainToWorkspaceInput {
|
||||
domain: String!
|
||||
workspaceId: ID!
|
||||
}
|
||||
|
||||
input JoinWorkspaceInput {
|
||||
workspaceId: ID!
|
||||
}
|
||||
|
||||
extend type ProjectInviteMutations {
|
||||
"""
|
||||
Create invite(-s) for a project in a workspace. Unlike the base create() mutation, this allows
|
||||
@@ -97,6 +108,20 @@ type WorkspaceMutations {
|
||||
@hasServerRole(role: SERVER_USER)
|
||||
leave(id: ID!): Boolean! @hasServerRole(role: SERVER_GUEST)
|
||||
invites: WorkspaceInviteMutations!
|
||||
# TODO: this mutation should have an hasWorkspaceRole directive to authorize only workspace admin
|
||||
# We are, for the moment, doing the check in the resolver
|
||||
addDomain(input: AddDomainToWorkspaceInput!): Workspace!
|
||||
@hasScope(scope: "workspace:update")
|
||||
# TODO: this mutation should have an hasWorkspaceRole directive to authorize only workspace admin
|
||||
# We are, for the moment, doing the check in the resolver
|
||||
deleteDomain(input: WorkspaceDomainDeleteInput!): Workspace!
|
||||
@hasScope(scope: "workspace:update")
|
||||
join(input: JoinWorkspaceInput!): Workspace! @hasScope(scope: "workspace:update")
|
||||
}
|
||||
|
||||
input WorkspaceDomainDeleteInput {
|
||||
workspaceId: ID!
|
||||
id: ID!
|
||||
}
|
||||
|
||||
input WorkspaceInviteCreateInput {
|
||||
@@ -168,6 +193,10 @@ type Workspace {
|
||||
"""
|
||||
defaultLogoIndex: Int!
|
||||
"""
|
||||
Verified workspace domains
|
||||
"""
|
||||
domains: [WorkspaceDomain!]! @hasWorkspaceRole(role: ADMIN)
|
||||
"""
|
||||
Active user's role for this workspace. `null` if request is not authenticated, or the workspace is not explicitly shared with you.
|
||||
"""
|
||||
role: String
|
||||
@@ -183,6 +212,27 @@ type Workspace {
|
||||
cursor: String
|
||||
filter: WorkspaceProjectsFilter
|
||||
): ProjectCollection!
|
||||
"""
|
||||
Enable/Disable restriction to invite users to workspace as Guests only
|
||||
"""
|
||||
domainBasedMembershipProtectionEnabled: Boolean!
|
||||
"""
|
||||
Enable/Disable discovery of the workspace
|
||||
"""
|
||||
discoverabilityEnabled: Boolean!
|
||||
}
|
||||
|
||||
type DiscoverableWorkspace {
|
||||
id: ID!
|
||||
name: String!
|
||||
description: String
|
||||
logo: String
|
||||
defaultLogoIndex: Int!
|
||||
}
|
||||
|
||||
type WorkspaceDomain {
|
||||
id: ID!
|
||||
domain: String!
|
||||
}
|
||||
|
||||
input WorkspaceProjectsFilter {
|
||||
@@ -247,6 +297,11 @@ type WorkspaceCollection {
|
||||
}
|
||||
|
||||
extend type User {
|
||||
"""
|
||||
Get discoverable workspaces with verified domains that match the active user's
|
||||
"""
|
||||
discoverableWorkspaces: [DiscoverableWorkspace!]!
|
||||
|
||||
"""
|
||||
Get the workspaces for the user
|
||||
"""
|
||||
|
||||
@@ -64,6 +64,15 @@ export type FindEmailsByUserId = ({
|
||||
userId
|
||||
}: Pick<Partial<UserEmail>, 'userId'>) => Promise<UserEmail[]>
|
||||
|
||||
export type FindVerifiedEmailsByUserId = ({
|
||||
userId
|
||||
}: Pick<UserEmail, 'userId'>) => Promise<UserEmail[]>
|
||||
|
||||
export type FindVerifiedEmailByUserIdAndDomain = ({
|
||||
userId,
|
||||
domain
|
||||
}: Pick<UserEmail, 'userId'> & { domain: string }) => Promise<UserEmail | null>
|
||||
|
||||
export type CountEmailsByUserId = ({
|
||||
userId
|
||||
}: Pick<UserEmail, 'userId'>) => Promise<number>
|
||||
|
||||
@@ -65,6 +65,11 @@ export type ActivityCollection = {
|
||||
totalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type AddDomainToWorkspaceInput = {
|
||||
domain: Scalars['String']['input'];
|
||||
workspaceId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type AdminInviteList = {
|
||||
__typename?: 'AdminInviteList';
|
||||
cursor?: Maybe<Scalars['String']['output']>;
|
||||
@@ -857,6 +862,15 @@ export type DiscoverableStreamsSortingInput = {
|
||||
type: DiscoverableStreamsSortType;
|
||||
};
|
||||
|
||||
export type DiscoverableWorkspace = {
|
||||
__typename?: 'DiscoverableWorkspace';
|
||||
defaultLogoIndex: Scalars['Int']['output'];
|
||||
description?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
logo?: Maybe<Scalars['String']['output']>;
|
||||
name: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type EditCommentInput = {
|
||||
commentId: Scalars['String']['input'];
|
||||
content: CommentContentInput;
|
||||
@@ -930,6 +944,10 @@ export type GendoAiRenderInput = {
|
||||
versionId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type JoinWorkspaceInput = {
|
||||
workspaceId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type LegacyCommentViewerData = {
|
||||
__typename?: 'LegacyCommentViewerData';
|
||||
/**
|
||||
@@ -3379,6 +3397,8 @@ export type User = {
|
||||
/** Returns the apps you have created. */
|
||||
createdApps?: Maybe<Array<ServerApp>>;
|
||||
createdAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
/** Get discoverable workspaces with verified domains that match the active user's */
|
||||
discoverableWorkspaces: Array<DiscoverableWorkspace>;
|
||||
/** Only returned if API user is the user being requested or an admin */
|
||||
email?: Maybe<Scalars['String']['output']>;
|
||||
emails: Array<UserEmail>;
|
||||
@@ -3838,6 +3858,12 @@ export type Workspace = {
|
||||
/** Selected fallback when `logo` not set */
|
||||
defaultLogoIndex: Scalars['Int']['output'];
|
||||
description?: Maybe<Scalars['String']['output']>;
|
||||
/** Enable/Disable discovery of the workspace */
|
||||
discoverabilityEnabled: Scalars['Boolean']['output'];
|
||||
/** Enable/Disable restriction to invite users to workspace as Guests only */
|
||||
domainBasedMembershipProtectionEnabled: Scalars['Boolean']['output'];
|
||||
/** Verified workspace domains */
|
||||
domains: Array<WorkspaceDomain>;
|
||||
id: Scalars['ID']['output'];
|
||||
/** Only available to workspace owners */
|
||||
invitedTeam?: Maybe<Array<PendingWorkspaceCollaborator>>;
|
||||
@@ -3888,6 +3914,17 @@ export type WorkspaceCreateInput = {
|
||||
name: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type WorkspaceDomain = {
|
||||
__typename?: 'WorkspaceDomain';
|
||||
domain: Scalars['String']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
};
|
||||
|
||||
export type WorkspaceDomainDeleteInput = {
|
||||
id: Scalars['ID']['input'];
|
||||
workspaceId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type WorkspaceInviteCreateInput = {
|
||||
/** Either this or userId must be filled */
|
||||
email?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -3953,15 +3990,23 @@ export type WorkspaceInviteUseInput = {
|
||||
|
||||
export type WorkspaceMutations = {
|
||||
__typename?: 'WorkspaceMutations';
|
||||
addDomain: Workspace;
|
||||
create: Workspace;
|
||||
delete: Scalars['Boolean']['output'];
|
||||
deleteDomain: Workspace;
|
||||
invites: WorkspaceInviteMutations;
|
||||
join: Workspace;
|
||||
leave: Scalars['Boolean']['output'];
|
||||
update: Workspace;
|
||||
updateRole: Workspace;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceMutationsAddDomainArgs = {
|
||||
input: AddDomainToWorkspaceInput;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceMutationsCreateArgs = {
|
||||
input: WorkspaceCreateInput;
|
||||
};
|
||||
@@ -3972,6 +4017,16 @@ export type WorkspaceMutationsDeleteArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceMutationsDeleteDomainArgs = {
|
||||
input: WorkspaceDomainDeleteInput;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceMutationsJoinArgs = {
|
||||
input: JoinWorkspaceInput;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceMutationsLeaveArgs = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
@@ -4032,6 +4087,8 @@ export type WorkspaceTeamFilter = {
|
||||
export type WorkspaceUpdateInput = {
|
||||
defaultLogoIndex?: InputMaybe<Scalars['Int']['input']>;
|
||||
description?: InputMaybe<Scalars['String']['input']>;
|
||||
discoverabilityEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
domainBasedMembershipProtectionEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
id: Scalars['String']['input'];
|
||||
/** Logo image as base64-encoded string */
|
||||
logo?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -4112,6 +4169,7 @@ export type ResolversTypes = {
|
||||
ActiveUserMutations: ResolverTypeWrapper<MutationsObjectGraphQLReturn>;
|
||||
Activity: ResolverTypeWrapper<Activity>;
|
||||
ActivityCollection: ResolverTypeWrapper<ActivityCollection>;
|
||||
AddDomainToWorkspaceInput: AddDomainToWorkspaceInput;
|
||||
AdminInviteList: ResolverTypeWrapper<Omit<AdminInviteList, 'items'> & { items: Array<ResolversTypes['ServerInvite']> }>;
|
||||
AdminQueries: ResolverTypeWrapper<GraphQLEmptyReturn>;
|
||||
AdminUserList: ResolverTypeWrapper<AdminUserList>;
|
||||
@@ -4191,6 +4249,7 @@ export type ResolversTypes = {
|
||||
DeleteVersionsInput: DeleteVersionsInput;
|
||||
DiscoverableStreamsSortType: DiscoverableStreamsSortType;
|
||||
DiscoverableStreamsSortingInput: DiscoverableStreamsSortingInput;
|
||||
DiscoverableWorkspace: ResolverTypeWrapper<DiscoverableWorkspace>;
|
||||
EditCommentInput: EditCommentInput;
|
||||
EmailVerificationRequestInput: EmailVerificationRequestInput;
|
||||
FileUpload: ResolverTypeWrapper<FileUploadGraphQLReturn>;
|
||||
@@ -4201,6 +4260,7 @@ export type ResolversTypes = {
|
||||
ID: ResolverTypeWrapper<Scalars['ID']['output']>;
|
||||
Int: ResolverTypeWrapper<Scalars['Int']['output']>;
|
||||
JSONObject: ResolverTypeWrapper<Scalars['JSONObject']['output']>;
|
||||
JoinWorkspaceInput: JoinWorkspaceInput;
|
||||
LegacyCommentViewerData: ResolverTypeWrapper<LegacyCommentViewerData>;
|
||||
LimitedUser: ResolverTypeWrapper<LimitedUserGraphQLReturn>;
|
||||
MarkReceivedVersionInput: MarkReceivedVersionInput;
|
||||
@@ -4341,6 +4401,8 @@ export type ResolversTypes = {
|
||||
WorkspaceCollaborator: ResolverTypeWrapper<WorkspaceCollaboratorGraphQLReturn>;
|
||||
WorkspaceCollection: ResolverTypeWrapper<Omit<WorkspaceCollection, 'items'> & { items: Array<ResolversTypes['Workspace']> }>;
|
||||
WorkspaceCreateInput: WorkspaceCreateInput;
|
||||
WorkspaceDomain: ResolverTypeWrapper<WorkspaceDomain>;
|
||||
WorkspaceDomainDeleteInput: WorkspaceDomainDeleteInput;
|
||||
WorkspaceInviteCreateInput: WorkspaceInviteCreateInput;
|
||||
WorkspaceInviteMutations: ResolverTypeWrapper<WorkspaceInviteMutationsGraphQLReturn>;
|
||||
WorkspaceInviteResendInput: WorkspaceInviteResendInput;
|
||||
@@ -4360,6 +4422,7 @@ export type ResolversParentTypes = {
|
||||
ActiveUserMutations: MutationsObjectGraphQLReturn;
|
||||
Activity: Activity;
|
||||
ActivityCollection: ActivityCollection;
|
||||
AddDomainToWorkspaceInput: AddDomainToWorkspaceInput;
|
||||
AdminInviteList: Omit<AdminInviteList, 'items'> & { items: Array<ResolversParentTypes['ServerInvite']> };
|
||||
AdminQueries: GraphQLEmptyReturn;
|
||||
AdminUserList: AdminUserList;
|
||||
@@ -4435,6 +4498,7 @@ export type ResolversParentTypes = {
|
||||
DeleteUserEmailInput: DeleteUserEmailInput;
|
||||
DeleteVersionsInput: DeleteVersionsInput;
|
||||
DiscoverableStreamsSortingInput: DiscoverableStreamsSortingInput;
|
||||
DiscoverableWorkspace: DiscoverableWorkspace;
|
||||
EditCommentInput: EditCommentInput;
|
||||
EmailVerificationRequestInput: EmailVerificationRequestInput;
|
||||
FileUpload: FileUploadGraphQLReturn;
|
||||
@@ -4445,6 +4509,7 @@ export type ResolversParentTypes = {
|
||||
ID: Scalars['ID']['output'];
|
||||
Int: Scalars['Int']['output'];
|
||||
JSONObject: Scalars['JSONObject']['output'];
|
||||
JoinWorkspaceInput: JoinWorkspaceInput;
|
||||
LegacyCommentViewerData: LegacyCommentViewerData;
|
||||
LimitedUser: LimitedUserGraphQLReturn;
|
||||
MarkReceivedVersionInput: MarkReceivedVersionInput;
|
||||
@@ -4568,6 +4633,8 @@ export type ResolversParentTypes = {
|
||||
WorkspaceCollaborator: WorkspaceCollaboratorGraphQLReturn;
|
||||
WorkspaceCollection: Omit<WorkspaceCollection, 'items'> & { items: Array<ResolversParentTypes['Workspace']> };
|
||||
WorkspaceCreateInput: WorkspaceCreateInput;
|
||||
WorkspaceDomain: WorkspaceDomain;
|
||||
WorkspaceDomainDeleteInput: WorkspaceDomainDeleteInput;
|
||||
WorkspaceInviteCreateInput: WorkspaceInviteCreateInput;
|
||||
WorkspaceInviteMutations: WorkspaceInviteMutationsGraphQLReturn;
|
||||
WorkspaceInviteResendInput: WorkspaceInviteResendInput;
|
||||
@@ -5009,6 +5076,15 @@ export interface DateTimeScalarConfig extends GraphQLScalarTypeConfig<ResolversT
|
||||
name: 'DateTime';
|
||||
}
|
||||
|
||||
export type DiscoverableWorkspaceResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['DiscoverableWorkspace'] = ResolversParentTypes['DiscoverableWorkspace']> = {
|
||||
defaultLogoIndex?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
logo?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type FileUploadResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['FileUpload'] = ResolversParentTypes['FileUpload']> = {
|
||||
branchName?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
convertedCommitId?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
@@ -5739,6 +5815,7 @@ export type UserResolvers<ContextType = GraphQLContext, ParentType extends Resol
|
||||
company?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
createdApps?: Resolver<Maybe<Array<ResolversTypes['ServerApp']>>, ParentType, ContextType>;
|
||||
createdAt?: Resolver<Maybe<ResolversTypes['DateTime']>, ParentType, ContextType>;
|
||||
discoverableWorkspaces?: Resolver<Array<ResolversTypes['DiscoverableWorkspace']>, ParentType, ContextType>;
|
||||
email?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
emails?: Resolver<Array<ResolversTypes['UserEmail']>, ParentType, ContextType>;
|
||||
favoriteStreams?: Resolver<ResolversTypes['StreamCollection'], ParentType, ContextType, RequireFields<UserFavoriteStreamsArgs, 'limit'>>;
|
||||
@@ -5909,6 +5986,9 @@ export type WorkspaceResolvers<ContextType = GraphQLContext, ParentType extends
|
||||
createdAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
|
||||
defaultLogoIndex?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
discoverabilityEnabled?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
|
||||
domainBasedMembershipProtectionEnabled?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
|
||||
domains?: Resolver<Array<ResolversTypes['WorkspaceDomain']>, ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
invitedTeam?: Resolver<Maybe<Array<ResolversTypes['PendingWorkspaceCollaborator']>>, ParentType, ContextType, Partial<WorkspaceInvitedTeamArgs>>;
|
||||
logo?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
@@ -5934,6 +6014,12 @@ export type WorkspaceCollectionResolvers<ContextType = GraphQLContext, ParentTyp
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type WorkspaceDomainResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspaceDomain'] = ResolversParentTypes['WorkspaceDomain']> = {
|
||||
domain?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type WorkspaceInviteMutationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspaceInviteMutations'] = ResolversParentTypes['WorkspaceInviteMutations']> = {
|
||||
batchCreate?: Resolver<ResolversTypes['Workspace'], ParentType, ContextType, RequireFields<WorkspaceInviteMutationsBatchCreateArgs, 'input' | 'workspaceId'>>;
|
||||
cancel?: Resolver<ResolversTypes['Workspace'], ParentType, ContextType, RequireFields<WorkspaceInviteMutationsCancelArgs, 'inviteId' | 'workspaceId'>>;
|
||||
@@ -5944,9 +6030,12 @@ export type WorkspaceInviteMutationsResolvers<ContextType = GraphQLContext, Pare
|
||||
};
|
||||
|
||||
export type WorkspaceMutationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspaceMutations'] = ResolversParentTypes['WorkspaceMutations']> = {
|
||||
addDomain?: Resolver<ResolversTypes['Workspace'], ParentType, ContextType, RequireFields<WorkspaceMutationsAddDomainArgs, 'input'>>;
|
||||
create?: Resolver<ResolversTypes['Workspace'], ParentType, ContextType, RequireFields<WorkspaceMutationsCreateArgs, 'input'>>;
|
||||
delete?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<WorkspaceMutationsDeleteArgs, 'workspaceId'>>;
|
||||
deleteDomain?: Resolver<ResolversTypes['Workspace'], ParentType, ContextType, RequireFields<WorkspaceMutationsDeleteDomainArgs, 'input'>>;
|
||||
invites?: Resolver<ResolversTypes['WorkspaceInviteMutations'], ParentType, ContextType>;
|
||||
join?: Resolver<ResolversTypes['Workspace'], ParentType, ContextType, RequireFields<WorkspaceMutationsJoinArgs, 'input'>>;
|
||||
leave?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<WorkspaceMutationsLeaveArgs, 'id'>>;
|
||||
update?: Resolver<ResolversTypes['Workspace'], ParentType, ContextType, RequireFields<WorkspaceMutationsUpdateArgs, 'input'>>;
|
||||
updateRole?: Resolver<ResolversTypes['Workspace'], ParentType, ContextType, RequireFields<WorkspaceMutationsUpdateRoleArgs, 'input'>>;
|
||||
@@ -5999,6 +6088,7 @@ export type Resolvers<ContextType = GraphQLContext> = {
|
||||
CommitCollection?: CommitCollectionResolvers<ContextType>;
|
||||
CountOnlyCollection?: CountOnlyCollectionResolvers<ContextType>;
|
||||
DateTime?: GraphQLScalarType;
|
||||
DiscoverableWorkspace?: DiscoverableWorkspaceResolvers<ContextType>;
|
||||
FileUpload?: FileUploadResolvers<ContextType>;
|
||||
GendoAIRender?: GendoAiRenderResolvers<ContextType>;
|
||||
GendoAIRenderCollection?: GendoAiRenderCollectionResolvers<ContextType>;
|
||||
@@ -6083,6 +6173,7 @@ export type Resolvers<ContextType = GraphQLContext> = {
|
||||
Workspace?: WorkspaceResolvers<ContextType>;
|
||||
WorkspaceCollaborator?: WorkspaceCollaboratorResolvers<ContextType>;
|
||||
WorkspaceCollection?: WorkspaceCollectionResolvers<ContextType>;
|
||||
WorkspaceDomain?: WorkspaceDomainResolvers<ContextType>;
|
||||
WorkspaceInviteMutations?: WorkspaceInviteMutationsResolvers<ContextType>;
|
||||
WorkspaceMutations?: WorkspaceMutationsResolvers<ContextType>;
|
||||
};
|
||||
|
||||
@@ -166,3 +166,12 @@ export const setPrimaryUserEmailFactory =
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
export const findVerifiedEmailsByUserIdFactory =
|
||||
({ db }: { db: Knex }): FindEmailsByUserId =>
|
||||
async ({ userId }) => {
|
||||
return db(UserEmails.name).where({
|
||||
[UserEmails.col.userId]: userId,
|
||||
[UserEmails.col.verified]: true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -54,6 +54,11 @@ export type ActivityCollection = {
|
||||
totalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type AddDomainToWorkspaceInput = {
|
||||
domain: Scalars['String']['input'];
|
||||
workspaceId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type AdminInviteList = {
|
||||
__typename?: 'AdminInviteList';
|
||||
cursor?: Maybe<Scalars['String']['output']>;
|
||||
@@ -846,6 +851,15 @@ export type DiscoverableStreamsSortingInput = {
|
||||
type: DiscoverableStreamsSortType;
|
||||
};
|
||||
|
||||
export type DiscoverableWorkspace = {
|
||||
__typename?: 'DiscoverableWorkspace';
|
||||
defaultLogoIndex: Scalars['Int']['output'];
|
||||
description?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
logo?: Maybe<Scalars['String']['output']>;
|
||||
name: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type EditCommentInput = {
|
||||
commentId: Scalars['String']['input'];
|
||||
content: CommentContentInput;
|
||||
@@ -919,6 +933,10 @@ export type GendoAiRenderInput = {
|
||||
versionId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type JoinWorkspaceInput = {
|
||||
workspaceId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type LegacyCommentViewerData = {
|
||||
__typename?: 'LegacyCommentViewerData';
|
||||
/**
|
||||
@@ -3368,6 +3386,8 @@ export type User = {
|
||||
/** Returns the apps you have created. */
|
||||
createdApps?: Maybe<Array<ServerApp>>;
|
||||
createdAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
/** Get discoverable workspaces with verified domains that match the active user's */
|
||||
discoverableWorkspaces: Array<DiscoverableWorkspace>;
|
||||
/** Only returned if API user is the user being requested or an admin */
|
||||
email?: Maybe<Scalars['String']['output']>;
|
||||
emails: Array<UserEmail>;
|
||||
@@ -3827,6 +3847,12 @@ export type Workspace = {
|
||||
/** Selected fallback when `logo` not set */
|
||||
defaultLogoIndex: Scalars['Int']['output'];
|
||||
description?: Maybe<Scalars['String']['output']>;
|
||||
/** Enable/Disable discovery of the workspace */
|
||||
discoverabilityEnabled: Scalars['Boolean']['output'];
|
||||
/** Enable/Disable restriction to invite users to workspace as Guests only */
|
||||
domainBasedMembershipProtectionEnabled: Scalars['Boolean']['output'];
|
||||
/** Verified workspace domains */
|
||||
domains: Array<WorkspaceDomain>;
|
||||
id: Scalars['ID']['output'];
|
||||
/** Only available to workspace owners */
|
||||
invitedTeam?: Maybe<Array<PendingWorkspaceCollaborator>>;
|
||||
@@ -3877,6 +3903,17 @@ export type WorkspaceCreateInput = {
|
||||
name: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type WorkspaceDomain = {
|
||||
__typename?: 'WorkspaceDomain';
|
||||
domain: Scalars['String']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
};
|
||||
|
||||
export type WorkspaceDomainDeleteInput = {
|
||||
id: Scalars['ID']['input'];
|
||||
workspaceId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type WorkspaceInviteCreateInput = {
|
||||
/** Either this or userId must be filled */
|
||||
email?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -3942,15 +3979,23 @@ export type WorkspaceInviteUseInput = {
|
||||
|
||||
export type WorkspaceMutations = {
|
||||
__typename?: 'WorkspaceMutations';
|
||||
addDomain: Workspace;
|
||||
create: Workspace;
|
||||
delete: Scalars['Boolean']['output'];
|
||||
deleteDomain: Workspace;
|
||||
invites: WorkspaceInviteMutations;
|
||||
join: Workspace;
|
||||
leave: Scalars['Boolean']['output'];
|
||||
update: Workspace;
|
||||
updateRole: Workspace;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceMutationsAddDomainArgs = {
|
||||
input: AddDomainToWorkspaceInput;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceMutationsCreateArgs = {
|
||||
input: WorkspaceCreateInput;
|
||||
};
|
||||
@@ -3961,6 +4006,16 @@ export type WorkspaceMutationsDeleteArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceMutationsDeleteDomainArgs = {
|
||||
input: WorkspaceDomainDeleteInput;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceMutationsJoinArgs = {
|
||||
input: JoinWorkspaceInput;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceMutationsLeaveArgs = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
@@ -4021,6 +4076,8 @@ export type WorkspaceTeamFilter = {
|
||||
export type WorkspaceUpdateInput = {
|
||||
defaultLogoIndex?: InputMaybe<Scalars['Int']['input']>;
|
||||
description?: InputMaybe<Scalars['String']['input']>;
|
||||
discoverabilityEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
domainBasedMembershipProtectionEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
id: Scalars['String']['input'];
|
||||
/** Logo image as base64-encoded string */
|
||||
logo?: InputMaybe<Scalars['String']['input']>;
|
||||
|
||||
@@ -45,7 +45,7 @@ type EventPayloadsMap = UnionToIntersection<
|
||||
EventPayloadsByNamespaceMap[keyof EventPayloadsByNamespaceMap]
|
||||
>
|
||||
|
||||
type EventNames = keyof EventPayloadsMap
|
||||
export type EventNames = keyof EventPayloadsMap
|
||||
|
||||
type EventPayloadsByNamespaceMap = {
|
||||
// for each event namespace
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Roles } from '@speckle/shared'
|
||||
import { expect } from 'chai'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
|
||||
const createFakeWorkspace = (): Workspace => {
|
||||
const createFakeWorkspace = (): Omit<Workspace, 'domains'> => {
|
||||
return {
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
description: cryptoRandomString({ length: 10 }),
|
||||
@@ -13,7 +13,9 @@ const createFakeWorkspace = (): Workspace => {
|
||||
defaultLogoIndex: 0,
|
||||
name: cryptoRandomString({ length: 10 }),
|
||||
updatedAt: new Date(),
|
||||
createdAt: new Date()
|
||||
createdAt: new Date(),
|
||||
domainBasedMembershipProtectionEnabled: false,
|
||||
discoverabilityEnabled: false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,10 +133,14 @@ describe('Event Bus', () => {
|
||||
const workspacePayload = {
|
||||
...createFakeWorkspace(),
|
||||
createdByUserId: cryptoRandomString({ length: 10 }),
|
||||
eventName: WorkspaceEvents.Created
|
||||
eventName: WorkspaceEvents.Created,
|
||||
domains: []
|
||||
}
|
||||
|
||||
await bus1.emit({ eventName: WorkspaceEvents.Created, payload: workspacePayload })
|
||||
await bus1.emit({
|
||||
eventName: WorkspaceEvents.Created,
|
||||
payload: { ...workspacePayload }
|
||||
})
|
||||
|
||||
expect(workspaces.length).to.equal(2)
|
||||
expect(workspaces).to.deep.equal([workspacePayload, workspacePayload])
|
||||
|
||||
@@ -3,6 +3,8 @@ import { LimitedUserRecord, StreamRecord } from '@/modules/core/helpers/types'
|
||||
import {
|
||||
Workspace,
|
||||
WorkspaceAcl,
|
||||
WorkspaceDomain,
|
||||
WorkspaceWithDomains,
|
||||
WorkspaceWithOptionalRole
|
||||
} from '@/modules/workspacesCore/domain/types'
|
||||
import { EventBusPayloads } from '@/modules/shared/services/eventBus'
|
||||
@@ -12,11 +14,18 @@ import { UserWithRole } from '@/modules/core/repositories/users'
|
||||
/** Workspace */
|
||||
|
||||
type UpsertWorkspaceArgs = {
|
||||
workspace: Workspace
|
||||
workspace: Omit<Workspace, 'domains'>
|
||||
}
|
||||
|
||||
export type UpsertWorkspace = (args: UpsertWorkspaceArgs) => Promise<void>
|
||||
|
||||
export type GetUserDiscoverableWorkspaces = (args: {
|
||||
domains: string[]
|
||||
userId: string
|
||||
}) => Promise<
|
||||
Pick<Workspace, 'id' | 'name' | 'description' | 'logo' | 'defaultLogoIndex'>[]
|
||||
>
|
||||
|
||||
export type GetWorkspace = (args: {
|
||||
workspaceId: string
|
||||
userId?: string
|
||||
@@ -27,10 +36,24 @@ export type GetWorkspaces = (args: {
|
||||
userId?: string
|
||||
}) => Promise<WorkspaceWithOptionalRole[]>
|
||||
|
||||
export type StoreWorkspaceDomain = (args: {
|
||||
workspaceDomain: WorkspaceDomain
|
||||
}) => Promise<void>
|
||||
|
||||
export type GetWorkspaceDomains = (args: {
|
||||
workspaceIds: string[]
|
||||
}) => Promise<WorkspaceDomain[]>
|
||||
|
||||
type DeleteWorkspaceArgs = {
|
||||
workspaceId: string
|
||||
}
|
||||
|
||||
export type DeleteWorkspaceDomain = (args: { id: string }) => Promise<void>
|
||||
|
||||
export type GetWorkspaceWithDomains = (args: {
|
||||
id: string
|
||||
}) => Promise<WorkspaceWithDomains | null>
|
||||
|
||||
export type DeleteWorkspace = (args: DeleteWorkspaceArgs) => Promise<void>
|
||||
|
||||
/** Workspace Roles */
|
||||
|
||||
@@ -42,3 +42,39 @@ export class WorkspaceNotFoundError extends BaseError {
|
||||
static code = 'WORKSPACE_NOT_FOUND_ERROR'
|
||||
static statusCode = 404
|
||||
}
|
||||
|
||||
export class WorkspaceNotDiscoverableError extends BaseError {
|
||||
static defaultMessage = 'Workspace is not discoverable'
|
||||
static code = 'WORKSPACE_NOT_DISCOVERABLE'
|
||||
static statusCode = 400
|
||||
}
|
||||
|
||||
export class WorkspaceNotJoinableError extends BaseError {
|
||||
static defaultMessage = 'Workspace is not joinable'
|
||||
static code = 'WORKSPACE_NOT_JOINABLE'
|
||||
static statusCode = 400
|
||||
}
|
||||
|
||||
export class WorkspaceJoinNotAllowedError extends BaseError {
|
||||
static defaultMessage = 'You do not have permissions to join this workspace'
|
||||
static code = 'WORKSPACE_JOIN_NOT_ALLOWED'
|
||||
static statusCode = 403
|
||||
}
|
||||
|
||||
export class WorkspaceUnverifiedDomainError extends BaseError {
|
||||
static defaultMessage = 'Cannot add unverified domain to workspace'
|
||||
static code = 'WORKSPACE_UNVERIFIED_DOMAIN_ERROR'
|
||||
static statusCode = 403
|
||||
}
|
||||
|
||||
export class WorkspaceDomainBlockedError extends BaseError {
|
||||
static defaultMessage = 'Cannot add blocked domain to workspace'
|
||||
static code = 'WORKSPACE_DOMAIN_BLOCKED_ERROR'
|
||||
static statusCode = 400
|
||||
}
|
||||
|
||||
export class WorkspaceProtectedError extends BaseError {
|
||||
static defaultMessage = 'Workspace protected'
|
||||
static code = 'WORKSPACE_PROTECTED'
|
||||
static statusCode = 400
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { db } from '@/db/knex'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
import { defineRequestDataloaders } from '@/modules/shared/helpers/graphqlHelper'
|
||||
import { getWorkspacesFactory } from '@/modules/workspaces/repositories/workspaces'
|
||||
import { WorkspaceWithOptionalRole } from '@/modules/workspacesCore/domain/types'
|
||||
import {
|
||||
getWorkspaceDomainsFactory,
|
||||
getWorkspacesFactory
|
||||
} from '@/modules/workspaces/repositories/workspaces'
|
||||
import {
|
||||
WorkspaceDomain,
|
||||
WorkspaceWithOptionalRole
|
||||
} from '@/modules/workspacesCore/domain/types'
|
||||
import { keyBy } from 'lodash'
|
||||
|
||||
const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
|
||||
@@ -14,6 +20,7 @@ declare module '@/modules/core/loaders' {
|
||||
|
||||
const dataLoadersDefinition = defineRequestDataloaders(({ ctx, createLoader }) => {
|
||||
const getWorkspaces = getWorkspacesFactory({ db })
|
||||
const getWorkspaceDomains = getWorkspaceDomainsFactory({ db })
|
||||
|
||||
return {
|
||||
workspaces: {
|
||||
@@ -29,6 +36,18 @@ const dataLoadersDefinition = defineRequestDataloaders(({ ctx, createLoader }) =
|
||||
return ids.map((id) => results[id] || null)
|
||||
}
|
||||
)
|
||||
},
|
||||
workspaceDomains: {
|
||||
/**
|
||||
* Get workspace, with the active user's role attached
|
||||
*/
|
||||
getWorkspaceDomains: createLoader<string, WorkspaceDomain | null>(async (ids) => {
|
||||
const results = keyBy(
|
||||
await getWorkspaceDomains({ workspaceIds: ids.slice() }),
|
||||
(w) => w.id
|
||||
)
|
||||
return ids.map((id) => results[id] || null)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -130,6 +130,15 @@ const config: SpeckleModuleMocksConfig = FF_WORKSPACES_MODULE_ENABLED
|
||||
}
|
||||
},
|
||||
User: {
|
||||
discoverableWorkspaces: resolveAndCache(() => [
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
name: workspaceName(),
|
||||
description: faker.lorem.sentence(),
|
||||
defaultLogoIndex: 0,
|
||||
logo: null
|
||||
}
|
||||
]),
|
||||
workspaces: resolveAndCache((_parent, args) =>
|
||||
getMockRef('WorkspaceCollection', {
|
||||
values: {
|
||||
@@ -159,7 +168,17 @@ const config: SpeckleModuleMocksConfig = FF_WORKSPACES_MODULE_ENABLED
|
||||
cursor: args.cursor ? null : undefined
|
||||
}
|
||||
})
|
||||
)
|
||||
),
|
||||
domains: resolveAndCache(() => [
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
domain: 'speckle.systems'
|
||||
},
|
||||
{
|
||||
id: faker.string.uuid(),
|
||||
domain: 'example.org'
|
||||
}
|
||||
])
|
||||
},
|
||||
WorkspaceCollaborator: {
|
||||
role: resolveFromMockParent(),
|
||||
|
||||
@@ -41,6 +41,7 @@ import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { WorkspaceInviteResourceType } from '@/modules/workspaces/domain/constants'
|
||||
import {
|
||||
WorkspaceInvalidRoleError,
|
||||
WorkspaceJoinNotAllowedError,
|
||||
WorkspaceNotFoundError,
|
||||
WorkspacesNotAuthorizedError,
|
||||
WorkspacesNotYetImplementedError
|
||||
@@ -55,7 +56,12 @@ import {
|
||||
getWorkspaceRolesForUserFactory,
|
||||
upsertWorkspaceFactory,
|
||||
upsertWorkspaceRoleFactory,
|
||||
workspaceInviteValidityFilter
|
||||
workspaceInviteValidityFilter,
|
||||
storeWorkspaceDomainFactory,
|
||||
deleteWorkspaceDomainFactory,
|
||||
getWorkspaceDomainsFactory,
|
||||
getUserDiscoverableWorkspacesFactory,
|
||||
getWorkspaceWithDomainsFactory
|
||||
} from '@/modules/workspaces/repositories/workspaces'
|
||||
import {
|
||||
buildWorkspaceInviteEmailContentsFactory,
|
||||
@@ -68,6 +74,7 @@ import {
|
||||
validateWorkspaceInviteBeforeFinalizationFactory
|
||||
} from '@/modules/workspaces/services/invites'
|
||||
import {
|
||||
addDomainToWorkspaceFactory,
|
||||
createWorkspaceFactory,
|
||||
deleteWorkspaceFactory,
|
||||
deleteWorkspaceRoleFactory,
|
||||
@@ -78,15 +85,21 @@ import {
|
||||
getWorkspaceProjectsFactory,
|
||||
queryAllWorkspaceProjectsFactory
|
||||
} from '@/modules/workspaces/services/projects'
|
||||
import { getWorkspacesForUserFactory } from '@/modules/workspaces/services/retrieval'
|
||||
import {
|
||||
getDiscoverableWorkspacesForUserFactory,
|
||||
getWorkspacesForUserFactory
|
||||
} from '@/modules/workspaces/services/retrieval'
|
||||
import { Roles, WorkspaceRoles, removeNullOrUndefinedKeys } from '@speckle/shared'
|
||||
import { chunk } from 'lodash'
|
||||
import { deleteStream } from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
findEmailsByUserIdFactory,
|
||||
findVerifiedEmailsByUserIdFactory,
|
||||
createUserEmailFactory,
|
||||
ensureNoPrimaryEmailForUserFactory,
|
||||
findEmailFactory
|
||||
} from '@/modules/core/repositories/userEmails'
|
||||
import { joinWorkspaceFactory } from '@/modules/workspaces/services/join'
|
||||
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
|
||||
import { requestNewEmailVerification } from '@/modules/emails/services/verification/request'
|
||||
|
||||
@@ -113,7 +126,9 @@ const buildCreateAndSendWorkspaceInvite = () =>
|
||||
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
|
||||
collectAndValidateResourceTargets: collectAndValidateWorkspaceTargetsFactory({
|
||||
getStream,
|
||||
getWorkspace: getWorkspaceFactory({ db })
|
||||
getWorkspace: getWorkspaceFactory({ db }),
|
||||
getWorkspaceDomains: getWorkspaceDomainsFactory({ db }),
|
||||
findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db })
|
||||
}),
|
||||
buildInviteEmailContents: buildWorkspaceInviteEmailContentsFactory({
|
||||
getStream,
|
||||
@@ -316,6 +331,8 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
|
||||
const updateWorkspaceRole = updateWorkspaceRoleFactory({
|
||||
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }),
|
||||
getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }),
|
||||
findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db }),
|
||||
getWorkspaceRoles,
|
||||
emitWorkspaceEvent,
|
||||
getStreams,
|
||||
@@ -327,6 +344,60 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
|
||||
return await getWorkspaceFactory({ db })({ workspaceId })
|
||||
},
|
||||
addDomain: async (_parent, args, context) => {
|
||||
await authorizeResolver(
|
||||
context.userId!,
|
||||
args.input.workspaceId,
|
||||
Roles.Workspace.Admin,
|
||||
context.resourceAccessRules
|
||||
)
|
||||
|
||||
await addDomainToWorkspaceFactory({
|
||||
getWorkspace: getWorkspaceFactory({ db }),
|
||||
findEmailsByUserId: findEmailsByUserIdFactory({ db }),
|
||||
storeWorkspaceDomain: storeWorkspaceDomainFactory({ db }),
|
||||
upsertWorkspace: upsertWorkspaceFactory({ db }),
|
||||
getDomains: getWorkspaceDomainsFactory({ db }),
|
||||
emitWorkspaceEvent: getEventBus().emit
|
||||
})({
|
||||
workspaceId: args.input.workspaceId,
|
||||
userId: context.userId!,
|
||||
domain: args.input.domain
|
||||
})
|
||||
|
||||
return await getWorkspaceFactory({ db })({
|
||||
workspaceId: args.input.workspaceId,
|
||||
userId: context.userId
|
||||
})
|
||||
},
|
||||
async deleteDomain(_parent, args, context) {
|
||||
await authorizeResolver(
|
||||
context.userId!,
|
||||
args.input.workspaceId,
|
||||
Roles.Workspace.Admin,
|
||||
context.resourceAccessRules
|
||||
)
|
||||
await deleteWorkspaceDomainFactory({ db })({ id: args.input.id })
|
||||
return await getWorkspaceFactory({ db })({
|
||||
workspaceId: args.input.workspaceId,
|
||||
userId: context.userId
|
||||
})
|
||||
},
|
||||
async join(_parent, args, context) {
|
||||
if (!context.userId) throw new WorkspaceJoinNotAllowedError()
|
||||
|
||||
await joinWorkspaceFactory({
|
||||
getUserEmails: findEmailsByUserIdFactory({ db }),
|
||||
getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }),
|
||||
insertWorkspaceRole: upsertWorkspaceRoleFactory({ db }),
|
||||
emitWorkspaceEvent: getEventBus().emit
|
||||
})({ userId: context.userId, workspaceId: args.input.workspaceId })
|
||||
|
||||
return await getWorkspaceFactory({ db })({
|
||||
workspaceId: args.input.workspaceId,
|
||||
userId: context.userId
|
||||
})
|
||||
},
|
||||
leave: async (_parent, args, ctx) => {
|
||||
const userId = ctx.userId!
|
||||
|
||||
@@ -437,6 +508,8 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
processInvite: processFinalizedWorkspaceInviteFactory({
|
||||
getWorkspace: getWorkspaceFactory({ db }),
|
||||
updateWorkspaceRole: updateWorkspaceRoleFactory({
|
||||
getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }),
|
||||
findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db }),
|
||||
getWorkspaceRoles: getWorkspaceRolesFactory({ db }),
|
||||
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }),
|
||||
emitWorkspaceEvent: ({ eventName, payload }) =>
|
||||
@@ -535,6 +608,9 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
filter: { ...(args.filter || {}) }
|
||||
}
|
||||
)
|
||||
},
|
||||
domains: async (parent) => {
|
||||
return await getWorkspaceDomainsFactory({ db })({ workspaceIds: [parent.id] })
|
||||
}
|
||||
},
|
||||
WorkspaceCollaborator: {
|
||||
@@ -597,6 +673,19 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
}
|
||||
},
|
||||
User: {
|
||||
discoverableWorkspaces: async (_parent, _args, context) => {
|
||||
if (!context.userId) {
|
||||
throw new WorkspacesNotAuthorizedError()
|
||||
}
|
||||
|
||||
const getDiscoverableWorkspacesForUser =
|
||||
getDiscoverableWorkspacesForUserFactory({
|
||||
findEmailsByUserId: findEmailsByUserIdFactory({ db }),
|
||||
getDiscoverableWorkspaces: getUserDiscoverableWorkspacesFactory({ db })
|
||||
})
|
||||
|
||||
return await getDiscoverableWorkspacesForUser({ userId: context.userId })
|
||||
},
|
||||
workspaces: async (_parent, _args, context) => {
|
||||
if (!context.userId) {
|
||||
throw new WorkspacesNotAuthorizedError()
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
export const blockedDomains = [
|
||||
// Common Free Email Providers
|
||||
'gmail.com',
|
||||
'yahoo.com',
|
||||
'hotmail.com',
|
||||
'outlook.com',
|
||||
'live.com',
|
||||
'aol.com',
|
||||
'ymail.com',
|
||||
'mail.com',
|
||||
'protonmail.com',
|
||||
'icloud.com',
|
||||
'zoho.com',
|
||||
'gmx.com',
|
||||
'me.com',
|
||||
'inbox.com',
|
||||
|
||||
// Temporary/Disposable Email Providers
|
||||
'mailinator.com',
|
||||
'10minutemail.com',
|
||||
'guerrillamail.com',
|
||||
'tempmail.com',
|
||||
'yopmail.com',
|
||||
'throwawaymail.com',
|
||||
'temp-mail.org',
|
||||
'maildrop.cc',
|
||||
'getairmail.com',
|
||||
'mintemail.com',
|
||||
'fakemail.net',
|
||||
'temp-mail.ru',
|
||||
'moakt.com',
|
||||
'emailondeck.com',
|
||||
'spamgourmet.com',
|
||||
'mailcatch.com',
|
||||
'sharklasers.com',
|
||||
'trashmail.com',
|
||||
'mytrashmail.com',
|
||||
'emailfake.com',
|
||||
'fakeinbox.com',
|
||||
'spamex.com',
|
||||
'spambox.us',
|
||||
'mailsac.com',
|
||||
'fakemailgenerator.com',
|
||||
'33mail.com',
|
||||
'anonmails.de',
|
||||
'anonbox.net',
|
||||
'anonymousspeech.com',
|
||||
'boun.cr',
|
||||
'guerrillamailblock.com',
|
||||
'mailfreeonline.com',
|
||||
'temp-email.com',
|
||||
'mailnesia.com',
|
||||
'hmamail.com',
|
||||
'fastmail.com',
|
||||
'tmailinator.com',
|
||||
'spam4.me',
|
||||
'fakebox.com',
|
||||
'emkei.cz',
|
||||
'dispostable.com',
|
||||
'mytemp.email',
|
||||
'deadaddress.com',
|
||||
'spamdecoy.net',
|
||||
'0wnd.net',
|
||||
'0wnd.org',
|
||||
'10mail.org',
|
||||
'20mail.it',
|
||||
'20mail.in',
|
||||
'24hourmail.com',
|
||||
'2prong.com',
|
||||
'3d-painting.com',
|
||||
'4warding.com',
|
||||
'4warding.net',
|
||||
'4warding.org',
|
||||
'5mail.cf',
|
||||
'60minutemail.com',
|
||||
'675hosting.com',
|
||||
'675hosting.net',
|
||||
'675hosting.org',
|
||||
'6ip.us',
|
||||
'6url.com',
|
||||
'75hosting.com',
|
||||
'75hosting.net',
|
||||
'75hosting.org',
|
||||
'7tags.com',
|
||||
'9ox.net',
|
||||
'a-bc.net',
|
||||
'afrobacon.com',
|
||||
'ajaxapp.net',
|
||||
'amilegit.com',
|
||||
'anonbox.net',
|
||||
'antichef.com',
|
||||
'antichef.net',
|
||||
'antireg.ru',
|
||||
'antispam.de',
|
||||
'baxomale.ht.cx',
|
||||
'beefmilk.com',
|
||||
'binkmail.com',
|
||||
'bio-muesli.net',
|
||||
'bobmail.info',
|
||||
'bofthew.com',
|
||||
'brefmail.com',
|
||||
'bsnow.net',
|
||||
'bugmenot.com',
|
||||
'bumpymail.com',
|
||||
'casualdx.com',
|
||||
'chogmail.com',
|
||||
'cool.fr.nf',
|
||||
'correo.blogos.net',
|
||||
'cosmorph.com',
|
||||
'courriel.fr.nf',
|
||||
'cubiclink.com',
|
||||
'curryworld.de',
|
||||
'dacoolest.com',
|
||||
'dandikmail.com',
|
||||
'deadspam.com',
|
||||
'despam.it',
|
||||
'devnullmail.com',
|
||||
'dfgh.net',
|
||||
'digitalsanctuary.com',
|
||||
'discardmail.com',
|
||||
'dispose.it',
|
||||
'disposableaddress.com',
|
||||
'disposeamail.com',
|
||||
'dispostable.com',
|
||||
'dodgeit.com',
|
||||
'dodgit.com',
|
||||
'dodgit.org',
|
||||
'dontreg.com',
|
||||
'dontsendmespam.de'
|
||||
]
|
||||
@@ -7,7 +7,9 @@ export const Workspaces = buildTableHelper('workspaces', [
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'logo',
|
||||
'defaultLogoIndex'
|
||||
'defaultLogoIndex',
|
||||
'domainBasedMembershipProtectionEnabled',
|
||||
'discoverabilityEnabled'
|
||||
])
|
||||
|
||||
export const WorkspaceAcl = buildTableHelper('workspace_acl', [
|
||||
@@ -15,3 +17,13 @@ export const WorkspaceAcl = buildTableHelper('workspace_acl', [
|
||||
'role',
|
||||
'workspaceId'
|
||||
])
|
||||
|
||||
export const WorkspaceDomains = buildTableHelper('workspace_domains', [
|
||||
'id',
|
||||
'workspaceId',
|
||||
'domain',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'createdByUserId',
|
||||
'verified'
|
||||
])
|
||||
|
||||
@@ -9,12 +9,14 @@ import { registerOrUpdateRole } from '@/modules/shared/repositories/roles'
|
||||
import { initializeEventListenersFactory } from '@/modules/workspaces/events/eventListener'
|
||||
import {
|
||||
getWorkspaceRolesFactory,
|
||||
getWorkspaceWithDomainsFactory,
|
||||
upsertWorkspaceRoleFactory
|
||||
} from '@/modules/workspaces/repositories/workspaces'
|
||||
import { getStream, grantStreamPermissions } from '@/modules/core/repositories/streams'
|
||||
import { updateWorkspaceRoleFactory } from '@/modules/workspaces/services/management'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { getStreams } from '@/modules/core/services/streams'
|
||||
import { findVerifiedEmailsByUserIdFactory } from '@/modules/core/repositories/userEmails'
|
||||
import { validateModuleLicense } from '@/modules/gatekeeper/services/validateLicense'
|
||||
|
||||
const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
|
||||
@@ -49,6 +51,8 @@ const workspacesModule: SpeckleModule = {
|
||||
getStream,
|
||||
logger: moduleLogger,
|
||||
updateWorkspaceRole: updateWorkspaceRoleFactory({
|
||||
getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }),
|
||||
findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db }),
|
||||
getWorkspaceRoles: getWorkspaceRolesFactory({ db }),
|
||||
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }),
|
||||
emitWorkspaceEvent: (...args) => getEventBus().emit(...args),
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import {
|
||||
Workspace,
|
||||
WorkspaceAcl,
|
||||
WorkspaceDomain,
|
||||
WorkspaceWithOptionalRole
|
||||
} from '@/modules/workspacesCore/domain/types'
|
||||
import {
|
||||
DeleteWorkspace,
|
||||
DeleteWorkspaceDomain,
|
||||
DeleteWorkspaceRole,
|
||||
GetUserDiscoverableWorkspaces,
|
||||
GetWorkspace,
|
||||
GetWorkspaceCollaborators,
|
||||
GetWorkspaceDomains,
|
||||
GetWorkspaceRoleForUser,
|
||||
GetWorkspaceRoles,
|
||||
GetWorkspaceRolesForUser,
|
||||
GetWorkspaceWithDomains,
|
||||
GetWorkspaces,
|
||||
StoreWorkspaceDomain,
|
||||
UpsertWorkspace,
|
||||
UpsertWorkspaceRole
|
||||
} from '@/modules/workspaces/domain/operations'
|
||||
@@ -21,6 +27,7 @@ import { StreamRecord } from '@/modules/core/helpers/types'
|
||||
import { WorkspaceInvalidRoleError } from '@/modules/workspaces/errors/workspace'
|
||||
import {
|
||||
WorkspaceAcl as DbWorkspaceAcl,
|
||||
WorkspaceDomains,
|
||||
Workspaces
|
||||
} from '@/modules/workspaces/helpers/db'
|
||||
import { knex, ServerAcl, ServerInvites, Users } from '@/modules/core/dbSchema'
|
||||
@@ -35,9 +42,35 @@ import { WorkspaceInviteResourceType } from '@/modules/workspaces/domain/constan
|
||||
const tables = {
|
||||
streams: (db: Knex) => db<StreamRecord>('streams'),
|
||||
workspaces: (db: Knex) => db<Workspace>('workspaces'),
|
||||
workspaceDomains: (db: Knex) => db<WorkspaceDomain>('workspace_domains'),
|
||||
workspacesAcl: (db: Knex) => db<WorkspaceAcl>('workspace_acl')
|
||||
}
|
||||
|
||||
export const getUserDiscoverableWorkspacesFactory =
|
||||
({ db }: { db: Knex }): GetUserDiscoverableWorkspaces =>
|
||||
async ({ domains, userId }) => {
|
||||
if (domains.length === 0) {
|
||||
return []
|
||||
}
|
||||
return (await tables
|
||||
.workspaces(db)
|
||||
.select('workspaces.id as id', 'name', 'description', 'logo', 'defaultLogoIndex')
|
||||
.distinctOn('workspaces.id')
|
||||
.join('workspace_domains', 'workspace_domains.workspaceId', 'workspaces.id')
|
||||
.leftJoin(
|
||||
tables.workspacesAcl(db).select('*').where({ userId }).as('acl'),
|
||||
'acl.workspaceId',
|
||||
'workspaces.id'
|
||||
)
|
||||
.whereIn('domain', domains)
|
||||
.where('discoverabilityEnabled', true)
|
||||
.where('verified', true)
|
||||
.where('role', null)) as Pick<
|
||||
Workspace,
|
||||
'id' | 'name' | 'description' | 'logo' | 'defaultLogoIndex'
|
||||
>[]
|
||||
}
|
||||
|
||||
export const getWorkspacesFactory =
|
||||
({ db }: { db: Knex }): GetWorkspaces =>
|
||||
async (params: {
|
||||
@@ -92,7 +125,15 @@ export const upsertWorkspaceFactory =
|
||||
.workspaces(db)
|
||||
.insert(workspace)
|
||||
.onConflict('id')
|
||||
.merge(['description', 'logo', 'defaultLogoIndex', 'name', 'updatedAt'])
|
||||
.merge([
|
||||
'description',
|
||||
'logo',
|
||||
'defaultLogoIndex',
|
||||
'name',
|
||||
'updatedAt',
|
||||
'domainBasedMembershipProtectionEnabled',
|
||||
'discoverabilityEnabled'
|
||||
])
|
||||
}
|
||||
|
||||
export const deleteWorkspaceFactory =
|
||||
@@ -220,3 +261,44 @@ export const workspaceInviteValidityFilter: InvitesRetrievalValidityFilter = (q)
|
||||
).orWhereNotNull(Workspaces.col.id)
|
||||
})
|
||||
}
|
||||
|
||||
export const storeWorkspaceDomainFactory =
|
||||
({ db }: { db: Knex }): StoreWorkspaceDomain =>
|
||||
async ({ workspaceDomain }): Promise<void> => {
|
||||
await tables.workspaceDomains(db).insert(workspaceDomain)
|
||||
}
|
||||
|
||||
export const getWorkspaceDomainsFactory =
|
||||
({ db }: { db: Knex }): GetWorkspaceDomains =>
|
||||
({ workspaceIds }) => {
|
||||
return tables.workspaceDomains(db).whereIn('workspaceId', workspaceIds)
|
||||
}
|
||||
|
||||
export const deleteWorkspaceDomainFactory =
|
||||
({ db }: { db: Knex }): DeleteWorkspaceDomain =>
|
||||
async ({ id }) => {
|
||||
await tables.workspaceDomains(db).where({ id }).delete()
|
||||
}
|
||||
|
||||
export const getWorkspaceWithDomainsFactory =
|
||||
({ db }: { db: Knex }): GetWorkspaceWithDomains =>
|
||||
async ({ id }) => {
|
||||
const workspace = await tables
|
||||
.workspaces(db)
|
||||
.select([...Workspaces.cols, WorkspaceDomains.groupArray('domains')])
|
||||
.where({ [Workspaces.col.id]: id })
|
||||
.leftJoin(
|
||||
WorkspaceDomains.name,
|
||||
WorkspaceDomains.col.workspaceId,
|
||||
Workspaces.col.id
|
||||
)
|
||||
.groupBy(Workspaces.col.id)
|
||||
.first()
|
||||
if (!workspace) return null
|
||||
return {
|
||||
...workspace,
|
||||
domains: workspace.domains.filter(
|
||||
(domain: WorkspaceDomain | null) => domain !== null
|
||||
)
|
||||
} as Workspace & { domains: WorkspaceDomain[] }
|
||||
}
|
||||
|
||||
@@ -51,12 +51,17 @@ import {
|
||||
import { authorizeResolver } from '@/modules/shared'
|
||||
import { getFrontendOrigin } from '@/modules/shared/helpers/envHelper'
|
||||
import { WorkspaceInviteResourceType } from '@/modules/workspaces/domain/constants'
|
||||
import { GetWorkspace } from '@/modules/workspaces/domain/operations'
|
||||
import {
|
||||
GetWorkspace,
|
||||
GetWorkspaceDomains
|
||||
} from '@/modules/workspaces/domain/operations'
|
||||
import { WorkspaceInviteResourceTarget } from '@/modules/workspaces/domain/types'
|
||||
import { mapGqlWorkspaceRoleToMainRole } from '@/modules/workspaces/helpers/roles'
|
||||
import { updateWorkspaceRoleFactory } from '@/modules/workspaces/services/management'
|
||||
import { PendingWorkspaceCollaboratorGraphQLReturn } from '@/modules/workspacesCore/helpers/graphTypes'
|
||||
import { MaybeNullOrUndefined, Nullable, Roles, WorkspaceRoles } from '@speckle/shared'
|
||||
import { WorkspaceProtectedError } from '@/modules/workspaces/errors/workspace'
|
||||
import { FindVerifiedEmailsByUserId } from '@/modules/core/domain/userEmails/operations'
|
||||
|
||||
const isWorkspaceResourceTarget = (
|
||||
target: InviteResourceTarget
|
||||
@@ -107,6 +112,8 @@ export const createWorkspaceInviteFactory =
|
||||
type CollectAndValidateWorkspaceTargetsFactoryDeps =
|
||||
CollectAndValidateCoreTargetsFactoryDeps & {
|
||||
getWorkspace: GetWorkspace
|
||||
getWorkspaceDomains: GetWorkspaceDomains
|
||||
findVerifiedEmailsByUserId: FindVerifiedEmailsByUserId
|
||||
}
|
||||
|
||||
export const collectAndValidateWorkspaceTargetsFactory =
|
||||
@@ -170,6 +177,27 @@ export const collectAndValidateWorkspaceTargetsFactory =
|
||||
'Guest users cannot be admins of workspaces'
|
||||
)
|
||||
}
|
||||
if (role !== Roles.Workspace.Guest && targetUser) {
|
||||
const domains = await deps.getWorkspaceDomains({ workspaceIds: [resourceId] })
|
||||
const verifiedDomains = domains.filter((domain) => domain?.verified)
|
||||
if (
|
||||
workspace &&
|
||||
verifiedDomains &&
|
||||
workspace?.domainBasedMembershipProtectionEnabled &&
|
||||
verifiedDomains.length > 0
|
||||
) {
|
||||
const domains = new Set<string>(verifiedDomains.map((vd) => vd.domain))
|
||||
const verifiedUserEmails = await deps.findVerifiedEmailsByUserId({
|
||||
userId: targetUser.id
|
||||
})
|
||||
const domainMatching = verifiedUserEmails.find((userEmail) =>
|
||||
domains.has(userEmail.email.split('@')[1])
|
||||
)
|
||||
if (!domainMatching) {
|
||||
throw new WorkspaceProtectedError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...baseTargets, { ...primaryWorkspaceResourceTarget, primary: true }]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { FindEmailsByUserId } from '@/modules/core/domain/userEmails/operations'
|
||||
import { EventBus } from '@/modules/shared/services/eventBus'
|
||||
import {
|
||||
GetWorkspaceWithDomains,
|
||||
UpsertWorkspaceRole
|
||||
} from '@/modules/workspaces/domain/operations'
|
||||
import {
|
||||
WorkspaceJoinNotAllowedError,
|
||||
WorkspaceNotDiscoverableError,
|
||||
WorkspaceNotJoinableError
|
||||
} from '@/modules/workspaces/errors/workspace'
|
||||
import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events'
|
||||
import { Roles } from '@speckle/shared'
|
||||
|
||||
export const joinWorkspaceFactory =
|
||||
({
|
||||
getUserEmails,
|
||||
getWorkspaceWithDomains,
|
||||
insertWorkspaceRole,
|
||||
emitWorkspaceEvent
|
||||
}: {
|
||||
getUserEmails: FindEmailsByUserId
|
||||
getWorkspaceWithDomains: GetWorkspaceWithDomains
|
||||
insertWorkspaceRole: UpsertWorkspaceRole
|
||||
emitWorkspaceEvent: EventBus['emit']
|
||||
}) =>
|
||||
async ({ userId, workspaceId }: { userId: string; workspaceId: string }) => {
|
||||
const userEmails = await getUserEmails({ userId })
|
||||
const workspace = await getWorkspaceWithDomains({ id: workspaceId })
|
||||
if (!workspace?.discoverabilityEnabled) throw new WorkspaceNotDiscoverableError()
|
||||
|
||||
const workspaceDomains = workspace.domains.filter((domain) => domain.verified)
|
||||
|
||||
if (!workspaceDomains.length) throw new WorkspaceNotJoinableError()
|
||||
|
||||
const matchingEmail = userEmails.find((userEmail) => {
|
||||
if (!userEmail.verified) return false
|
||||
return workspaceDomains
|
||||
.map((domain) => domain.domain)
|
||||
.includes(userEmail.email.split('@')[1])
|
||||
})
|
||||
|
||||
if (!matchingEmail) throw new WorkspaceJoinNotAllowedError()
|
||||
|
||||
const role = Roles.Workspace.Member
|
||||
await insertWorkspaceRole({ userId, workspaceId, role })
|
||||
await emitWorkspaceEvent({
|
||||
eventName: WorkspaceEvents.JoinedFromDiscovery,
|
||||
payload: { userId, workspaceId }
|
||||
})
|
||||
await emitWorkspaceEvent({
|
||||
eventName: WorkspaceEvents.RoleUpdated,
|
||||
payload: { userId, workspaceId, role }
|
||||
})
|
||||
}
|
||||
@@ -3,11 +3,18 @@ import {
|
||||
DeleteWorkspace,
|
||||
EmitWorkspaceEvent,
|
||||
GetWorkspace,
|
||||
StoreWorkspaceDomain,
|
||||
QueryAllWorkspaceProjects,
|
||||
UpsertWorkspace,
|
||||
UpsertWorkspaceRole
|
||||
UpsertWorkspaceRole,
|
||||
GetWorkspaceWithDomains,
|
||||
GetWorkspaceDomains
|
||||
} from '@/modules/workspaces/domain/operations'
|
||||
import { Workspace, WorkspaceAcl } from '@/modules/workspacesCore/domain/types'
|
||||
import {
|
||||
Workspace,
|
||||
WorkspaceAcl,
|
||||
WorkspaceDomain
|
||||
} from '@/modules/workspacesCore/domain/types'
|
||||
import { MaybeNullOrUndefined, Roles } from '@speckle/shared'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import {
|
||||
@@ -23,8 +30,11 @@ import {
|
||||
} from '@/modules/workspaces/domain/operations'
|
||||
import {
|
||||
WorkspaceAdminRequiredError,
|
||||
WorkspaceInvalidDescriptionError,
|
||||
WorkspaceNotFoundError
|
||||
WorkspaceDomainBlockedError,
|
||||
WorkspaceNotFoundError,
|
||||
WorkspaceProtectedError,
|
||||
WorkspaceUnverifiedDomainError,
|
||||
WorkspaceInvalidDescriptionError
|
||||
} from '@/modules/workspaces/errors/workspace'
|
||||
import {
|
||||
isUserLastWorkspaceAdmin,
|
||||
@@ -40,6 +50,11 @@ import {
|
||||
} from '@/modules/core/domain/tokens/types'
|
||||
import { ForbiddenError } from '@/modules/shared/errors'
|
||||
import { validateImageString } from '@/modules/workspaces/helpers/images'
|
||||
import {
|
||||
FindEmailsByUserId,
|
||||
FindVerifiedEmailsByUserId
|
||||
} from '@/modules/core/domain/userEmails/operations'
|
||||
import { blockedDomains } from '@/modules/workspaces/helpers/blockedDomains'
|
||||
import { DeleteAllResourceInvites } from '@/modules/serverinvites/domain/operations'
|
||||
import { WorkspaceInviteResourceType } from '@/modules/workspaces/domain/constants'
|
||||
import { ProjectInviteResourceType } from '@/modules/serverinvites/domain/constants'
|
||||
@@ -84,7 +99,9 @@ export const createWorkspaceFactory =
|
||||
...workspaceInput,
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
updatedAt: new Date(),
|
||||
domainBasedMembershipProtectionEnabled: false,
|
||||
discoverabilityEnabled: false
|
||||
}
|
||||
await upsertWorkspace({ workspace })
|
||||
// assign the creator as workspace administrator
|
||||
@@ -100,7 +117,7 @@ export const createWorkspaceFactory =
|
||||
payload: { ...workspace, createdByUserId: userId }
|
||||
})
|
||||
|
||||
return workspace
|
||||
return { ...workspace }
|
||||
}
|
||||
|
||||
type WorkspaceUpdateArgs = {
|
||||
@@ -110,6 +127,8 @@ type WorkspaceUpdateArgs = {
|
||||
description?: string | null
|
||||
logo?: string | null
|
||||
defaultLogoIndex?: number | null
|
||||
discoverabilityEnabled?: boolean | null
|
||||
domainBasedMembershipProtectionEnabled?: boolean | null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,12 +292,16 @@ export const getWorkspaceRoleFactory =
|
||||
export const updateWorkspaceRoleFactory =
|
||||
({
|
||||
getWorkspaceRoles,
|
||||
getWorkspaceWithDomains,
|
||||
findVerifiedEmailsByUserId,
|
||||
upsertWorkspaceRole,
|
||||
emitWorkspaceEvent,
|
||||
getStreams,
|
||||
grantStreamPermissions
|
||||
}: {
|
||||
getWorkspaceRoles: GetWorkspaceRoles
|
||||
getWorkspaceWithDomains: GetWorkspaceWithDomains
|
||||
findVerifiedEmailsByUserId: FindVerifiedEmailsByUserId
|
||||
upsertWorkspaceRole: UpsertWorkspaceRole
|
||||
emitWorkspaceEvent: EmitWorkspaceEvent
|
||||
// TODO: Create `core` domain and import type from there
|
||||
@@ -305,6 +328,26 @@ export const updateWorkspaceRoleFactory =
|
||||
throw new WorkspaceAdminRequiredError()
|
||||
}
|
||||
|
||||
if (role !== Roles.Workspace.Guest) {
|
||||
const workspace = await getWorkspaceWithDomains({ id: workspaceId })
|
||||
const verifiedDomains = workspace?.domains.filter((domain) => domain?.verified)
|
||||
if (
|
||||
workspace &&
|
||||
verifiedDomains &&
|
||||
workspace?.domainBasedMembershipProtectionEnabled &&
|
||||
verifiedDomains.length > 0
|
||||
) {
|
||||
const domains = new Set<string>(verifiedDomains.map((vd) => vd.domain))
|
||||
const verifiedUserEmails = await findVerifiedEmailsByUserId({ userId })
|
||||
const domainMatching = verifiedUserEmails.find((userEmail) =>
|
||||
domains.has(userEmail.email.split('@')[1])
|
||||
)
|
||||
if (!domainMatching) {
|
||||
throw new WorkspaceProtectedError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Perform upsert
|
||||
await upsertWorkspaceRole({ userId, workspaceId, role })
|
||||
|
||||
@@ -340,3 +383,86 @@ export const updateWorkspaceRoleFactory =
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const addDomainToWorkspaceFactory =
|
||||
({
|
||||
findEmailsByUserId,
|
||||
storeWorkspaceDomain,
|
||||
getWorkspace,
|
||||
upsertWorkspace,
|
||||
emitWorkspaceEvent,
|
||||
getDomains
|
||||
}: {
|
||||
findEmailsByUserId: FindEmailsByUserId
|
||||
storeWorkspaceDomain: StoreWorkspaceDomain
|
||||
getWorkspace: GetWorkspace
|
||||
upsertWorkspace: UpsertWorkspace
|
||||
getDomains: GetWorkspaceDomains
|
||||
emitWorkspaceEvent: EventBus['emit']
|
||||
}) =>
|
||||
async ({
|
||||
userId,
|
||||
domain,
|
||||
workspaceId
|
||||
}: {
|
||||
userId: string
|
||||
domain: string
|
||||
workspaceId: string
|
||||
}) => {
|
||||
// this function makes the assumption, that the user has a workspace admin role
|
||||
const sanitizedDomain = domain.toLowerCase().trim()
|
||||
if (blockedDomains.includes(sanitizedDomain))
|
||||
throw new WorkspaceDomainBlockedError()
|
||||
const userEmails = await findEmailsByUserId({
|
||||
userId
|
||||
})
|
||||
|
||||
const email = userEmails.find(
|
||||
(userEmail) =>
|
||||
userEmail.verified && userEmail.email.split('@')[1] === sanitizedDomain
|
||||
)
|
||||
|
||||
if (!email) {
|
||||
throw new WorkspaceUnverifiedDomainError()
|
||||
}
|
||||
// we're treating all user owned domains as verified, cause they have it in their verified emails list
|
||||
const verified = true
|
||||
|
||||
const workspaceWithRole = await getWorkspace({ workspaceId, userId })
|
||||
|
||||
if (!workspaceWithRole) throw new WorkspaceAdminRequiredError()
|
||||
|
||||
const { role, ...workspace } = workspaceWithRole
|
||||
|
||||
if (role !== Roles.Workspace.Admin) {
|
||||
throw new WorkspaceAdminRequiredError()
|
||||
}
|
||||
|
||||
const domains = await getDomains({ workspaceIds: [workspaceId] })
|
||||
|
||||
// idempotent operation
|
||||
if (domains.find((domain) => domain.domain === sanitizedDomain)) return
|
||||
|
||||
const workspaceDomain: WorkspaceDomain = {
|
||||
workspaceId,
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
domain: sanitizedDomain,
|
||||
createdByUserId: userId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
verified
|
||||
}
|
||||
|
||||
await storeWorkspaceDomain({ workspaceDomain })
|
||||
|
||||
if (domains.length === 0) {
|
||||
await upsertWorkspace({
|
||||
workspace: { ...workspace, discoverabilityEnabled: true }
|
||||
})
|
||||
}
|
||||
|
||||
await emitWorkspaceEvent({
|
||||
eventName: WorkspaceEvents.Updated,
|
||||
payload: workspace
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,10 +1,41 @@
|
||||
import { FindEmailsByUserId } from '@/modules/core/domain/userEmails/operations'
|
||||
import {
|
||||
GetUserDiscoverableWorkspaces,
|
||||
GetWorkspace,
|
||||
GetWorkspaceRolesForUser
|
||||
} from '@/modules/workspaces/domain/operations'
|
||||
import { Workspace } from '@/modules/workspacesCore/domain/types'
|
||||
import { chunk, isNull } from 'lodash'
|
||||
|
||||
type GetDiscoverableWorkspaceForUserArgs = {
|
||||
userId: string
|
||||
}
|
||||
|
||||
export const getDiscoverableWorkspacesForUserFactory =
|
||||
({
|
||||
findEmailsByUserId,
|
||||
getDiscoverableWorkspaces
|
||||
}: {
|
||||
findEmailsByUserId: FindEmailsByUserId
|
||||
getDiscoverableWorkspaces: GetUserDiscoverableWorkspaces
|
||||
}) =>
|
||||
async ({
|
||||
userId
|
||||
}: GetDiscoverableWorkspaceForUserArgs): Promise<
|
||||
Pick<Workspace, 'id' | 'name' | 'description' | 'logo' | 'defaultLogoIndex'>[]
|
||||
> => {
|
||||
const userEmails = await findEmailsByUserId({ userId })
|
||||
const userVerifiedDomains = userEmails
|
||||
.filter((email) => email.verified)
|
||||
.map((email) => email.email.split('@')[1])
|
||||
const workspaces = await getDiscoverableWorkspaces({
|
||||
domains: userVerifiedDomains,
|
||||
userId
|
||||
})
|
||||
|
||||
return workspaces
|
||||
}
|
||||
|
||||
type GetWorkspacesForUserArgs = {
|
||||
userId: string
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
grantStreamPermissions,
|
||||
revokeStreamPermissions
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import { findVerifiedEmailsByUserIdFactory } from '@/modules/core/repositories/userEmails'
|
||||
import { getStreams } from '@/modules/core/services/streams'
|
||||
import {
|
||||
findUserByTargetFactory,
|
||||
@@ -16,7 +17,9 @@ import {
|
||||
upsertWorkspaceFactory,
|
||||
upsertWorkspaceRoleFactory,
|
||||
deleteWorkspaceRoleFactory as dbDeleteWorkspaceRoleFactory,
|
||||
getWorkspaceFactory
|
||||
getWorkspaceFactory,
|
||||
getWorkspaceWithDomainsFactory,
|
||||
getWorkspaceDomainsFactory
|
||||
} from '@/modules/workspaces/repositories/workspaces'
|
||||
import {
|
||||
buildWorkspaceInviteEmailContentsFactory,
|
||||
@@ -26,7 +29,8 @@ import {
|
||||
import {
|
||||
createWorkspaceFactory,
|
||||
updateWorkspaceRoleFactory,
|
||||
deleteWorkspaceRoleFactory
|
||||
deleteWorkspaceRoleFactory,
|
||||
updateWorkspaceFactory
|
||||
} from '@/modules/workspaces/services/management'
|
||||
import { BasicTestUser } from '@/test/authHelper'
|
||||
import { CreateWorkspaceInviteMutationVariables } from '@/test/graphql/generated/graphql'
|
||||
@@ -44,6 +48,8 @@ export type BasicTestWorkspace = {
|
||||
name: string
|
||||
description?: string
|
||||
logo?: string
|
||||
discoverabilityEnabled?: boolean
|
||||
domainBasedMembershipProtectionEnabled?: boolean
|
||||
}
|
||||
|
||||
export const createTestWorkspace = async (
|
||||
@@ -56,7 +62,7 @@ export const createTestWorkspace = async (
|
||||
emitWorkspaceEvent: (...args) => getEventBus().emit(...args)
|
||||
})
|
||||
|
||||
const finalWorkspace = await createWorkspace({
|
||||
const newWorkspace = await createWorkspace({
|
||||
userId: owner.id,
|
||||
workspaceInput: {
|
||||
name: workspace.name,
|
||||
@@ -67,7 +73,31 @@ export const createTestWorkspace = async (
|
||||
userResourceAccessLimits: null
|
||||
})
|
||||
|
||||
workspace.id = finalWorkspace.id
|
||||
workspace.id = newWorkspace.id
|
||||
if (workspace.discoverabilityEnabled) {
|
||||
const updateWorkspace = updateWorkspaceFactory({
|
||||
getWorkspace: getWorkspaceFactory({ db }),
|
||||
upsertWorkspace: upsertWorkspaceFactory({ db }),
|
||||
emitWorkspaceEvent: (...args) => getEventBus().emit(...args)
|
||||
})
|
||||
|
||||
await updateWorkspace({
|
||||
workspaceId: newWorkspace.id,
|
||||
workspaceInput: {
|
||||
discoverabilityEnabled: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
await updateWorkspaceFactory({
|
||||
getWorkspace: getWorkspaceFactory({ db }),
|
||||
upsertWorkspace: upsertWorkspaceFactory({ db }),
|
||||
emitWorkspaceEvent: getEventBus().emit
|
||||
})({
|
||||
workspaceId: newWorkspace.id,
|
||||
workspaceInput: { domainBasedMembershipProtectionEnabled: true }
|
||||
})
|
||||
|
||||
workspace.ownerId = owner.id
|
||||
}
|
||||
|
||||
@@ -77,6 +107,8 @@ export const assignToWorkspace = async (
|
||||
role?: WorkspaceRoles
|
||||
) => {
|
||||
const updateWorkspaceRole = updateWorkspaceRoleFactory({
|
||||
getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }),
|
||||
findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db }),
|
||||
getWorkspaceRoles: getWorkspaceRolesFactory({ db }),
|
||||
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }),
|
||||
emitWorkspaceEvent: (...args) => getEventBus().emit(...args),
|
||||
@@ -136,7 +168,9 @@ export const createWorkspaceInviteDirectly = async (
|
||||
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
|
||||
collectAndValidateResourceTargets: collectAndValidateWorkspaceTargetsFactory({
|
||||
getStream,
|
||||
getWorkspace: getWorkspaceFactory({ db })
|
||||
getWorkspace: getWorkspaceFactory({ db }),
|
||||
getWorkspaceDomains: getWorkspaceDomainsFactory({ db }),
|
||||
findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db })
|
||||
}),
|
||||
buildInviteEmailContents: buildWorkspaceInviteEmailContentsFactory({
|
||||
getStream,
|
||||
|
||||
@@ -65,8 +65,23 @@ import {
|
||||
} from '@/modules/auth/tests/helpers/registration'
|
||||
import type { Express } from 'express'
|
||||
import { AllScopes } from '@/modules/core/helpers/mainConstants'
|
||||
import { getWorkspaceFactory } from '@/modules/workspaces/repositories/workspaces'
|
||||
import {
|
||||
getWorkspaceDomainsFactory,
|
||||
getWorkspaceFactory,
|
||||
storeWorkspaceDomainFactory,
|
||||
upsertWorkspaceFactory
|
||||
} from '@/modules/workspaces/repositories/workspaces'
|
||||
import { getStream } from '@/modules/core/repositories/streams'
|
||||
import { addDomainToWorkspaceFactory } from '@/modules/workspaces/services/management'
|
||||
import {
|
||||
createUserEmailFactory,
|
||||
findEmailsByUserIdFactory,
|
||||
updateUserEmailFactory
|
||||
} from '@/modules/core/repositories/userEmails'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { markUserEmailAsVerifiedFactory } from '@/modules/core/services/users/emailVerification'
|
||||
import { WorkspaceProtectedError } from '@/modules/workspaces/errors/workspace'
|
||||
import { createRandomPassword } from '@/modules/core/helpers/testHelpers'
|
||||
|
||||
enum InviteByTarget {
|
||||
Email = 'email',
|
||||
@@ -222,7 +237,8 @@ describe('Workspaces Invites GQL', () => {
|
||||
const myFirstWorkspace: BasicTestWorkspace = {
|
||||
name: 'My First Workspace',
|
||||
id: '',
|
||||
ownerId: ''
|
||||
ownerId: '',
|
||||
domainBasedMembershipProtectionEnabled: true
|
||||
}
|
||||
|
||||
const otherGuysWorkspace: BasicTestWorkspace = {
|
||||
@@ -236,6 +252,18 @@ describe('Workspaces Invites GQL', () => {
|
||||
app = ctx.app
|
||||
|
||||
await createTestUsers([me, otherGuy, myWorkspaceFriend])
|
||||
|
||||
const email = 'something@example.org'
|
||||
await createUserEmailFactory({ db })({
|
||||
userEmail: {
|
||||
email,
|
||||
primary: false,
|
||||
userId: me.id
|
||||
}
|
||||
})
|
||||
await markUserEmailAsVerifiedFactory({
|
||||
updateUserEmail: updateUserEmailFactory({ db })
|
||||
})({ email })
|
||||
await createTestWorkspaces([
|
||||
[myFirstWorkspace, me],
|
||||
[otherGuysWorkspace, otherGuy]
|
||||
@@ -319,6 +347,28 @@ describe('Workspaces Invites GQL', () => {
|
||||
expect(res.data?.workspaceMutations?.invites?.create).to.not.be.ok
|
||||
})
|
||||
|
||||
it('should throw an error when trying to invite a user as a memeber without email matching domain and domain protection is enabled', async () => {
|
||||
await addDomainToWorkspaceFactory({
|
||||
findEmailsByUserId: findEmailsByUserIdFactory({ db }),
|
||||
storeWorkspaceDomain: storeWorkspaceDomainFactory({ db }),
|
||||
getWorkspace: getWorkspaceFactory({ db }),
|
||||
upsertWorkspace: upsertWorkspaceFactory({ db }),
|
||||
emitWorkspaceEvent: getEventBus().emit,
|
||||
getDomains: getWorkspaceDomainsFactory({ db })
|
||||
})({ userId: me.id, workspaceId: myFirstWorkspace.id, domain: 'example.org' })
|
||||
|
||||
const res = await gqlHelpers.createInvite({
|
||||
workspaceId: myFirstWorkspace.id,
|
||||
input: {
|
||||
userId: otherGuy.id,
|
||||
role: WorkspaceRole.Member
|
||||
}
|
||||
})
|
||||
|
||||
expect(res).to.haveGraphQLErrors(new WorkspaceProtectedError().message)
|
||||
expect(res.data?.workspaceMutations?.invites?.create).to.not.be.ok
|
||||
})
|
||||
|
||||
it('batch inviting fails if more than 10 invites', async () => {
|
||||
const res = await gqlHelpers.batchCreateInvites({
|
||||
workspaceId: myFirstWorkspace.id,
|
||||
@@ -400,55 +450,88 @@ describe('Workspaces Invites GQL', () => {
|
||||
expect(sendEmailInvocations.args).to.have.lengthOf(count)
|
||||
})
|
||||
|
||||
itEach(
|
||||
[InviteByTarget.Email, InviteByTarget.Id],
|
||||
(type) => `works when inviting user by ${type}`,
|
||||
async (type) => {
|
||||
const sendEmailInvocations = EmailSendingServiceMock.hijackFunction(
|
||||
'sendEmail',
|
||||
async () => true
|
||||
)
|
||||
it('works when inviting user by id', async () => {
|
||||
const sendEmailInvocations = EmailSendingServiceMock.hijackFunction(
|
||||
'sendEmail',
|
||||
async () => true
|
||||
)
|
||||
|
||||
const randomUnregisteredEmail = 'randomunregisteredguy@email.com'
|
||||
|
||||
const res = await gqlHelpers.createInvite({
|
||||
workspaceId: myFirstWorkspace.id,
|
||||
input: {
|
||||
...(type === InviteByTarget.Email
|
||||
? { email: randomUnregisteredEmail }
|
||||
: {}),
|
||||
...(type === InviteByTarget.Id ? { userId: otherGuy.id } : {}),
|
||||
role: WorkspaceRole.Member
|
||||
}
|
||||
})
|
||||
|
||||
expect(res).to.not.haveGraphQLErrors()
|
||||
expect(res.data?.workspaceMutations?.invites?.create).to.be.ok
|
||||
|
||||
const workspace = res.data!.workspaceMutations!.invites!.create
|
||||
expect(workspace.invitedTeam).to.have.length(1)
|
||||
expect(workspace.invitedTeam![0].invitedBy.id).to.equal(me.id)
|
||||
expect(workspace.invitedTeam![0].token).to.be.not.ok
|
||||
|
||||
if (type === InviteByTarget.Email) {
|
||||
expect(workspace.invitedTeam![0].user).to.be.not.ok
|
||||
expect(workspace.invitedTeam![0].title).to.equal(randomUnregisteredEmail)
|
||||
} else {
|
||||
expect(workspace.invitedTeam![0].user?.id).to.equal(otherGuy.id)
|
||||
const randomUnregisteredEmail = `${createRandomPassword()}@example.org`
|
||||
await createUserEmailFactory({ db })({
|
||||
userEmail: {
|
||||
userId: otherGuy.id,
|
||||
email: randomUnregisteredEmail
|
||||
}
|
||||
})
|
||||
await markUserEmailAsVerifiedFactory({
|
||||
updateUserEmail: updateUserEmailFactory({ db })
|
||||
})({
|
||||
email: randomUnregisteredEmail
|
||||
})
|
||||
|
||||
expect(sendEmailInvocations.args).to.have.lengthOf(1)
|
||||
const emailParams = sendEmailInvocations.args[0][0]
|
||||
expect(emailParams).to.be.ok
|
||||
expect(emailParams.to).to.eq(
|
||||
type === InviteByTarget.Id ? otherGuy.email : randomUnregisteredEmail
|
||||
)
|
||||
expect(emailParams.subject).to.be.ok
|
||||
const res = await gqlHelpers.createInvite({
|
||||
workspaceId: myFirstWorkspace.id,
|
||||
input: {
|
||||
userId: otherGuy.id,
|
||||
role: WorkspaceRole.Member
|
||||
}
|
||||
})
|
||||
|
||||
// Validate that invite exists
|
||||
await validateInviteExistanceFromEmail(emailParams)
|
||||
}
|
||||
)
|
||||
expect(res).to.not.haveGraphQLErrors()
|
||||
expect(res.data?.workspaceMutations?.invites?.create).to.be.ok
|
||||
|
||||
const workspace = res.data!.workspaceMutations!.invites!.create
|
||||
expect(workspace.invitedTeam).to.have.length(1)
|
||||
expect(workspace.invitedTeam![0].invitedBy.id).to.equal(me.id)
|
||||
expect(workspace.invitedTeam![0].token).to.be.not.ok
|
||||
|
||||
expect(workspace.invitedTeam![0].user?.id).to.equal(otherGuy.id)
|
||||
|
||||
expect(sendEmailInvocations.args).to.have.lengthOf(1)
|
||||
const emailParams = sendEmailInvocations.args[0][0]
|
||||
expect(emailParams).to.be.ok
|
||||
expect(emailParams.to).to.eq(otherGuy.email)
|
||||
expect(emailParams.subject).to.be.ok
|
||||
|
||||
// Validate that invite exists
|
||||
await validateInviteExistanceFromEmail(emailParams)
|
||||
})
|
||||
it('works when inviting user by email', async () => {
|
||||
const sendEmailInvocations = EmailSendingServiceMock.hijackFunction(
|
||||
'sendEmail',
|
||||
async () => true
|
||||
)
|
||||
|
||||
const randomUnregisteredEmail = `${createRandomPassword()}@example.org`
|
||||
|
||||
const res = await gqlHelpers.createInvite({
|
||||
workspaceId: myFirstWorkspace.id,
|
||||
input: {
|
||||
email: randomUnregisteredEmail,
|
||||
role: WorkspaceRole.Member
|
||||
}
|
||||
})
|
||||
|
||||
expect(res).to.not.haveGraphQLErrors()
|
||||
expect(res.data?.workspaceMutations?.invites?.create).to.be.ok
|
||||
|
||||
const workspace = res.data!.workspaceMutations!.invites!.create
|
||||
expect(workspace.invitedTeam).to.have.length(1)
|
||||
expect(workspace.invitedTeam![0].invitedBy.id).to.equal(me.id)
|
||||
expect(workspace.invitedTeam![0].token).to.be.not.ok
|
||||
|
||||
expect(workspace.invitedTeam![0].user).to.be.not.ok
|
||||
expect(workspace.invitedTeam![0].title).to.equal(randomUnregisteredEmail)
|
||||
|
||||
expect(sendEmailInvocations.args).to.have.lengthOf(1)
|
||||
const emailParams = sendEmailInvocations.args[0][0]
|
||||
expect(emailParams).to.be.ok
|
||||
expect(emailParams.to).to.eq(randomUnregisteredEmail)
|
||||
expect(emailParams.subject).to.be.ok
|
||||
|
||||
// Validate that invite exists
|
||||
await validateInviteExistanceFromEmail(emailParams)
|
||||
})
|
||||
|
||||
it("doesn't work if inviting to a workspace that the token doesn't have access to", async () => {
|
||||
const res = await gqlHelpers.createInvite(
|
||||
|
||||
@@ -6,7 +6,10 @@ import {
|
||||
upsertWorkspaceRoleFactory,
|
||||
getWorkspaceRolesFactory,
|
||||
getWorkspaceRolesForUserFactory,
|
||||
deleteWorkspaceFactory
|
||||
deleteWorkspaceFactory,
|
||||
storeWorkspaceDomainFactory,
|
||||
getUserDiscoverableWorkspacesFactory,
|
||||
getWorkspaceWithDomainsFactory
|
||||
} from '@/modules/workspaces/repositories/workspaces'
|
||||
import db from '@/db/knex'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
@@ -18,6 +21,13 @@ import {
|
||||
BasicTestWorkspace,
|
||||
createTestWorkspace
|
||||
} from '@/modules/workspaces/tests/helpers/creation'
|
||||
import {
|
||||
createUserEmailFactory,
|
||||
updateUserEmailFactory
|
||||
} from '@/modules/core/repositories/userEmails'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { createRandomPassword } from '@/modules/core/helpers/testHelpers'
|
||||
import { truncateTables } from '@/test/hooks'
|
||||
|
||||
const getWorkspace = getWorkspaceFactory({ db })
|
||||
const upsertWorkspace = upsertWorkspaceFactory({ db })
|
||||
@@ -27,6 +37,10 @@ const getWorkspaceRoles = getWorkspaceRolesFactory({ db })
|
||||
const getWorkspaceRoleForUser = getWorkspaceRoleForUserFactory({ db })
|
||||
const getWorkspaceRolesForUser = getWorkspaceRolesForUserFactory({ db })
|
||||
const upsertWorkspaceRole = upsertWorkspaceRoleFactory({ db })
|
||||
const storeWorkspaceDomain = storeWorkspaceDomainFactory({ db })
|
||||
const createUserEmail = createUserEmailFactory({ db })
|
||||
const updateUserEmail = updateUserEmailFactory({ db })
|
||||
const getUserDiscoverableWorkspaces = getUserDiscoverableWorkspacesFactory({ db })
|
||||
|
||||
const createAndStoreTestUser = async (): Promise<BasicTestUser> => {
|
||||
const testId = cryptoRandomString({ length: 6 })
|
||||
@@ -44,15 +58,20 @@ const createAndStoreTestUser = async (): Promise<BasicTestUser> => {
|
||||
return userRecord
|
||||
}
|
||||
|
||||
const createAndStoreTestWorkspace = async (): Promise<Workspace> => {
|
||||
const workspace: Workspace = {
|
||||
const createAndStoreTestWorkspace = async (
|
||||
workspaceOverrides: Partial<Workspace> = {}
|
||||
) => {
|
||||
const workspace: Omit<Workspace, 'domains'> = {
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
name: cryptoRandomString({ length: 10 }),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
description: null,
|
||||
logo: null,
|
||||
defaultLogoIndex: 0
|
||||
domainBasedMembershipProtectionEnabled: false,
|
||||
discoverabilityEnabled: false,
|
||||
defaultLogoIndex: 0,
|
||||
...workspaceOverrides
|
||||
}
|
||||
|
||||
await upsertWorkspace({ workspace })
|
||||
@@ -77,7 +96,7 @@ describe('Workspace repositories', () => {
|
||||
const storedWorkspace = await getWorkspace({ workspaceId: testWorkspace.id })
|
||||
expect(storedWorkspace).to.deep.equal(testWorkspace)
|
||||
|
||||
const modifiedTestWorkspace: Workspace = {
|
||||
const modifiedTestWorkspace: Omit<Workspace, 'domains'> = {
|
||||
...testWorkspace,
|
||||
description: 'now im adding a description to the workspace'
|
||||
}
|
||||
@@ -282,4 +301,343 @@ describe('Workspace repositories', () => {
|
||||
await expectToThrow(() => upsertWorkspaceRole(role))
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDiscoverableWorkspacesForUserFactory creates a function, that', () => {
|
||||
afterEach(async () => {
|
||||
await truncateTables(['workspaces'])
|
||||
})
|
||||
|
||||
it('should return only one workspace where multiple emails match', async () => {
|
||||
const user = await createAndStoreTestUser()
|
||||
await updateUserEmail({
|
||||
query: {
|
||||
email: user.email
|
||||
},
|
||||
update: {
|
||||
verified: true
|
||||
}
|
||||
})
|
||||
await createUserEmail({
|
||||
userEmail: {
|
||||
email: 'john-speckle@speckle.systems',
|
||||
userId: user.id
|
||||
}
|
||||
})
|
||||
await updateUserEmail({
|
||||
query: {
|
||||
email: 'john-speckle@speckle.systems'
|
||||
},
|
||||
update: {
|
||||
verified: true
|
||||
}
|
||||
})
|
||||
|
||||
const workspace = await createAndStoreTestWorkspace({
|
||||
discoverabilityEnabled: true
|
||||
})
|
||||
await storeWorkspaceDomain({
|
||||
workspaceDomain: {
|
||||
id: cryptoRandomString({ length: 6 }),
|
||||
domain: 'example.org',
|
||||
workspaceId: workspace.id,
|
||||
verified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
createdByUserId: user.id
|
||||
}
|
||||
})
|
||||
await storeWorkspaceDomain({
|
||||
workspaceDomain: {
|
||||
id: cryptoRandomString({ length: 6 }),
|
||||
domain: 'speckle.systems',
|
||||
workspaceId: workspace.id,
|
||||
verified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
createdByUserId: user.id
|
||||
}
|
||||
})
|
||||
|
||||
const workspaces = await getUserDiscoverableWorkspaces({
|
||||
domains: ['example.org', 'speckle.systems'],
|
||||
userId: user.id
|
||||
})
|
||||
|
||||
expect(workspaces.length).to.equal(1)
|
||||
})
|
||||
|
||||
it('should not return matches if the user email is not verified', async () => {
|
||||
const user = await createAndStoreTestUser()
|
||||
const workspace = await createAndStoreTestWorkspace({
|
||||
discoverabilityEnabled: true
|
||||
})
|
||||
await storeWorkspaceDomain({
|
||||
workspaceDomain: {
|
||||
id: cryptoRandomString({ length: 6 }),
|
||||
domain: 'example.org',
|
||||
workspaceId: workspace.id,
|
||||
verified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
createdByUserId: user.id
|
||||
}
|
||||
})
|
||||
|
||||
const workspaces = await getUserDiscoverableWorkspaces({
|
||||
domains: [],
|
||||
userId: user.id
|
||||
})
|
||||
|
||||
expect(workspaces.length).to.equal(0)
|
||||
})
|
||||
|
||||
it('should not return workspaces if the workspace email is not verified', async () => {
|
||||
const user = await createAndStoreTestUser()
|
||||
await updateUserEmail({
|
||||
query: {
|
||||
email: user.email
|
||||
},
|
||||
update: {
|
||||
verified: true
|
||||
}
|
||||
})
|
||||
|
||||
const workspace = await createAndStoreTestWorkspace({
|
||||
discoverabilityEnabled: true
|
||||
})
|
||||
await storeWorkspaceDomain({
|
||||
workspaceDomain: {
|
||||
id: cryptoRandomString({ length: 6 }),
|
||||
domain: 'example.org',
|
||||
workspaceId: workspace.id,
|
||||
verified: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
createdByUserId: user.id
|
||||
}
|
||||
})
|
||||
|
||||
const workspaces = await getUserDiscoverableWorkspaces({
|
||||
domains: ['example.org'],
|
||||
userId: user.id
|
||||
})
|
||||
|
||||
expect(workspaces.length).to.equal(0)
|
||||
})
|
||||
|
||||
it('should return multiple workspaces matching the user email', async () => {
|
||||
const user = await createAndStoreTestUser()
|
||||
await updateUserEmail({
|
||||
query: {
|
||||
email: user.email
|
||||
},
|
||||
update: {
|
||||
verified: true
|
||||
}
|
||||
})
|
||||
await createUserEmail({
|
||||
userEmail: {
|
||||
email: 'john-speckle@speckle.systems',
|
||||
userId: user.id
|
||||
}
|
||||
})
|
||||
await updateUserEmail({
|
||||
query: {
|
||||
email: 'john-speckle@speckle.systems'
|
||||
},
|
||||
update: {
|
||||
verified: true
|
||||
}
|
||||
})
|
||||
|
||||
const workspaceA = await createAndStoreTestWorkspace({
|
||||
discoverabilityEnabled: true
|
||||
})
|
||||
await storeWorkspaceDomain({
|
||||
workspaceDomain: {
|
||||
id: cryptoRandomString({ length: 6 }),
|
||||
domain: 'example.org',
|
||||
workspaceId: workspaceA.id,
|
||||
verified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
createdByUserId: user.id
|
||||
}
|
||||
})
|
||||
const workspaceB = await createAndStoreTestWorkspace({
|
||||
discoverabilityEnabled: true
|
||||
})
|
||||
await storeWorkspaceDomain({
|
||||
workspaceDomain: {
|
||||
id: cryptoRandomString({ length: 6 }),
|
||||
domain: 'example.org',
|
||||
workspaceId: workspaceB.id,
|
||||
verified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
createdByUserId: user.id
|
||||
}
|
||||
})
|
||||
|
||||
const workspaces = await getUserDiscoverableWorkspaces({
|
||||
domains: ['example.org'],
|
||||
userId: user.id
|
||||
})
|
||||
|
||||
expect(workspaces.length).to.equal(2)
|
||||
})
|
||||
|
||||
it('should not return workspaces the user is already a member of', async () => {
|
||||
const user = await createAndStoreTestUser()
|
||||
await updateUserEmail({
|
||||
query: {
|
||||
email: user.email
|
||||
},
|
||||
update: {
|
||||
verified: true
|
||||
}
|
||||
})
|
||||
|
||||
const workspace = await createAndStoreTestWorkspace({
|
||||
discoverabilityEnabled: true
|
||||
})
|
||||
await storeWorkspaceDomain({
|
||||
workspaceDomain: {
|
||||
id: cryptoRandomString({ length: 6 }),
|
||||
domain: 'example.org',
|
||||
workspaceId: workspace.id,
|
||||
verified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
createdByUserId: user.id
|
||||
}
|
||||
})
|
||||
await upsertWorkspaceRole({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
role: Roles.Workspace.Member
|
||||
})
|
||||
|
||||
const workspaces = await getUserDiscoverableWorkspaces({
|
||||
domains: ['example.org'],
|
||||
userId: user.id
|
||||
})
|
||||
|
||||
expect(workspaces.length).to.equal(0)
|
||||
})
|
||||
|
||||
it('should not return workspaces that are not discoverable', async () => {
|
||||
const user = await createAndStoreTestUser()
|
||||
await updateUserEmail({
|
||||
query: {
|
||||
email: user.email
|
||||
},
|
||||
update: {
|
||||
verified: true
|
||||
}
|
||||
})
|
||||
|
||||
const workspace = await createAndStoreTestWorkspace()
|
||||
await storeWorkspaceDomain({
|
||||
workspaceDomain: {
|
||||
id: cryptoRandomString({ length: 6 }),
|
||||
domain: 'example.org',
|
||||
workspaceId: workspace.id,
|
||||
verified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
createdByUserId: user.id
|
||||
}
|
||||
})
|
||||
|
||||
const workspaces = await getUserDiscoverableWorkspaces({
|
||||
domains: ['example.org'],
|
||||
userId: user.id
|
||||
})
|
||||
|
||||
expect(workspaces.length).to.equal(0)
|
||||
})
|
||||
|
||||
it('should return discoverable workspaces that already have members', async () => {
|
||||
const user = await createAndStoreTestUser()
|
||||
await updateUserEmail({
|
||||
query: {
|
||||
email: user.email
|
||||
},
|
||||
update: {
|
||||
verified: true
|
||||
}
|
||||
})
|
||||
|
||||
const problemChild = await createAndStoreTestUser()
|
||||
await updateUserEmail({
|
||||
query: {
|
||||
email: problemChild.email
|
||||
},
|
||||
update: {
|
||||
verified: true
|
||||
}
|
||||
})
|
||||
|
||||
const workspace = await createAndStoreTestWorkspace({
|
||||
discoverabilityEnabled: true
|
||||
})
|
||||
await storeWorkspaceDomain({
|
||||
workspaceDomain: {
|
||||
id: cryptoRandomString({ length: 6 }),
|
||||
domain: 'example.org',
|
||||
workspaceId: workspace.id,
|
||||
verified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
createdByUserId: user.id
|
||||
}
|
||||
})
|
||||
await upsertWorkspaceRole({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
role: Roles.Workspace.Member
|
||||
})
|
||||
|
||||
const workspaces = await getUserDiscoverableWorkspaces({
|
||||
domains: ['example.org'],
|
||||
userId: problemChild.id
|
||||
})
|
||||
|
||||
expect(workspaces.length).to.equal(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getWorkspaceDomainsFactory creates a function, that', () => {
|
||||
it('returns a workspace with domains', async () => {
|
||||
const user = {
|
||||
id: createRandomPassword(),
|
||||
name: createRandomPassword(),
|
||||
email: createRandomPassword()
|
||||
}
|
||||
await createTestUser(user)
|
||||
const workspace = {
|
||||
id: createRandomPassword(),
|
||||
name: 'my workspace',
|
||||
ownerId: user.id
|
||||
}
|
||||
await createTestWorkspace(workspace, user)
|
||||
|
||||
await storeWorkspaceDomainFactory({ db })({
|
||||
workspaceDomain: {
|
||||
id: createRandomPassword(),
|
||||
domain: 'example.org',
|
||||
verified: true,
|
||||
workspaceId: workspace.id,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
createdByUserId: user.id
|
||||
}
|
||||
})
|
||||
const workspaceWithDomains = await getWorkspaceWithDomainsFactory({ db })({
|
||||
id: workspace.id
|
||||
})
|
||||
expect(workspaceWithDomains?.domains.length).to.eq(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -357,17 +357,22 @@ describe('Workspaces GQL CRUD', () => {
|
||||
const workspaceCreateResult = await apollo.execute(CreateWorkspaceDocument, {
|
||||
input: { name }
|
||||
})
|
||||
expect(workspaceCreateResult).to.not.haveGraphQLErrors()
|
||||
|
||||
const id = workspaceCreateResult.data?.workspaceMutations.create.id
|
||||
if (!id) throw new Error('This should have succeeded')
|
||||
|
||||
await apollo.execute(UpdateWorkspaceRoleDocument, {
|
||||
input: {
|
||||
userId: testMemberUser.id,
|
||||
workspaceId: id,
|
||||
role: Roles.Workspace.Admin
|
||||
const updateWorkspaceRole = await apollo.execute(
|
||||
UpdateWorkspaceRoleDocument,
|
||||
{
|
||||
input: {
|
||||
userId: testMemberUser.id,
|
||||
workspaceId: id,
|
||||
role: Roles.Workspace.Admin
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
expect(updateWorkspaceRole).to.not.haveGraphQLErrors()
|
||||
|
||||
let userWorkspaces = await apollo.execute(GetActiveUserWorkspacesDocument, {})
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { UserEmail } from '@/modules/core/domain/userEmails/types'
|
||||
import { createRandomPassword } from '@/modules/core/helpers/testHelpers'
|
||||
import {
|
||||
WorkspaceJoinNotAllowedError,
|
||||
WorkspaceNotDiscoverableError,
|
||||
WorkspaceNotJoinableError
|
||||
} from '@/modules/workspaces/errors/workspace'
|
||||
import { joinWorkspaceFactory } from '@/modules/workspaces/services/join'
|
||||
import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events'
|
||||
import {
|
||||
WorkspaceAcl,
|
||||
WorkspaceDomain,
|
||||
WorkspaceWithDomains
|
||||
} from '@/modules/workspacesCore/domain/types'
|
||||
import { expectToThrow } from '@/test/assertionHelper'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { expect } from 'chai'
|
||||
import { assign } from 'lodash'
|
||||
|
||||
const createTestWorkspaceWithDomains = (
|
||||
arg?: Partial<WorkspaceWithDomains> | undefined
|
||||
): WorkspaceWithDomains => {
|
||||
const workspace: WorkspaceWithDomains = {
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: createRandomPassword(),
|
||||
description: createRandomPassword(),
|
||||
id: createRandomPassword(),
|
||||
logo: null,
|
||||
domains: [],
|
||||
discoverabilityEnabled: false,
|
||||
domainBasedMembershipProtectionEnabled: false,
|
||||
defaultLogoIndex: 0
|
||||
}
|
||||
if (arg) assign(workspace, arg)
|
||||
return workspace
|
||||
}
|
||||
|
||||
describe('Workspace join services', () => {
|
||||
describe('joinWorkspaceFactory returns a function, that', () => {
|
||||
it('throws an error if the workspace is not discoverable', async () => {
|
||||
const userId = createRandomPassword()
|
||||
const workspaceId = createRandomPassword()
|
||||
const error = await expectToThrow(async () => {
|
||||
await joinWorkspaceFactory({
|
||||
getUserEmails: async () => [],
|
||||
getWorkspaceWithDomains: async () => {
|
||||
return createTestWorkspaceWithDomains()
|
||||
},
|
||||
insertWorkspaceRole: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
emitWorkspaceEvent: async () => {
|
||||
expect.fail()
|
||||
}
|
||||
})({ userId, workspaceId })
|
||||
})
|
||||
expect(error.message).to.be.equal(new WorkspaceNotDiscoverableError().message)
|
||||
})
|
||||
it('throws an error if the workspace has no verified domains', async () => {
|
||||
const userId = createRandomPassword()
|
||||
const workspaceId = createRandomPassword()
|
||||
const error = await expectToThrow(async () => {
|
||||
await joinWorkspaceFactory({
|
||||
getUserEmails: async () => [],
|
||||
getWorkspaceWithDomains: async () => {
|
||||
return createTestWorkspaceWithDomains({
|
||||
discoverabilityEnabled: true,
|
||||
domains: [{ domain: 'example.com', verified: false }] as WorkspaceDomain[]
|
||||
})
|
||||
},
|
||||
insertWorkspaceRole: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
emitWorkspaceEvent: async () => {
|
||||
expect.fail()
|
||||
}
|
||||
})({ userId, workspaceId })
|
||||
})
|
||||
expect(error.message).to.be.equal(new WorkspaceNotJoinableError().message)
|
||||
})
|
||||
it('throws an error if the user has no verified email matching the domains', async () => {
|
||||
const userId = createRandomPassword()
|
||||
const workspaceId = createRandomPassword()
|
||||
const error = await expectToThrow(async () => {
|
||||
await joinWorkspaceFactory({
|
||||
getUserEmails: async () =>
|
||||
[{ email: 'test@example.com', verified: false }] as UserEmail[],
|
||||
getWorkspaceWithDomains: async () => {
|
||||
return createTestWorkspaceWithDomains({
|
||||
discoverabilityEnabled: true,
|
||||
domains: [{ domain: 'example.com', verified: true }] as WorkspaceDomain[]
|
||||
})
|
||||
},
|
||||
insertWorkspaceRole: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
emitWorkspaceEvent: async () => {
|
||||
expect.fail()
|
||||
}
|
||||
})({ userId, workspaceId })
|
||||
})
|
||||
expect(error.message).to.be.equal(new WorkspaceJoinNotAllowedError().message)
|
||||
})
|
||||
it('creates a workspace member role and emits workspace events', async () => {
|
||||
const userId = createRandomPassword()
|
||||
const workspaceId = createRandomPassword()
|
||||
let storedWorkspaceRole: WorkspaceAcl | undefined = undefined
|
||||
const firedEvents: string[] = []
|
||||
await joinWorkspaceFactory({
|
||||
getUserEmails: async () =>
|
||||
[{ email: 'test@example.com', verified: true }] as UserEmail[],
|
||||
getWorkspaceWithDomains: async () => {
|
||||
return createTestWorkspaceWithDomains({
|
||||
discoverabilityEnabled: true,
|
||||
domains: [{ domain: 'example.com', verified: true }] as WorkspaceDomain[]
|
||||
})
|
||||
},
|
||||
insertWorkspaceRole: async (workspaceRole) => {
|
||||
storedWorkspaceRole = workspaceRole
|
||||
},
|
||||
emitWorkspaceEvent: async ({ eventName }) => {
|
||||
firedEvents.push(eventName)
|
||||
return []
|
||||
}
|
||||
})({ userId, workspaceId })
|
||||
expect(storedWorkspaceRole).deep.equal({
|
||||
userId,
|
||||
workspaceId,
|
||||
role: Roles.Workspace.Member
|
||||
})
|
||||
expect(firedEvents).deep.equal([
|
||||
WorkspaceEvents.JoinedFromDiscovery,
|
||||
WorkspaceEvents.RoleUpdated
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,10 @@
|
||||
import { Workspace, WorkspaceAcl } from '@/modules/workspacesCore/domain/types'
|
||||
import {
|
||||
Workspace,
|
||||
WorkspaceAcl,
|
||||
WorkspaceDomain
|
||||
} from '@/modules/workspacesCore/domain/types'
|
||||
import {
|
||||
addDomainToWorkspaceFactory,
|
||||
createWorkspaceFactory,
|
||||
deleteWorkspaceRoleFactory,
|
||||
updateWorkspaceRoleFactory
|
||||
@@ -10,9 +15,21 @@ import cryptoRandomString from 'crypto-random-string'
|
||||
import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events'
|
||||
import { StreamAclRecord, StreamRecord } from '@/modules/core/helpers/types'
|
||||
import { expectToThrow } from '@/test/assertionHelper'
|
||||
import { createRandomPassword } from '@/modules/core/helpers/testHelpers'
|
||||
import {
|
||||
WorkspaceAdminRequiredError,
|
||||
WorkspaceDomainBlockedError,
|
||||
WorkspaceProtectedError,
|
||||
WorkspaceUnverifiedDomainError
|
||||
} from '@/modules/workspaces/errors/workspace'
|
||||
import { UserEmail } from '@/modules/core/domain/userEmails/types'
|
||||
import { omit } from 'lodash'
|
||||
import { GetWorkspaceWithDomains } from '@/modules/workspaces/domain/operations'
|
||||
import { FindVerifiedEmailsByUserId } from '@/modules/core/domain/userEmails/operations'
|
||||
import { EventNames } from '@/modules/shared/services/eventBus'
|
||||
|
||||
type WorkspaceTestContext = {
|
||||
storedWorkspaces: Workspace[]
|
||||
storedWorkspaces: Omit<Workspace, 'domains'>[]
|
||||
storedRoles: WorkspaceAcl[]
|
||||
eventData: {
|
||||
isCalled: boolean
|
||||
@@ -22,7 +39,7 @@ type WorkspaceTestContext = {
|
||||
}
|
||||
|
||||
const buildCreateWorkspaceWithTestContext = (
|
||||
dependecyOverrides: Partial<Parameters<typeof createWorkspaceFactory>[0]> = {}
|
||||
dependencyOverrides: Partial<Parameters<typeof createWorkspaceFactory>[0]> = {}
|
||||
) => {
|
||||
const context: WorkspaceTestContext = {
|
||||
storedWorkspaces: [],
|
||||
@@ -35,7 +52,11 @@ const buildCreateWorkspaceWithTestContext = (
|
||||
}
|
||||
|
||||
const deps: Parameters<typeof createWorkspaceFactory>[0] = {
|
||||
upsertWorkspace: async ({ workspace }: { workspace: Workspace }) => {
|
||||
upsertWorkspace: async ({
|
||||
workspace
|
||||
}: {
|
||||
workspace: Omit<Workspace, 'domains'>
|
||||
}) => {
|
||||
context.storedWorkspaces.push(workspace)
|
||||
},
|
||||
upsertWorkspaceRole: async (workspaceAcl: WorkspaceAcl) => {
|
||||
@@ -47,7 +68,7 @@ const buildCreateWorkspaceWithTestContext = (
|
||||
context.eventData.payload = payload
|
||||
return []
|
||||
},
|
||||
...dependecyOverrides
|
||||
...dependencyOverrides
|
||||
}
|
||||
|
||||
const createWorkspace = createWorkspaceFactory(deps)
|
||||
@@ -80,7 +101,7 @@ describe('Workspace services', () => {
|
||||
})
|
||||
|
||||
expect(context.storedWorkspaces.length).to.equal(1)
|
||||
expect(context.storedWorkspaces[0]).to.deep.equal(workspace)
|
||||
expect(context.storedWorkspaces[0]).to.deep.equal(omit(workspace, 'domains'))
|
||||
})
|
||||
it('makes the workspace creator becomes a workspace:admin', async () => {
|
||||
const { context, createWorkspace } = buildCreateWorkspaceWithTestContext()
|
||||
@@ -129,11 +150,13 @@ type WorkspaceRoleTestContext = {
|
||||
eventName: string
|
||||
payload: unknown
|
||||
}
|
||||
workspace: Partial<Workspace & { domains: Partial<WorkspaceDomain[]> }>
|
||||
}
|
||||
|
||||
const getDefaultWorkspaceRoleTestContext = (): WorkspaceRoleTestContext => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
return {
|
||||
workspaceId: cryptoRandomString({ length: 10 }),
|
||||
workspaceId,
|
||||
workspaceRoles: [],
|
||||
workspaceProjects: [],
|
||||
workspaceProjectRoles: [],
|
||||
@@ -141,6 +164,10 @@ const getDefaultWorkspaceRoleTestContext = (): WorkspaceRoleTestContext => {
|
||||
isCalled: false,
|
||||
eventName: '',
|
||||
payload: {}
|
||||
},
|
||||
workspace: {
|
||||
id: workspaceId,
|
||||
domains: []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,6 +237,9 @@ const buildUpdateWorkspaceRoleAndTestContext = (
|
||||
|
||||
const deps: Parameters<typeof updateWorkspaceRoleFactory>[0] = {
|
||||
getWorkspaceRoles: async () => context.workspaceRoles,
|
||||
getWorkspaceWithDomains: async () =>
|
||||
context.workspace as unknown as Workspace & { domains: WorkspaceDomain[] },
|
||||
findVerifiedEmailsByUserId: async () => [],
|
||||
upsertWorkspaceRole: async (role) => {
|
||||
context.workspaceRoles = context.workspaceRoles.filter(
|
||||
(acl) => acl.userId !== role.userId
|
||||
@@ -359,6 +389,57 @@ describe('Workspace role services', () => {
|
||||
updateWorkspaceRole({ ...role, role: Roles.Workspace.Member })
|
||||
)
|
||||
})
|
||||
it('throws if attempting to set user role to more than GUEST and workspace domain protection is enabled and user has not an email matching a workspace domain', async () => {
|
||||
const adminId = cryptoRandomString({ length: 10 })
|
||||
const guestId = cryptoRandomString({ length: 10 })
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const roleAdmin: WorkspaceAcl = {
|
||||
userId: adminId,
|
||||
workspaceId,
|
||||
role: Roles.Workspace.Admin
|
||||
}
|
||||
const roleGuest: WorkspaceAcl = {
|
||||
userId: guestId,
|
||||
workspaceId,
|
||||
role: Roles.Workspace.Guest
|
||||
}
|
||||
|
||||
const workspace = {
|
||||
id: workspaceId,
|
||||
domainBasedMembershipProtectionEnabled: true,
|
||||
domains: [
|
||||
{
|
||||
verified: true,
|
||||
domain: 'example.org'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const { updateWorkspaceRole } = buildUpdateWorkspaceRoleAndTestContext(
|
||||
{
|
||||
workspaceId,
|
||||
workspaceRoles: [roleAdmin, roleGuest]
|
||||
},
|
||||
{
|
||||
getWorkspaceWithDomains: (() =>
|
||||
workspace) as unknown as GetWorkspaceWithDomains,
|
||||
findVerifiedEmailsByUserId: (() => [
|
||||
{
|
||||
email: 'notcorrect@nonexample.org'
|
||||
}
|
||||
]) as unknown as FindVerifiedEmailsByUserId
|
||||
}
|
||||
)
|
||||
|
||||
const err = await expectToThrow(() =>
|
||||
updateWorkspaceRole({
|
||||
workspaceId,
|
||||
userId: guestId,
|
||||
role: Roles.Workspace.Member
|
||||
})
|
||||
)
|
||||
expect(err.message).to.eq(new WorkspaceProtectedError().message)
|
||||
})
|
||||
it('sets roles on workspace projects when user added to workspace', async () => {
|
||||
const userId = cryptoRandomString({ length: 10 })
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
@@ -410,4 +491,383 @@ describe('Workspace role services', () => {
|
||||
expect(context.workspaceProjectRoles[0].role).to.equal(Roles.Stream.Contributor)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Workspace domains', () => {
|
||||
describe('addDomainToWorkspaceFactory returns a function that,', () => {
|
||||
it('throws a ForbiddenDomainError if the domain is not allowed to be registered', async () => {
|
||||
const userId = createRandomPassword()
|
||||
const workspaceId = createRandomPassword()
|
||||
const domain = 'gmail.com'
|
||||
|
||||
const err = await expectToThrow(
|
||||
async () =>
|
||||
await addDomainToWorkspaceFactory({
|
||||
findEmailsByUserId: async () => [],
|
||||
getWorkspace: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
getDomains: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
storeWorkspaceDomain: async () => {
|
||||
return
|
||||
},
|
||||
upsertWorkspace: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
emitWorkspaceEvent: async () => {
|
||||
expect.fail()
|
||||
}
|
||||
})({ userId, workspaceId, domain })
|
||||
)
|
||||
|
||||
expect(err.message).to.eq(new WorkspaceDomainBlockedError().message)
|
||||
})
|
||||
it('should throw and error if user has no email with specified domain', async () => {
|
||||
const userId = createRandomPassword()
|
||||
const workspaceId = createRandomPassword()
|
||||
const domain = 'example.org'
|
||||
|
||||
const err = await expectToThrow(
|
||||
async () =>
|
||||
await addDomainToWorkspaceFactory({
|
||||
findEmailsByUserId: async () => [],
|
||||
getWorkspace: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
getDomains: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
storeWorkspaceDomain: async () => {
|
||||
return
|
||||
},
|
||||
upsertWorkspace: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
emitWorkspaceEvent: async () => {
|
||||
expect.fail()
|
||||
}
|
||||
})({ userId, workspaceId, domain })
|
||||
)
|
||||
|
||||
expect(err.message).to.eq(new WorkspaceUnverifiedDomainError().message)
|
||||
})
|
||||
it('should throw and error if the workspace is not found', async () => {
|
||||
const userId = createRandomPassword()
|
||||
const workspaceId = createRandomPassword()
|
||||
const domain = 'example.org'
|
||||
|
||||
const err = await expectToThrow(
|
||||
async () =>
|
||||
await addDomainToWorkspaceFactory({
|
||||
findEmailsByUserId: async () =>
|
||||
[{ email: `foo@${domain}`, verified: true }] as UserEmail[],
|
||||
getWorkspace: async () => {
|
||||
return null
|
||||
},
|
||||
getDomains: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
storeWorkspaceDomain: async () => {
|
||||
return
|
||||
},
|
||||
upsertWorkspace: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
emitWorkspaceEvent: async () => {
|
||||
expect.fail()
|
||||
}
|
||||
})({ userId, workspaceId, domain })
|
||||
)
|
||||
|
||||
expect(err.message).to.eq(new WorkspaceAdminRequiredError().message)
|
||||
})
|
||||
it('throws a WorkspaceUnverifiedDomainError if the users domain matching email is not verified', async () => {
|
||||
const userId = createRandomPassword()
|
||||
const workspaceId = createRandomPassword()
|
||||
const domain = 'example.org'
|
||||
|
||||
const err = await expectToThrow(
|
||||
async () =>
|
||||
await addDomainToWorkspaceFactory({
|
||||
findEmailsByUserId: async () =>
|
||||
[{ email: `foo@${domain}`, verified: false }] as UserEmail[],
|
||||
getWorkspace: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
getDomains: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
storeWorkspaceDomain: async () => {
|
||||
return
|
||||
},
|
||||
upsertWorkspace: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
emitWorkspaceEvent: async () => {
|
||||
expect.fail()
|
||||
}
|
||||
})({ userId, workspaceId, domain })
|
||||
)
|
||||
|
||||
expect(err.message).to.eq(new WorkspaceUnverifiedDomainError().message)
|
||||
})
|
||||
it('throws a WorkspaceAdminRequiredError if the user does not have a workspace role', async () => {
|
||||
const userId = createRandomPassword()
|
||||
const workspaceId = createRandomPassword()
|
||||
const domain = 'example.org'
|
||||
|
||||
const err = await expectToThrow(
|
||||
async () =>
|
||||
await addDomainToWorkspaceFactory({
|
||||
findEmailsByUserId: async () =>
|
||||
[{ email: `foo@${domain}`, verified: true }] as UserEmail[],
|
||||
getWorkspace: async () => {
|
||||
return null
|
||||
},
|
||||
getDomains: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
storeWorkspaceDomain: async () => {
|
||||
return
|
||||
},
|
||||
upsertWorkspace: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
emitWorkspaceEvent: async () => {
|
||||
expect.fail()
|
||||
}
|
||||
})({ userId, workspaceId, domain })
|
||||
)
|
||||
|
||||
expect(err.message).to.eq(new WorkspaceAdminRequiredError().message)
|
||||
})
|
||||
it('throws a WorkspaceAdminRequiredError if the user is not an admin of the workspace', async () => {
|
||||
const userId = createRandomPassword()
|
||||
const workspaceId = createRandomPassword()
|
||||
const domain = 'example.org'
|
||||
|
||||
const err = await expectToThrow(
|
||||
async () =>
|
||||
await addDomainToWorkspaceFactory({
|
||||
findEmailsByUserId: async () =>
|
||||
[{ email: `foo@${domain}`, verified: true }] as UserEmail[],
|
||||
getWorkspace: async () => {
|
||||
return {
|
||||
role: Roles.Workspace.Guest,
|
||||
userId,
|
||||
id: workspaceId,
|
||||
name: cryptoRandomString({ length: 10 }),
|
||||
logo: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
description: null,
|
||||
discoverabilityEnabled: false,
|
||||
domainBasedMembershipProtectionEnabled: false,
|
||||
defaultLogoIndex: 0
|
||||
}
|
||||
},
|
||||
getDomains: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
storeWorkspaceDomain: async () => {
|
||||
return
|
||||
},
|
||||
upsertWorkspace: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
emitWorkspaceEvent: async () => {
|
||||
expect.fail()
|
||||
}
|
||||
})({ userId, workspaceId, domain })
|
||||
)
|
||||
|
||||
expect(err.message).to.eq(new WorkspaceAdminRequiredError().message)
|
||||
})
|
||||
it('does NOT store the verified workspace domain if its already stored', async () => {
|
||||
const userId = createRandomPassword()
|
||||
const workspaceId = createRandomPassword()
|
||||
const domain = 'example.org'
|
||||
|
||||
const domainRequest = {
|
||||
userId,
|
||||
workspaceId,
|
||||
domain
|
||||
}
|
||||
|
||||
const storedDomains: WorkspaceDomain | undefined = undefined
|
||||
|
||||
const workspace: Workspace = {
|
||||
id: workspaceId,
|
||||
name: cryptoRandomString({ length: 10 }),
|
||||
logo: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
description: null,
|
||||
discoverabilityEnabled: false,
|
||||
domainBasedMembershipProtectionEnabled: false,
|
||||
defaultLogoIndex: 0
|
||||
}
|
||||
|
||||
await addDomainToWorkspaceFactory({
|
||||
findEmailsByUserId: async () =>
|
||||
[{ email: `foo@${domain}`, verified: true }] as UserEmail[],
|
||||
getWorkspace: async () => {
|
||||
return {
|
||||
role: Roles.Workspace.Admin,
|
||||
userId,
|
||||
...workspace
|
||||
}
|
||||
},
|
||||
|
||||
getDomains: async () => {
|
||||
return [{ domain }] as WorkspaceDomain[]
|
||||
},
|
||||
upsertWorkspace: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
emitWorkspaceEvent: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
storeWorkspaceDomain: async () => {
|
||||
expect.fail()
|
||||
}
|
||||
})(domainRequest)
|
||||
|
||||
expect(storedDomains).to.be.undefined
|
||||
})
|
||||
it('stores the verified workspace domain, toggles workspace discoverability for first domain, emits update event', async () => {
|
||||
const userId = createRandomPassword()
|
||||
const workspaceId = createRandomPassword()
|
||||
const domain = 'example.org'
|
||||
|
||||
const domainRequest = {
|
||||
userId,
|
||||
workspaceId,
|
||||
domain
|
||||
}
|
||||
|
||||
let storedDomains: WorkspaceDomain | undefined = undefined
|
||||
let storedWorkspace: Omit<Workspace, 'domains'> | undefined = undefined
|
||||
let omittedEventName: EventNames | undefined = undefined
|
||||
|
||||
const workspace: Workspace = {
|
||||
id: workspaceId,
|
||||
name: cryptoRandomString({ length: 10 }),
|
||||
logo: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
description: null,
|
||||
discoverabilityEnabled: false,
|
||||
domainBasedMembershipProtectionEnabled: false,
|
||||
defaultLogoIndex: 0
|
||||
}
|
||||
|
||||
await addDomainToWorkspaceFactory({
|
||||
findEmailsByUserId: async () =>
|
||||
[{ email: `foo@${domain}`, verified: true }] as UserEmail[],
|
||||
getWorkspace: async () => {
|
||||
return {
|
||||
role: Roles.Workspace.Admin,
|
||||
userId,
|
||||
...workspace
|
||||
}
|
||||
},
|
||||
|
||||
getDomains: async () => {
|
||||
return []
|
||||
},
|
||||
upsertWorkspace: async ({ workspace }) => {
|
||||
storedWorkspace = workspace
|
||||
},
|
||||
emitWorkspaceEvent: async ({ eventName }) => {
|
||||
omittedEventName = eventName
|
||||
return []
|
||||
},
|
||||
storeWorkspaceDomain: async ({ workspaceDomain }) => {
|
||||
storedDomains = workspaceDomain
|
||||
}
|
||||
})(domainRequest)
|
||||
|
||||
expect(storedDomains).to.not.be.undefined
|
||||
expect(storedDomains!.createdByUserId).to.be.equal(userId)
|
||||
expect(storedDomains!.domain).to.be.equal(domain)
|
||||
expect(storedDomains!.workspaceId).to.be.equal(workspaceId)
|
||||
expect(storedDomains!.verified).to.be.true
|
||||
|
||||
expect(storedWorkspace!.discoverabilityEnabled).to.be.true
|
||||
|
||||
expect(omittedEventName).to.be.equal(WorkspaceEvents.Updated)
|
||||
})
|
||||
it('stores the second verified domain, does NOT toggle workspace discoverability for subsequent domains', async () => {
|
||||
const userId = createRandomPassword()
|
||||
const workspaceId = createRandomPassword()
|
||||
const domain = 'example.org'
|
||||
const domain2 = 'example2.org'
|
||||
|
||||
const domainRequest = {
|
||||
userId,
|
||||
workspaceId,
|
||||
domain
|
||||
}
|
||||
|
||||
const workspaceWithoutDomains = {
|
||||
id: workspaceId,
|
||||
name: cryptoRandomString({ length: 10 }),
|
||||
logo: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
description: null,
|
||||
discoverabilityEnabled: false,
|
||||
domainBasedMembershipProtectionEnabled: false,
|
||||
domains: [],
|
||||
defaultLogoIndex: 0
|
||||
}
|
||||
|
||||
let workspaceData: Workspace = {
|
||||
...workspaceWithoutDomains
|
||||
}
|
||||
const insertedDomains: WorkspaceDomain[] = []
|
||||
let storedDomains: WorkspaceDomain[] = []
|
||||
|
||||
const addDomainToWorkspace = addDomainToWorkspaceFactory({
|
||||
findEmailsByUserId: async () =>
|
||||
[
|
||||
{ email: `foo@${domain}`, verified: true },
|
||||
{ email: `foo@${domain2}`, verified: true }
|
||||
] as UserEmail[],
|
||||
getWorkspace: async () => {
|
||||
return {
|
||||
role: Roles.Workspace.Admin,
|
||||
userId,
|
||||
...workspaceData
|
||||
}
|
||||
},
|
||||
getDomains: async () => storedDomains,
|
||||
upsertWorkspace: async ({ workspace }) => {
|
||||
workspaceData = { ...workspaceData, ...workspace }
|
||||
},
|
||||
emitWorkspaceEvent: async () => {
|
||||
return []
|
||||
},
|
||||
storeWorkspaceDomain: async ({ workspaceDomain }) => {
|
||||
insertedDomains.push(workspaceDomain)
|
||||
}
|
||||
})
|
||||
await addDomainToWorkspace(domainRequest)
|
||||
|
||||
expect(insertedDomains).to.have.lengthOf(1)
|
||||
expect(workspaceData.discoverabilityEnabled).to.be.true
|
||||
|
||||
// dirty hack, im post fact storing the domain on the test object
|
||||
storedDomains = insertedDomains
|
||||
|
||||
//faking user interaction disabling discoverability
|
||||
workspaceData.discoverabilityEnabled = false
|
||||
await addDomainToWorkspace({ ...domainRequest, domain: domain2 })
|
||||
|
||||
expect(workspaceData.discoverabilityEnabled).to.be.false
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,7 +8,8 @@ export const WorkspaceEvents = {
|
||||
Created: `${workspaceEventPrefix}created`,
|
||||
Updated: `${workspaceEventPrefix}updated`,
|
||||
RoleDeleted: `${workspaceEventPrefix}role-deleted`,
|
||||
RoleUpdated: `${workspaceEventPrefix}role-updated`
|
||||
RoleUpdated: `${workspaceEventPrefix}role-updated`,
|
||||
JoinedFromDiscovery: `${workspaceEventPrefix}joined-from-discovery`
|
||||
} as const
|
||||
|
||||
export type WorkspaceEvents = (typeof WorkspaceEvents)[keyof typeof WorkspaceEvents]
|
||||
@@ -19,10 +20,12 @@ type WorkspaceCreatedPayload = Workspace & {
|
||||
type WorkspaceUpdatedPayload = Workspace
|
||||
type WorkspaceRoleDeletedPayload = WorkspaceAcl
|
||||
type WorkspaceRoleUpdatedPayload = WorkspaceAcl
|
||||
type WorkspaceJoinedFromDiscoveryPayload = { userId: string; workspaceId: string }
|
||||
|
||||
export type WorkspaceEventsPayloads = {
|
||||
[WorkspaceEvents.Created]: WorkspaceCreatedPayload
|
||||
[WorkspaceEvents.Updated]: WorkspaceUpdatedPayload
|
||||
[WorkspaceEvents.RoleDeleted]: WorkspaceRoleDeletedPayload
|
||||
[WorkspaceEvents.RoleUpdated]: WorkspaceRoleUpdatedPayload
|
||||
[WorkspaceEvents.JoinedFromDiscovery]: WorkspaceJoinedFromDiscoveryPayload
|
||||
}
|
||||
|
||||
@@ -8,6 +8,19 @@ export type Workspace = {
|
||||
updatedAt: Date
|
||||
logo: string | null
|
||||
defaultLogoIndex: number
|
||||
domainBasedMembershipProtectionEnabled: boolean
|
||||
discoverabilityEnabled: boolean
|
||||
}
|
||||
export type WorkspaceWithDomains = Workspace & { domains: WorkspaceDomain[] }
|
||||
|
||||
export type WorkspaceDomain = {
|
||||
id: string
|
||||
workspaceId: string
|
||||
domain: string
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
createdByUserId: string | null
|
||||
verified: boolean
|
||||
}
|
||||
|
||||
export type WorkspaceWithOptionalRole = Workspace & { role?: WorkspaceRoles }
|
||||
|
||||
@@ -30,6 +30,15 @@ export = !FF_WORKSPACES_MODULE_ENABLED
|
||||
updateRole: async () => {
|
||||
throw new WorkspacesModuleDisabledError()
|
||||
},
|
||||
addDomain: async () => {
|
||||
throw new WorkspacesModuleDisabledError()
|
||||
},
|
||||
deleteDomain: async () => {
|
||||
throw new WorkspacesModuleDisabledError()
|
||||
},
|
||||
join: async () => {
|
||||
throw new WorkspacesModuleDisabledError()
|
||||
},
|
||||
leave: async () => {
|
||||
throw new WorkspacesModuleDisabledError()
|
||||
},
|
||||
@@ -64,9 +73,15 @@ export = !FF_WORKSPACES_MODULE_ENABLED
|
||||
},
|
||||
projects: async () => {
|
||||
throw new WorkspacesModuleDisabledError()
|
||||
},
|
||||
domains: async () => {
|
||||
throw new WorkspacesModuleDisabledError()
|
||||
}
|
||||
},
|
||||
User: {
|
||||
discoverableWorkspaces: async () => {
|
||||
throw new WorkspacesModuleDisabledError()
|
||||
},
|
||||
workspaces: async () => {
|
||||
throw new WorkspacesModuleDisabledError()
|
||||
},
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Knex } from 'knex'
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.createTable('workspace_domains', (table) => {
|
||||
table.text('id').primary()
|
||||
table.text('domain').notNullable()
|
||||
table.boolean('verified').notNullable()
|
||||
table.timestamp('createdAt', { precision: 3, useTz: true }).notNullable()
|
||||
table.timestamp('updatedAt', { precision: 3, useTz: true }).notNullable()
|
||||
table.text('createdByUserId').references('id').inTable('users').onDelete('set null')
|
||||
table.text('workspaceId').references('id').inTable('workspaces').onDelete('cascade')
|
||||
table.unique(['workspaceId', 'domain'])
|
||||
table.index('workspaceId')
|
||||
table.index('domain')
|
||||
})
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTable('workspace_domains')
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { Knex } from 'knex'
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('workspaces', (table) => {
|
||||
table.boolean('domainBasedMembershipProtectionEnabled').defaultTo(false)
|
||||
})
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('workspaces', (table) => {
|
||||
table.dropColumn('domainBasedMembershipProtectionEnabled')
|
||||
})
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { Knex } from 'knex'
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('workspaces', (table) => {
|
||||
table.boolean('discoverabilityEnabled').defaultTo(false)
|
||||
})
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('workspaces', (table) => {
|
||||
table.dropColumn('discoverabilityEnabled')
|
||||
})
|
||||
}
|
||||
@@ -55,6 +55,11 @@ export type ActivityCollection = {
|
||||
totalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type AddDomainToWorkspaceInput = {
|
||||
domain: Scalars['String']['input'];
|
||||
workspaceId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type AdminInviteList = {
|
||||
__typename?: 'AdminInviteList';
|
||||
cursor?: Maybe<Scalars['String']['output']>;
|
||||
@@ -847,6 +852,15 @@ export type DiscoverableStreamsSortingInput = {
|
||||
type: DiscoverableStreamsSortType;
|
||||
};
|
||||
|
||||
export type DiscoverableWorkspace = {
|
||||
__typename?: 'DiscoverableWorkspace';
|
||||
defaultLogoIndex: Scalars['Int']['output'];
|
||||
description?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
logo?: Maybe<Scalars['String']['output']>;
|
||||
name: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type EditCommentInput = {
|
||||
commentId: Scalars['String']['input'];
|
||||
content: CommentContentInput;
|
||||
@@ -920,6 +934,10 @@ export type GendoAiRenderInput = {
|
||||
versionId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type JoinWorkspaceInput = {
|
||||
workspaceId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type LegacyCommentViewerData = {
|
||||
__typename?: 'LegacyCommentViewerData';
|
||||
/**
|
||||
@@ -3369,6 +3387,8 @@ export type User = {
|
||||
/** Returns the apps you have created. */
|
||||
createdApps?: Maybe<Array<ServerApp>>;
|
||||
createdAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
/** Get discoverable workspaces with verified domains that match the active user's */
|
||||
discoverableWorkspaces: Array<DiscoverableWorkspace>;
|
||||
/** Only returned if API user is the user being requested or an admin */
|
||||
email?: Maybe<Scalars['String']['output']>;
|
||||
emails: Array<UserEmail>;
|
||||
@@ -3828,6 +3848,12 @@ export type Workspace = {
|
||||
/** Selected fallback when `logo` not set */
|
||||
defaultLogoIndex: Scalars['Int']['output'];
|
||||
description?: Maybe<Scalars['String']['output']>;
|
||||
/** Enable/Disable discovery of the workspace */
|
||||
discoverabilityEnabled: Scalars['Boolean']['output'];
|
||||
/** Enable/Disable restriction to invite users to workspace as Guests only */
|
||||
domainBasedMembershipProtectionEnabled: Scalars['Boolean']['output'];
|
||||
/** Verified workspace domains */
|
||||
domains: Array<WorkspaceDomain>;
|
||||
id: Scalars['ID']['output'];
|
||||
/** Only available to workspace owners */
|
||||
invitedTeam?: Maybe<Array<PendingWorkspaceCollaborator>>;
|
||||
@@ -3878,6 +3904,17 @@ export type WorkspaceCreateInput = {
|
||||
name: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type WorkspaceDomain = {
|
||||
__typename?: 'WorkspaceDomain';
|
||||
domain: Scalars['String']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
};
|
||||
|
||||
export type WorkspaceDomainDeleteInput = {
|
||||
id: Scalars['ID']['input'];
|
||||
workspaceId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type WorkspaceInviteCreateInput = {
|
||||
/** Either this or userId must be filled */
|
||||
email?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -3943,15 +3980,23 @@ export type WorkspaceInviteUseInput = {
|
||||
|
||||
export type WorkspaceMutations = {
|
||||
__typename?: 'WorkspaceMutations';
|
||||
addDomain: Workspace;
|
||||
create: Workspace;
|
||||
delete: Scalars['Boolean']['output'];
|
||||
deleteDomain: Workspace;
|
||||
invites: WorkspaceInviteMutations;
|
||||
join: Workspace;
|
||||
leave: Scalars['Boolean']['output'];
|
||||
update: Workspace;
|
||||
updateRole: Workspace;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceMutationsAddDomainArgs = {
|
||||
input: AddDomainToWorkspaceInput;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceMutationsCreateArgs = {
|
||||
input: WorkspaceCreateInput;
|
||||
};
|
||||
@@ -3962,6 +4007,16 @@ export type WorkspaceMutationsDeleteArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceMutationsDeleteDomainArgs = {
|
||||
input: WorkspaceDomainDeleteInput;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceMutationsJoinArgs = {
|
||||
input: JoinWorkspaceInput;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceMutationsLeaveArgs = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
@@ -4022,6 +4077,8 @@ export type WorkspaceTeamFilter = {
|
||||
export type WorkspaceUpdateInput = {
|
||||
defaultLogoIndex?: InputMaybe<Scalars['Int']['input']>;
|
||||
description?: InputMaybe<Scalars['String']['input']>;
|
||||
discoverabilityEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
domainBasedMembershipProtectionEnabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
id: Scalars['String']['input'];
|
||||
/** Logo image as base64-encoded string */
|
||||
logo?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -4633,6 +4690,11 @@ export type GetWorkspaceQueryVariables = Exact<{
|
||||
|
||||
export type GetWorkspaceQuery = { __typename?: 'Query', workspace: { __typename?: 'Workspace', id: string, name: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, team: Array<{ __typename?: 'WorkspaceCollaborator', id: string, role: string }> } };
|
||||
|
||||
export type GetActiveUserDiscoverableWorkspacesQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetActiveUserDiscoverableWorkspacesQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', discoverableWorkspaces: Array<{ __typename?: 'DiscoverableWorkspace', id: string, name: string, description?: string | null }> } | null };
|
||||
|
||||
export type UpdateWorkspaceMutationVariables = Exact<{
|
||||
input: WorkspaceUpdateInput;
|
||||
}>;
|
||||
@@ -4780,6 +4842,7 @@ export const MarkProjectVersionReceivedDocument = {"kind":"Document","definition
|
||||
export const CreateWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}}]}}]} as unknown as DocumentNode<CreateWorkspaceMutation, CreateWorkspaceMutationVariables>;
|
||||
export const DeleteWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"delete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}]}]}}]}}]} as unknown as DocumentNode<DeleteWorkspaceMutation, DeleteWorkspaceMutationVariables>;
|
||||
export const GetWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspaceTeam"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspaceTeam"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]} as unknown as DocumentNode<GetWorkspaceQuery, GetWorkspaceQueryVariables>;
|
||||
export const GetActiveUserDiscoverableWorkspacesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"getActiveUserDiscoverableWorkspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"discoverableWorkspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}}]}}]} as unknown as DocumentNode<GetActiveUserDiscoverableWorkspacesQuery, GetActiveUserDiscoverableWorkspacesQueryVariables>;
|
||||
export const UpdateWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceUpdateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"update"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}}]}}]} as unknown as DocumentNode<UpdateWorkspaceMutation, UpdateWorkspaceMutationVariables>;
|
||||
export const GetActiveUserWorkspacesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetActiveUserWorkspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}}]}}]} as unknown as DocumentNode<GetActiveUserWorkspacesQuery, GetActiveUserWorkspacesQueryVariables>;
|
||||
export const UpdateWorkspaceRoleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWorkspaceRole"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceRoleUpdateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateRole"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]}}]}}]} as unknown as DocumentNode<UpdateWorkspaceRoleMutation, UpdateWorkspaceRoleMutationVariables>;
|
||||
|
||||
@@ -66,6 +66,18 @@ export const getWorkspaceQuery = gql`
|
||||
${workspaceTeamFragment}
|
||||
`
|
||||
|
||||
export const getActiveUserDiscoverableWorkspacesQuery = gql`
|
||||
query getActiveUserDiscoverableWorkspaces {
|
||||
activeUser {
|
||||
discoverableWorkspaces {
|
||||
id
|
||||
name
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const updateWorkspaceQuery = gql`
|
||||
mutation UpdateWorkspace($input: WorkspaceUpdateInput!) {
|
||||
workspaceMutations {
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
},
|
||||
"files.eol": "\n",
|
||||
"volar.vueserver.maxOldSpaceSize": 4000,
|
||||
"cSpell.words": ["Automations", "Bursty", "Insertable", "mjml"],
|
||||
"cSpell.words": ["Automations", "Bursty", "discoverability", "Insertable", "mjml"],
|
||||
"tailwindCSS.experimental.configFile": {
|
||||
"packages/frontend-2/tailwind.config.mjs": "packages/frontend-2/**"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user