PersonalSettingTokenAccess.vue 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { computed } from 'vue'
  4. import { useRouter } from 'vue-router'
  5. import { NotificationTypes } from '#shared/components/CommonNotifications/types.ts'
  6. import { useNotifications } from '#shared/components/CommonNotifications/useNotifications.ts'
  7. import { useConfirmation } from '#shared/composables/useConfirmation.ts'
  8. import { useUserCurrentAccessTokenDeleteMutation } from '#shared/entities/user/current/graphql/mutations/userCurrentAccessTokenDelete.api.ts'
  9. import { useUserCurrentAccessTokenListQuery } from '#shared/entities/user/current/graphql/queries/userCurrentAcessTokenList.api.ts'
  10. import type {
  11. Token,
  12. UserCurrentAccessTokenUpdatesSubscription,
  13. UserCurrentAccessTokenUpdatesSubscriptionVariables,
  14. UserCurrentAccessTokenListQuery,
  15. } from '#shared/graphql/types.ts'
  16. import { i18n } from '#shared/i18n/index.ts'
  17. import { redirectToError } from '#shared/router/error.ts'
  18. import MutationHandler from '#shared/server/apollo/handler/MutationHandler.ts'
  19. import QueryHandler from '#shared/server/apollo/handler/QueryHandler.ts'
  20. import { useSessionStore } from '#shared/stores/session.ts'
  21. import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
  22. import { useFlyout } from '#desktop/components/CommonFlyout/useFlyout.ts'
  23. import CommonLoader from '#desktop/components/CommonLoader/CommonLoader.vue'
  24. import type { MenuItem } from '#desktop/components/CommonPopoverMenu/types.ts'
  25. import CommonSimpleTable from '#desktop/components/CommonSimpleTable/CommonSimpleTable.vue'
  26. import type {
  27. TableHeader,
  28. TableItem,
  29. } from '#desktop/components/CommonSimpleTable/types.ts'
  30. import LayoutContent from '#desktop/components/layout/LayoutContent.vue'
  31. import { useCheckTokenAccess } from '../composables/permission/useCheckTokenAccess.ts'
  32. import { useBreadcrumb } from '../composables/useBreadcrumb.ts'
  33. import { UserCurrentAccessTokenUpdatesDocument } from '../graphql/subscriptions/userCurrentAccessTokenUpdates.api.ts'
  34. defineOptions({
  35. beforeRouteEnter() {
  36. const { canUseAccessToken } = useCheckTokenAccess()
  37. if (canUseAccessToken.value) return true
  38. redirectToError(useRouter())
  39. },
  40. })
  41. const session = useSessionStore()
  42. const { breadcrumbItems } = useBreadcrumb(__('Token Access'))
  43. const newAccessTokenFlyout = useFlyout({
  44. name: 'new-access-token',
  45. component: () =>
  46. import('../components/PersonalSettingNewAccessTokenFlyout.vue'),
  47. })
  48. const accessTokenListQuery = new QueryHandler(
  49. useUserCurrentAccessTokenListQuery(),
  50. )
  51. const accessTokenListQueryResult = accessTokenListQuery.result()
  52. const accessTokenListLoading = accessTokenListQuery.loading()
  53. accessTokenListQuery.subscribeToMore<
  54. UserCurrentAccessTokenUpdatesSubscriptionVariables,
  55. UserCurrentAccessTokenUpdatesSubscription
  56. >({
  57. document: UserCurrentAccessTokenUpdatesDocument,
  58. variables: {
  59. userId: session.user?.id || '',
  60. },
  61. updateQuery: (prev, { subscriptionData }) => {
  62. if (!subscriptionData.data?.userCurrentAccessTokenUpdates.tokens) {
  63. return null as unknown as UserCurrentAccessTokenListQuery
  64. }
  65. return {
  66. userCurrentAccessTokenList:
  67. subscriptionData.data.userCurrentAccessTokenUpdates.tokens,
  68. }
  69. },
  70. })
  71. const tableHeaders: TableHeader[] = [
  72. {
  73. key: 'name',
  74. label: __('Name'),
  75. truncate: true,
  76. },
  77. {
  78. key: 'permissions',
  79. label: __('Permissions'),
  80. truncate: true,
  81. },
  82. {
  83. key: 'createdAt',
  84. label: __('Created'),
  85. type: 'timestamp',
  86. },
  87. {
  88. key: 'expiresAt',
  89. label: __('Expires'),
  90. type: 'timestamp',
  91. },
  92. {
  93. key: 'lastUsedAt',
  94. label: __('Last Used'),
  95. type: 'timestamp',
  96. },
  97. ]
  98. const { notify } = useNotifications()
  99. const { waitForVariantConfirmation } = useConfirmation()
  100. const deleteDevice = (accessToken: Token) => {
  101. const accessTokenDeleteMutation = new MutationHandler(
  102. useUserCurrentAccessTokenDeleteMutation(() => ({
  103. variables: {
  104. tokenId: accessToken.id,
  105. },
  106. update(cache) {
  107. cache.evict({ id: cache.identify(accessToken) })
  108. cache.gc()
  109. },
  110. })),
  111. {
  112. errorNotificationMessage: __(
  113. 'The personal access token could not be deleted.',
  114. ),
  115. },
  116. )
  117. accessTokenDeleteMutation.send().then(() => {
  118. notify({
  119. id: 'personal-access-token-removed',
  120. type: NotificationTypes.Success,
  121. message: __('Personal access token has been deleted.'),
  122. })
  123. })
  124. }
  125. const confirmDeleteDevice = async (accessToken: Token) => {
  126. const confirmed = await waitForVariantConfirmation('delete')
  127. if (confirmed) deleteDevice(accessToken)
  128. }
  129. const tableActions: MenuItem[] = [
  130. {
  131. key: 'delete',
  132. label: __('Delete this access token'),
  133. icon: 'trash3',
  134. variant: 'danger',
  135. onClick: (data) => {
  136. confirmDeleteDevice(data as Token)
  137. },
  138. },
  139. ]
  140. const currentAccessTokens = computed<TableItem[]>(() => {
  141. return (
  142. accessTokenListQueryResult.value?.userCurrentAccessTokenList || []
  143. ).map((accessToken) => {
  144. return {
  145. ...accessToken,
  146. permissions: accessToken.preferences?.permission?.join(', ') || '',
  147. }
  148. })
  149. })
  150. const currentAccessTokenPresent = computed(
  151. () => currentAccessTokens.value.length > 0,
  152. )
  153. const helpText = computed(() => [
  154. i18n.t(
  155. 'You can generate a personal access token for each application you use that needs access to the Zammad API.',
  156. ),
  157. i18n.t("Pick a name for the application, and we'll give you a unique token."),
  158. ])
  159. </script>
  160. <template>
  161. <LayoutContent
  162. :help-text="helpText"
  163. :show-inline-help="!currentAccessTokenPresent && !accessTokenListLoading"
  164. :breadcrumb-items="breadcrumbItems"
  165. width="narrow"
  166. >
  167. <template #headerRight>
  168. <div class="flex flex-row gap-2">
  169. <CommonButton
  170. prefix-icon="key"
  171. variant="primary"
  172. size="medium"
  173. @click="newAccessTokenFlyout.open()"
  174. >
  175. {{ $t('New Personal Access Token') }}
  176. </CommonButton>
  177. </div>
  178. </template>
  179. <CommonLoader :loading="accessTokenListLoading">
  180. <div class="mb-4">
  181. <CommonSimpleTable
  182. :headers="tableHeaders"
  183. :items="currentAccessTokens"
  184. :actions="tableActions"
  185. :aria-label="$t('Personal Access Tokens')"
  186. class="min-w-150"
  187. >
  188. <template #item-suffix-name="{ item }">
  189. <CommonBadge
  190. v-if="item.current"
  191. size="medium"
  192. variant="info"
  193. class="ltr:ml-2 rtl:mr-2"
  194. >{{ $t('This device') }}
  195. </CommonBadge>
  196. </template>
  197. </CommonSimpleTable>
  198. </div>
  199. </CommonLoader>
  200. </LayoutContent>
  201. </template>