Skip to content

Commit

Permalink
feat(fe2): app authorization workflow redesign [WBX-217] (#2044)
Browse files Browse the repository at this point in the history
* WIP

* new permissions table

* permissions grouped

* updated scope descriptions

* more scope copy adjustments

* allow auth error handling

* manually closable toast notification

* fixed mentions rendering

* error view

* not you? feature

* cleanup

* minor styling changes

* WIP table

* finished authorized apps table

* minor cleanup

* cleaning up comment

* testing changes
  • Loading branch information
fabis94 authored Feb 23, 2024
1 parent c4ce83e commit 6af6c65
Show file tree
Hide file tree
Showing 47 changed files with 794 additions and 454 deletions.
8 changes: 8 additions & 0 deletions packages/frontend-2/assets/css/tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,11 @@ body,
div#__nuxt {
height: 100%;
}

@layer components {
.terms-of-service {
a {
@apply underline;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
<FormButton submit full-width class="mt-4" :disabled="loading">Sign up</FormButton>
<div
v-if="serverInfo.termsOfService"
class="mt-2 text-xs text-foreground-2 text-center linkify-tos"
class="mt-2 text-xs text-foreground-2 text-center terms-of-service"
v-html="serverInfo.termsOfService"
></div>
<div class="mt-2 sm:mt-8 text-center text-xs sm:text-base">
Expand Down Expand Up @@ -164,8 +164,3 @@ watch(
{ immediate: true }
)
</script>
<style>
.linkify-tos a {
text-decoration: underline;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<a
:class="[
'flex flex-col p-2 cursor-pointer',
isSelected ? 'bg-foundation-2' : 'hover:bg-foundation-3'
isSelected ? 'bg-foundation-focus dark:bg-foundation-2' : 'hover:bg-foundation-3'
]"
@click="($event) => $emit('click', $event)"
>
Expand Down
27 changes: 15 additions & 12 deletions packages/frontend-2/components/common/tiptap/TextEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ import type {
TiptapEditorSchemaOptions
} from '~~/lib/common/helpers/tiptap'
import type { Nullable } from '@speckle/shared'
import { userProfileRoute } from '~~/lib/common/helpers/route'
// import { userProfileRoute } from '~~/lib/common/helpers/route'
import { onKeyDown } from '@vueuse/core'
import { noop } from 'lodash-es'
const emit = defineEmits<{
(e: 'update:modelValue', val: JSONContent): void
Expand Down Expand Up @@ -95,17 +96,19 @@ const onEditorContentClick = (e: MouseEvent) => {
e.stopPropagation()
}
const onMentionClick = (userId: string, e: MouseEvent) => {
if (!props.readonly) return
// TODO: No profile page to link to in FE2 yet
// const onMentionClick = (userId: string, e: MouseEvent) => {
// if (!props.readonly) return
const path = userProfileRoute(userId)
const isMetaKey = e.metaKey || e.ctrlKey
if (isMetaKey) {
window.open(path, '_blank')
} else {
window.location.href = path
}
}
// const path = userProfileRoute(userId)
// const isMetaKey = e.metaKey || e.ctrlKey
// if (isMetaKey) {
// window.open(path, '_blank')
// } else {
// window.location.href = path
// }
// }
const onMentionClick = noop
onKeyDown(
'Escape',
Expand Down Expand Up @@ -184,7 +187,7 @@ onBeforeUnmount(() => {
box-shadow: unset !important;
.editor-mention {
cursor: pointer;
/* cursor: pointer; TODO: Reenable once mentions are clickable again */
}
}
}
Expand Down
123 changes: 95 additions & 28 deletions packages/frontend-2/components/developer-settings/DeleteDialog.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
<template>
<LayoutDialog v-model:open="isOpen" max-width="sm" :buttons="dialogButtons">
<template #header>Delete {{ itemType }}</template>
<template #header>{{ title }}</template>
<div class="flex flex-col gap-6 text-sm text-foreground">
<p>
Are you sure you want to
<strong>permanently delete</strong>
<strong>permanently {{ lowerFirst(itemActionVerb) }}</strong>
the selected {{ itemType.toLowerCase() }}?
<template v-if="isAuthorization(item)">
(Removing access to an app will log you out of it on all devices.)
</template>
</p>
<div v-if="item" class="flex flex-col gap-2">
<strong class="truncate">{{ item.name }}</strong>
</div>

<p>
This
This action
<strong>cannot</strong>
be undone.
</p>
Expand All @@ -21,49 +24,75 @@
</template>

<script setup lang="ts">
import { useMutation } from '@vue/apollo-composable'
import { useMutation, useMutationLoading } from '@vue/apollo-composable'
import { LayoutDialog } from '@speckle/ui-components'
import type {
ApplicationItem,
TokenItem
TokenItem,
AuthorizedAppItem
} from '~~/lib/developer-settings/helpers/types'
import {
deleteAccessTokenMutation,
deleteApplicationMutation
deleteApplicationMutation,
revokeAppAccessMutation
} from '~~/lib/developer-settings/graphql/mutations'
import {
convertThrowIntoFetchResult,
getCacheId,
getFirstErrorMessage
getFirstErrorMessage,
modifyObjectFields
} from '~~/lib/common/helpers/graphql'
import { useGlobalToast, ToastNotificationType } from '~~/lib/common/composables/toast'
import { lowerFirst } from 'lodash-es'
import { useActiveUser } from '~/lib/auth/composables/activeUser'
import type { User } from '~/lib/common/generated/gql/graphql'
type ItemType = TokenItem | ApplicationItem | AuthorizedAppItem | null
const isToken = (i: ItemType): i is TokenItem => !!(i && 'lastChars' in i)
const isApplication = (i: ItemType): i is ApplicationItem => !!(i && 'secret' in i)
const isAuthorization = (i: ItemType): i is AuthorizedAppItem =>
!(isToken(i) || isApplication(i))
const props = defineProps<{
item: TokenItem | ApplicationItem | null
item: ItemType
}>()
const { triggerNotification } = useGlobalToast()
const { mutate: deleteTokenMutation } = useMutation(deleteAccessTokenMutation)
const { mutate: deleteAppMutation } = useMutation(deleteApplicationMutation)
const isLoading = useMutationLoading()
const { mutate: deleteToken } = useMutation(deleteAccessTokenMutation)
const { mutate: deleteApp } = useMutation(deleteApplicationMutation)
const { mutate: revokeAuthorization } = useMutation(revokeAppAccessMutation)
const { userId } = useActiveUser()
const isOpen = defineModel<boolean>('open', { required: true })
const itemType = computed(() => {
return props.item && 'secret' in props.item ? 'Application' : 'Access Token'
if (isToken(props.item)) {
return `Access Token`
} else if (isApplication(props.item)) {
return `Application`
} else {
return 'Authorization'
}
})
const isApplication = (i: TokenItem | ApplicationItem | null): i is ApplicationItem =>
!!(i && 'secret' in i)
const itemActionVerb = computed(() => {
return isToken(props.item) || isApplication(props.item) ? 'Delete' : 'Remove'
})
const title = computed(() => {
return `${itemActionVerb.value} ${itemType.value}`
})
const deleteConfirmed = async () => {
const uid = userId.value
const itemId = props.item?.id
if (!itemId) {
if (!itemId || !uid) {
return
}
if (!isApplication(props.item)) {
const result = await deleteTokenMutation(
if (isToken(props.item)) {
const result = await deleteToken(
{
token: itemId
},
Expand Down Expand Up @@ -92,8 +121,8 @@ const deleteConfirmed = async () => {
description: errorMessage
})
}
} else {
const result = await deleteAppMutation(
} else if (isApplication(props.item)) {
const result = await deleteApp(
{
appId: itemId
},
Expand Down Expand Up @@ -122,19 +151,57 @@ const deleteConfirmed = async () => {
description: errorMessage
})
}
} else {
const result = await revokeAuthorization(
{ appId: itemId },
{
update: (cache, res) => {
if (res.data?.appRevokeAccess) {
modifyObjectFields<undefined, User['authorizedApps']>(
cache,
getCacheId('User', uid),
(_fieldName, _variables, value) => {
if (!value) return value
return value.filter(
(a) => a.__ref !== getCacheId('ServerAppListItem', itemId)
)
},
{ fieldNameWhitelist: ['authorizedApps'] }
)
}
}
}
).catch(convertThrowIntoFetchResult)
if (result?.data?.appRevokeAccess) {
isOpen.value = false
triggerNotification({
type: ToastNotificationType.Success,
title: 'Authorization removed',
description: 'The application authorization has been successfully removed'
})
} else {
const errorMessage = getFirstErrorMessage(result?.errors)
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Failed to revoke app authorization',
description: errorMessage
})
}
}
}
const dialogButtons = [
{
text: 'Delete',
props: { color: 'danger', fullWidth: true },
onClick: deleteConfirmed
},
const dialogButtons = computed(() => [
{
text: 'Cancel',
props: { color: 'secondary', fullWidth: true, outline: true },
onClick: () => (isOpen.value = false)
onClick: (): boolean => (isOpen.value = false)
},
{
text: itemActionVerb.value,
props: { color: 'danger', fullWidth: true },
disabled: isLoading.value,
onClick: deleteConfirmed
}
]
])
</script>
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<template>
<div class="flex flex-col gap-4">
<div class="flex flex-col md:flex-row gap-3 md:gap-0 justify-between">
<h1 class="text-2xl font-bold">{{ title }}</h1>
<h2 v-if="subheading" class="h5 font-bold">{{ title }}</h2>
<h1 v-else class="h4 font-bold">{{ title }}</h1>
<div class="flex flex-wrap gap-2">
<FormButton
v-for="(button, index) in buttons"
Expand Down Expand Up @@ -30,6 +31,7 @@ withDefaults(
title: string
text?: string
buttons?: Button[]
subheading?: boolean
}>(),
{
buttons: () => []
Expand Down
20 changes: 12 additions & 8 deletions packages/frontend-2/components/header/LogoBlock.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<template>
<NuxtLink class="flex items-center shrink-0" :to="to" :target="target">
<Component
:is="mainComponent"
class="flex items-center shrink-0"
:to="to"
:target="target"
>
<img
class="h-8 w-8 block"
:class="{
grayscale: active
}"
src="~~/assets/images/speckle_logo_big.png"
alt="Speckle"
/>
Expand All @@ -16,20 +18,22 @@
>
Speckle
</div>
</NuxtLink>
</Component>
</template>
<script setup lang="ts">
withDefaults(
const props = withDefaults(
defineProps<{
minimal?: boolean
active?: boolean
to?: string
showTextOnMobile?: boolean
target?: string
noLink?: boolean
}>(),
{
active: true,
to: '/'
}
)
const NuxtLink = resolveComponent('NuxtLink')
const mainComponent = computed(() => (props.noLink ? 'div' : NuxtLink))
</script>
14 changes: 3 additions & 11 deletions packages/frontend-2/components/header/NavUserMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
active ? 'bg-foundation-focus' : '',
'flex gap-3.5 items-center px-3 py-2.5 text-sm text-foreground cursor-pointer transition mx-1 rounded'
]"
@click="onThemeClick"
@click="toggleTheme"
>
<Icon class="w-5 h-5" />
{{ isDarkTheme ? 'Light Mode' : 'Dark Mode' }}
Expand Down Expand Up @@ -145,7 +145,7 @@ import {
import { Roles } from '@speckle/shared'
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
import { useAuthManager } from '~~/lib/auth/composables/auth'
import { useTheme, AppTheme } from '~~/lib/core/composables/theme'
import { useTheme } from '~~/lib/core/composables/theme'
import { useServerInfo } from '~/lib/core/composables/server'
import type { RouteLocationRaw } from 'vue-router'
import { homeRoute, profileRoute } from '~/lib/common/helpers/route'
Expand All @@ -156,7 +156,7 @@ defineProps<{
const { logout } = useAuthManager()
const { activeUser, isGuest } = useActiveUser()
const { isDarkTheme, setTheme } = useTheme()
const { isDarkTheme, toggleTheme } = useTheme()
const { serverInfo } = useServerInfo()
const router = useRouter()
const route = useRoute()
Expand All @@ -173,14 +173,6 @@ const toggleInviteDialog = () => {
showInviteDialog.value = true
}
const onThemeClick = () => {
if (isDarkTheme.value) {
setTheme(AppTheme.Light)
} else {
setTheme(AppTheme.Dark)
}
}
const goToConnectors = () => {
router.push('/downloads')
}
Expand Down
Loading

0 comments on commit 6af6c65

Please sign in to comment.