PersonalSettingTokenAccess.vue 6.5 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 { NotificationTypes } from '#shared/components/CommonNotifications/types.ts'
  5. import { useNotifications } from '#shared/components/CommonNotifications/useNotifications.ts'
  6. import { useConfirmation } from '#shared/composables/useConfirmation.ts'
  7. import { useUserCurrentAccessTokenDeleteMutation } from '#shared/entities/user/current/graphql/mutations/userCurrentAccessTokenDelete.api.ts'
  8. import { useUserCurrentAccessTokenListQuery } from '#shared/entities/user/current/graphql/queries/userCurrentAcessTokenList.api.ts'
  9. import type {
  10. Token,
  11. UserCurrentAccessTokenUpdatesSubscription,
  12. UserCurrentAccessTokenUpdatesSubscriptionVariables,
  13. UserCurrentAccessTokenListQuery,
  14. } from '#shared/graphql/types.ts'
  15. import { i18n } from '#shared/i18n/index.ts'
  16. import MutationHandler from '#shared/server/apollo/handler/MutationHandler.ts'
  17. import QueryHandler from '#shared/server/apollo/handler/QueryHandler.ts'
  18. import { useSessionStore } from '#shared/stores/session.ts'
  19. import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
  20. import { useFlyout } from '#desktop/components/CommonFlyout/useFlyout.ts'
  21. import CommonLoader from '#desktop/components/CommonLoader/CommonLoader.vue'
  22. import type { MenuItem } from '#desktop/components/CommonPopover/types.ts'
  23. import CommonSimpleTable from '#desktop/components/CommonSimpleTable/CommonSimpleTable.vue'
  24. import type {
  25. TableHeader,
  26. TableItem,
  27. } from '#desktop/components/CommonSimpleTable/types.ts'
  28. import LayoutContent from '#desktop/components/layout/LayoutContent.vue'
  29. import { useCheckTokenAccess } from '../composables/permission/useCheckTokenAccess.ts'
  30. import { useBreadcrumb } from '../composables/useBreadcrumb.ts'
  31. import { UserCurrentAccessTokenUpdatesDocument } from '../graphql/subscriptions/userCurrentAccessTokenUpdates.api.ts'
  32. defineOptions({
  33. beforeRouteEnter() {
  34. const { canUseAccessToken } = useCheckTokenAccess()
  35. if (!canUseAccessToken.value) {
  36. // TODO: Redirect to error page using redirectToError or something similar.
  37. return '/error'
  38. }
  39. return true
  40. },
  41. })
  42. const session = useSessionStore()
  43. const { breadcrumbItems } = useBreadcrumb(__('Token Access'))
  44. const newAccessTokenFlyout = useFlyout({
  45. name: 'new-access-token',
  46. component: () =>
  47. import('../components/PersonalSettingNewAccessTokenFlyout.vue'),
  48. })
  49. const accessTokenListQuery = new QueryHandler(
  50. useUserCurrentAccessTokenListQuery(),
  51. )
  52. const accessTokenListQueryResult = accessTokenListQuery.result()
  53. const accessTokenListLoading = accessTokenListQuery.loading()
  54. accessTokenListQuery.subscribeToMore<
  55. UserCurrentAccessTokenUpdatesSubscriptionVariables,
  56. UserCurrentAccessTokenUpdatesSubscription
  57. >({
  58. document: UserCurrentAccessTokenUpdatesDocument,
  59. variables: {
  60. userId: session.user?.id || '',
  61. },
  62. updateQuery: (prev, { subscriptionData }) => {
  63. if (!subscriptionData.data?.userCurrentAccessTokenUpdates.tokens) {
  64. return null as unknown as UserCurrentAccessTokenListQuery
  65. }
  66. return {
  67. userCurrentAccessTokenList:
  68. subscriptionData.data.userCurrentAccessTokenUpdates.tokens,
  69. }
  70. },
  71. })
  72. const tableHeaders: TableHeader[] = [
  73. {
  74. key: 'name',
  75. label: __('Name'),
  76. truncate: true,
  77. },
  78. {
  79. key: 'permissions',
  80. label: __('Permissions'),
  81. truncate: true,
  82. },
  83. {
  84. key: 'createdAt',
  85. label: __('Created'),
  86. type: 'timestamp',
  87. },
  88. {
  89. key: 'expiresAt',
  90. label: __('Expires'),
  91. type: 'timestamp',
  92. },
  93. {
  94. key: 'lastUsedAt',
  95. label: __('Last Used'),
  96. type: 'timestamp',
  97. },
  98. ]
  99. const { notify } = useNotifications()
  100. const { waitForVariantConfirmation } = useConfirmation()
  101. const deleteDevice = (accessToken: Token) => {
  102. const accessTokenDeleteMutation = new MutationHandler(
  103. useUserCurrentAccessTokenDeleteMutation(() => ({
  104. variables: {
  105. tokenId: accessToken.id,
  106. },
  107. update(cache) {
  108. cache.evict({ id: cache.identify(accessToken) })
  109. cache.gc()
  110. },
  111. })),
  112. {
  113. errorNotificationMessage: __(
  114. 'The personal access token could not be deleted.',
  115. ),
  116. },
  117. )
  118. accessTokenDeleteMutation.send().then(() => {
  119. notify({
  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>