PersonalSettingLinkedAccounts.vue 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { storeToRefs } from 'pinia'
  4. import { computed, ref } from 'vue'
  5. import {
  6. NotificationTypes,
  7. useNotifications,
  8. } from '#shared/components/CommonNotifications/index.ts'
  9. import { useThirdPartyAuthentication } from '#shared/composables/authentication/useThirdPartyAuthentication.ts'
  10. import { useConfirmation } from '#shared/composables/useConfirmation.ts'
  11. import useFingerprint from '#shared/composables/useFingerprint.ts'
  12. import {
  13. type Authorization,
  14. EnumAuthenticationProvider,
  15. } from '#shared/graphql/types.ts'
  16. import { i18n } from '#shared/i18n.ts'
  17. import { MutationHandler } from '#shared/server/apollo/handler/index.ts'
  18. import { useSessionStore } from '#shared/stores/session.ts'
  19. import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
  20. import type { MenuItem } from '#desktop/components/CommonPopoverMenu/types.ts'
  21. import CommonSimpleTable from '#desktop/components/CommonSimpleTable/CommonSimpleTable.vue'
  22. import type {
  23. TableHeader,
  24. TableItem,
  25. } from '#desktop/components/CommonSimpleTable/types.ts'
  26. import CommonThirdPartyAuthenticationButton from '#desktop/components/CommonThirdPartyAuthenticationButton/CommonThirdPartyAuthenticationButton.vue'
  27. import LayoutContent from '#desktop/components/layout/LayoutContent.vue'
  28. import { useBreadcrumb } from '#desktop/pages/personal-setting/composables/useBreadcrumb.ts'
  29. import { useUserCurrentRemoveLinkedAccountMutation } from '#desktop/pages/personal-setting/graphql/mutations/userCurrentLinkedAccount.api.ts'
  30. import type { LinkedAccountTableItem } from '#desktop/pages/personal-setting/types/linked-accounts.ts'
  31. defineOptions({
  32. beforeRouteEnter() {
  33. const { hasEnabledProviders } = useThirdPartyAuthentication()
  34. if (!hasEnabledProviders.value) return '/error'
  35. return true
  36. },
  37. })
  38. const { notify } = useNotifications()
  39. const { breadcrumbItems } = useBreadcrumb(__('Linked Accounts'))
  40. const { user } = storeToRefs(useSessionStore())
  41. const { enabledProviders } = useThirdPartyAuthentication()
  42. const { fingerprint } = useFingerprint()
  43. const providersLookup = computed(() => {
  44. if (!user.value?.authorizations) return []
  45. const { authorizations } = user.value
  46. return enabledProviders.value.map((enabledProvider) => {
  47. const configuredProvider = authorizations.find(
  48. ({ provider }) => provider === enabledProvider.name,
  49. )
  50. return {
  51. ...enabledProvider,
  52. uid: configuredProvider?.uid,
  53. username: configuredProvider?.username || configuredProvider?.uid,
  54. authorizationId: configuredProvider?.id,
  55. }
  56. })
  57. })
  58. const tableHeaders: TableHeader[] = [
  59. {
  60. key: 'application',
  61. label: __('Application'),
  62. },
  63. {
  64. key: 'username',
  65. label: __('Username'),
  66. truncate: true,
  67. },
  68. ]
  69. const tableItems = computed<TableItem[]>(() =>
  70. providersLookup.value.map((provider, index) => ({
  71. id: `${index}-${provider.name}`,
  72. application: provider.label,
  73. ...provider,
  74. })),
  75. )
  76. const loading = ref(false)
  77. const unlinkMutation = async (
  78. authId: string,
  79. authProvider: EnumAuthenticationProvider,
  80. uid: string,
  81. ) => {
  82. return new MutationHandler(
  83. useUserCurrentRemoveLinkedAccountMutation(() => ({
  84. update(cache) {
  85. if (user.value === null) return
  86. // Evict authorization cache to align in-memory cache
  87. const normalizedId = cache.identify({
  88. authId,
  89. __typename: 'Authorization',
  90. })
  91. cache.evict({ id: normalizedId })
  92. // Identify current user cache and update authorizations field to align in-memory cache
  93. cache.modify({
  94. id: cache.identify(user.value),
  95. fields: {
  96. authorizations(existingAuthorizations, { readField }) {
  97. return existingAuthorizations.filter(
  98. (auth: Authorization) => readField('id', auth) !== authId,
  99. )
  100. },
  101. },
  102. })
  103. cache.gc()
  104. },
  105. })),
  106. ).send({
  107. provider: authProvider,
  108. uid,
  109. })
  110. }
  111. const { waitForVariantConfirmation } = useConfirmation()
  112. const unlinkAccount = async (providerTableItem: LinkedAccountTableItem) => {
  113. const confirmed = await waitForVariantConfirmation('delete')
  114. if (!confirmed) return
  115. try {
  116. loading.value = true
  117. const response = await unlinkMutation(
  118. providerTableItem.authorizationId,
  119. providerTableItem.name,
  120. providerTableItem.uid,
  121. )
  122. if (!response?.userCurrentRemoveLinkedAccount) return
  123. const { success } = response.userCurrentRemoveLinkedAccount
  124. if (success)
  125. notify({
  126. id: 'linked-account-removed',
  127. type: NotificationTypes.Success,
  128. message: __('The account link was successfully removed!'),
  129. })
  130. } finally {
  131. loading.value = false
  132. }
  133. }
  134. const tableActions = computed((): MenuItem[] => [
  135. {
  136. key: 'delete',
  137. icon: 'trash3',
  138. variant: 'danger',
  139. ariaLabel: (provider) =>
  140. i18n.t('Remove account link on %s', provider?.application),
  141. show: (provider) => !!provider?.username,
  142. onClick: (provider) => unlinkAccount(provider as LinkedAccountTableItem),
  143. },
  144. {
  145. key: 'setup',
  146. icon: 'plus-square-fill',
  147. variant: 'secondary',
  148. ariaLabel: (provider) =>
  149. i18n.t('Link account on %s', provider?.application),
  150. show: (provider) => !provider?.username,
  151. },
  152. ])
  153. </script>
  154. <template>
  155. <LayoutContent :breadcrumb-items="breadcrumbItems" width="narrow">
  156. <CommonSimpleTable
  157. :headers="tableHeaders"
  158. :items="tableItems"
  159. :actions="tableActions"
  160. >
  161. <template #actions="{ actions, item }">
  162. <div class="flex items-center justify-center">
  163. <template v-for="action in actions" :key="action.key">
  164. <CommonThirdPartyAuthenticationButton
  165. v-if="action.key === 'setup' && action.show?.(item)"
  166. button-class="flex"
  167. :button-icon="action.icon"
  168. :disabled="loading"
  169. button-size="medium"
  170. :button-label="(action?.ariaLabel as Function)(item)"
  171. :url="`${item?.url}?fingerprint=${fingerprint}`"
  172. />
  173. <CommonButton
  174. v-else-if="action.onClick && action.show?.(item)"
  175. :icon="action.icon"
  176. :disabled="loading"
  177. :class="{ '!bg-transparent': action.variant === 'danger' }"
  178. size="medium"
  179. :variant="action.variant"
  180. :aria-label="(action?.ariaLabel as Function)(item)"
  181. @click="action.onClick?.(item)"
  182. />
  183. </template>
  184. </div>
  185. </template>
  186. </CommonSimpleTable>
  187. </LayoutContent>
  188. </template>