123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
- <script setup lang="ts">
- import { computed } from 'vue'
- import { NotificationTypes } from '#shared/components/CommonNotifications/types.ts'
- import { useNotifications } from '#shared/components/CommonNotifications/useNotifications.ts'
- import { useConfirmation } from '#shared/composables/useConfirmation.ts'
- import { useUserCurrentAccessTokenDeleteMutation } from '#shared/entities/user/current/graphql/mutations/userCurrentAccessTokenDelete.api.ts'
- import { useUserCurrentAccessTokenListQuery } from '#shared/entities/user/current/graphql/queries/userCurrentAcessTokenList.api.ts'
- import type {
- Token,
- UserCurrentAccessTokenUpdatesSubscription,
- UserCurrentAccessTokenUpdatesSubscriptionVariables,
- UserCurrentAccessTokenListQuery,
- } from '#shared/graphql/types.ts'
- import { i18n } from '#shared/i18n/index.ts'
- import { ErrorRouteType, redirectErrorRoute } from '#shared/router/error.ts'
- import MutationHandler from '#shared/server/apollo/handler/MutationHandler.ts'
- import QueryHandler from '#shared/server/apollo/handler/QueryHandler.ts'
- import { useSessionStore } from '#shared/stores/session.ts'
- import { ErrorStatusCodes } from '#shared/types/error.ts'
- import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
- import { useFlyout } from '#desktop/components/CommonFlyout/useFlyout.ts'
- import CommonLoader from '#desktop/components/CommonLoader/CommonLoader.vue'
- import type { MenuItem } from '#desktop/components/CommonPopoverMenu/types.ts'
- import CommonSimpleTable from '#desktop/components/CommonSimpleTable/CommonSimpleTable.vue'
- import type {
- TableHeader,
- TableItem,
- } from '#desktop/components/CommonSimpleTable/types.ts'
- import LayoutContent from '#desktop/components/layout/LayoutContent.vue'
- import { useCheckTokenAccess } from '../composables/permission/useCheckTokenAccess.ts'
- import { useBreadcrumb } from '../composables/useBreadcrumb.ts'
- import { UserCurrentAccessTokenUpdatesDocument } from '../graphql/subscriptions/userCurrentAccessTokenUpdates.api.ts'
- defineOptions({
- beforeRouteEnter() {
- const { canUseAccessToken } = useCheckTokenAccess()
- if (!canUseAccessToken.value)
- return redirectErrorRoute({
- type: ErrorRouteType.AuthenticatedError,
- title: __('Forbidden'),
- message: __(
- 'Token-based API access has been disabled by the administrator.',
- ),
- statusCode: ErrorStatusCodes.Forbidden,
- })
- return true
- },
- })
- const session = useSessionStore()
- const { breadcrumbItems } = useBreadcrumb(__('Token Access'))
- const newAccessTokenFlyout = useFlyout({
- name: 'new-access-token',
- component: () =>
- import('../components/PersonalSettingNewAccessTokenFlyout.vue'),
- })
- const accessTokenListQuery = new QueryHandler(
- useUserCurrentAccessTokenListQuery(),
- )
- const accessTokenListQueryResult = accessTokenListQuery.result()
- const accessTokenListLoading = accessTokenListQuery.loading()
- accessTokenListQuery.subscribeToMore<
- UserCurrentAccessTokenUpdatesSubscriptionVariables,
- UserCurrentAccessTokenUpdatesSubscription
- >({
- document: UserCurrentAccessTokenUpdatesDocument,
- variables: {
- userId: session.user?.id || '',
- },
- updateQuery: (prev, { subscriptionData }) => {
- if (!subscriptionData.data?.userCurrentAccessTokenUpdates.tokens) {
- return null as unknown as UserCurrentAccessTokenListQuery
- }
- return {
- userCurrentAccessTokenList:
- subscriptionData.data.userCurrentAccessTokenUpdates.tokens,
- }
- },
- })
- const tableHeaders: TableHeader[] = [
- {
- key: 'name',
- label: __('Name'),
- truncate: true,
- },
- {
- key: 'permissions',
- label: __('Permissions'),
- truncate: true,
- },
- {
- key: 'createdAt',
- label: __('Created'),
- type: 'timestamp',
- },
- {
- key: 'expiresAt',
- label: __('Expires'),
- type: 'timestamp',
- },
- {
- key: 'lastUsedAt',
- label: __('Last Used'),
- type: 'timestamp',
- },
- ]
- const { notify } = useNotifications()
- const { waitForVariantConfirmation } = useConfirmation()
- const deleteDevice = (accessToken: Token) => {
- const accessTokenDeleteMutation = new MutationHandler(
- useUserCurrentAccessTokenDeleteMutation(() => ({
- variables: {
- tokenId: accessToken.id,
- },
- update(cache) {
- cache.evict({ id: cache.identify(accessToken) })
- cache.gc()
- },
- })),
- {
- errorNotificationMessage: __(
- 'The personal access token could not be deleted.',
- ),
- },
- )
- accessTokenDeleteMutation.send().then(() => {
- notify({
- id: 'personal-access-token-removed',
- type: NotificationTypes.Success,
- message: __('Personal access token has been deleted.'),
- })
- })
- }
- const confirmDeleteDevice = async (accessToken: Token) => {
- const confirmed = await waitForVariantConfirmation('delete')
- if (confirmed) deleteDevice(accessToken)
- }
- const tableActions: MenuItem[] = [
- {
- key: 'delete',
- label: __('Delete this access token'),
- icon: 'trash3',
- variant: 'danger',
- onClick: (data) => {
- confirmDeleteDevice(data as Token)
- },
- },
- ]
- const currentAccessTokens = computed<TableItem[]>(() => {
- return (
- accessTokenListQueryResult.value?.userCurrentAccessTokenList || []
- ).map((accessToken) => {
- return {
- ...accessToken,
- permissions: accessToken.preferences?.permission?.join(', ') || '',
- }
- })
- })
- const currentAccessTokenPresent = computed(
- () => currentAccessTokens.value.length > 0,
- )
- const helpText = computed(() => [
- i18n.t(
- 'You can generate a personal access token for each application you use that needs access to the Zammad API.',
- ),
- i18n.t("Pick a name for the application, and we'll give you a unique token."),
- ])
- </script>
- <template>
- <LayoutContent
- :help-text="helpText"
- :show-inline-help="!currentAccessTokenPresent && !accessTokenListLoading"
- :breadcrumb-items="breadcrumbItems"
- width="narrow"
- >
- <template #headerRight>
- <div class="flex flex-row gap-2">
- <CommonButton
- prefix-icon="key"
- variant="primary"
- size="medium"
- @click="newAccessTokenFlyout.open()"
- >
- {{ $t('New Personal Access Token') }}
- </CommonButton>
- </div>
- </template>
- <CommonLoader :loading="accessTokenListLoading">
- <div class="mb-4">
- <CommonSimpleTable
- :headers="tableHeaders"
- :items="currentAccessTokens"
- :actions="tableActions"
- :aria-label="$t('Personal Access Tokens')"
- class="min-w-150"
- >
- <template #item-suffix-name="{ item }">
- <CommonBadge
- v-if="item.current"
- size="medium"
- variant="info"
- class="ltr:ml-2 rtl:mr-2"
- >{{ $t('This device') }}
- </CommonBadge>
- </template>
- </CommonSimpleTable>
- </div>
- </CommonLoader>
- </LayoutContent>
- </template>
|