PersonalSettingTwoFactorAuth.vue 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { computed, watch } from 'vue'
  4. import { useRouter } from 'vue-router'
  5. import {
  6. NotificationTypes,
  7. useNotifications,
  8. } from '#shared/components/CommonNotifications/index.ts'
  9. import { useApplicationConfigTwoFactor } from '#shared/composables/authentication/useApplicationConfigTwoFactor.ts'
  10. import type { TwoFactorActionTypes } from '#shared/entities/two-factor/types.ts'
  11. import { useUserCurrentTwoFactorRemoveMethodMutation } from '#shared/entities/user/current/graphql/mutations/two-factor/userCurrentTwoFactorRemoveMethod.api.ts'
  12. import { useUserCurrentTwoFactorSetDefaultMethodMutation } from '#shared/entities/user/current/graphql/mutations/two-factor/userCurrentTwoFactorSetDefaultMethod.api.ts'
  13. import { MutationHandler } from '#shared/server/apollo/handler/index.ts'
  14. import { useSessionStore } from '#shared/stores/session.ts'
  15. import type { ObjectLike } from '#shared/types/utils.ts'
  16. import CommonActionMenu from '#desktop/components/CommonActionMenu/CommonActionMenu.vue'
  17. import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
  18. import { useFlyout } from '#desktop/components/CommonFlyout/useFlyout.ts'
  19. import type { MenuItem } from '#desktop/components/CommonPopoverMenu/types.ts'
  20. import LayoutContent from '#desktop/components/layout/LayoutContent.vue'
  21. import type { TwoFactorConfigurationType } from '#desktop/components/TwoFactor/types.ts'
  22. import { useConfigurationTwoFactor } from '#desktop/entities/two-factor-configuration/composables/useConfigurationTwoFactor.ts'
  23. import { useBreadcrumb } from '../composables/useBreadcrumb.ts'
  24. defineOptions({
  25. beforeRouteEnter() {
  26. const { hasEnabledMethods } = useApplicationConfigTwoFactor()
  27. if (!hasEnabledMethods.value) return '/error'
  28. return true
  29. },
  30. })
  31. const session = useSessionStore()
  32. const router = useRouter()
  33. const { notify } = useNotifications()
  34. const {
  35. hasEnabledMethods,
  36. hasEnabledRecoveryCodes,
  37. hasConfiguredMethods,
  38. hasRecoveryCodes,
  39. twoFactorConfigurationMethods,
  40. } = useConfigurationTwoFactor()
  41. watch(hasEnabledMethods, (newValue) => {
  42. if (newValue) return
  43. router.replace('/error')
  44. })
  45. const { breadcrumbItems } = useBreadcrumb(__('Two-factor Authentication'))
  46. const twoFactorConfigurationFlyout = useFlyout({
  47. name: 'two-factor-flyout',
  48. component: () =>
  49. import('#desktop/components/TwoFactor/TwoFactorConfigurationFlyout.vue'),
  50. })
  51. const openTwoFactorConfigurationFlyout = async (
  52. type: TwoFactorConfigurationType,
  53. ) => {
  54. return twoFactorConfigurationFlyout.open({
  55. type,
  56. })
  57. }
  58. const setDefaultTwoFactorMethod = new MutationHandler(
  59. useUserCurrentTwoFactorSetDefaultMethodMutation(),
  60. {
  61. errorNotificationMessage: __(
  62. 'Could not set two-factor authentication method as default',
  63. ),
  64. },
  65. )
  66. const submitTwoFactorDefaultMethod = (entity?: ObjectLike) => {
  67. if (!entity) return
  68. setDefaultTwoFactorMethod
  69. .send({
  70. methodName: entity.name,
  71. })
  72. .then(() => {
  73. session.setUserPreference('two_factor_authentication', {
  74. ...(session.user?.preferences?.two_factor_authentication || {}),
  75. default: entity.name,
  76. })
  77. notify({
  78. id: 'two-factor-method-set-default',
  79. type: NotificationTypes.Success,
  80. message: __('Two-factor authentication method was set as default.'),
  81. })
  82. })
  83. }
  84. const removeTwoFactorMethod = new MutationHandler(
  85. useUserCurrentTwoFactorRemoveMethodMutation(),
  86. {
  87. errorNotificationMessage: __(
  88. 'Could not remove two-factor authentication method.',
  89. ),
  90. },
  91. )
  92. const submitTwoFactorMethodRemoval = async (entity?: ObjectLike) => {
  93. if (!entity) return
  94. return twoFactorConfigurationFlyout.open({
  95. type: 'removal_confirmation',
  96. successCallback: async () => {
  97. const data = await removeTwoFactorMethod.send({
  98. methodName: entity.name,
  99. })
  100. if (data?.userCurrentTwoFactorRemoveMethod?.success) {
  101. notify({
  102. id: 'two-factor-method-removed',
  103. type: NotificationTypes.Success,
  104. message: __('Two-factor authentication method was removed.'),
  105. })
  106. }
  107. },
  108. })
  109. }
  110. const lookUpA11yActionLabel = (
  111. entity: ObjectLike,
  112. type: TwoFactorActionTypes,
  113. ) => {
  114. const authenticatorMethod = twoFactorConfigurationMethods.value.find(
  115. (method) => method.name === entity.name,
  116. )
  117. return (
  118. authenticatorMethod?.configurationOptions?.getActionA11yLabel(type) || ''
  119. )
  120. }
  121. const actions = computed<MenuItem[]>(() => [
  122. {
  123. key: 'setup',
  124. label: __('Set up'),
  125. ariaLabel: (entity) => lookUpA11yActionLabel(entity!, 'setup'),
  126. icon: 'wrench',
  127. show: (entity) => !entity?.configured,
  128. onClick: (entity) => openTwoFactorConfigurationFlyout(entity?.name),
  129. },
  130. {
  131. key: 'edit',
  132. label: __('Edit'),
  133. ariaLabel: (entity) => lookUpA11yActionLabel(entity!, 'edit'),
  134. icon: 'pencil',
  135. show: (entity) => {
  136. return Boolean(
  137. entity?.configured && entity?.configurationOptions.editable,
  138. )
  139. },
  140. onClick: (entity) => openTwoFactorConfigurationFlyout(entity?.name),
  141. },
  142. {
  143. key: 'setAsDefault',
  144. label: __('Set as default'),
  145. ariaLabel: (entity) => lookUpA11yActionLabel(entity!, 'default'),
  146. icon: 'arrow-repeat',
  147. show: (entity) => Boolean(entity?.configured && !entity?.default),
  148. onClick: (entity) => submitTwoFactorDefaultMethod(entity),
  149. },
  150. {
  151. key: 'remove',
  152. label: __('Remove'),
  153. ariaLabel: (entity) => lookUpA11yActionLabel(entity!, 'remove'),
  154. icon: 'trash3',
  155. variant: 'danger',
  156. show: (entity) => Boolean(entity?.configured),
  157. onClick: (entity) => submitTwoFactorMethodRemoval(entity),
  158. },
  159. ])
  160. </script>
  161. <template>
  162. <LayoutContent :breadcrumb-items="breadcrumbItems" width="narrow">
  163. <div class="flex flex-col gap-2.5">
  164. <div>
  165. <CommonLabel class="mb-1.5">{{ $t('Available methods') }}</CommonLabel>
  166. <div class="flex flex-col rounded-lg bg-blue-200 p-1 dark:bg-gray-700">
  167. <div
  168. v-for="twoFactorMethod in twoFactorConfigurationMethods"
  169. :key="twoFactorMethod.name"
  170. class="flex items-start gap-1.5 p-2.5"
  171. >
  172. <CommonIcon
  173. class="text-stone-200 dark:text-neutral-500"
  174. :name="twoFactorMethod.icon"
  175. size="small"
  176. />
  177. <div class="flex grow flex-col gap-0.5">
  178. <div class="flex grow gap-1.5">
  179. <CommonLabel class="text-black dark:text-white"
  180. >{{ $t(twoFactorMethod.label) }}
  181. </CommonLabel>
  182. <CommonBadge
  183. v-if="twoFactorMethod.configured"
  184. variant="success"
  185. >
  186. {{ $t('Active') }}
  187. </CommonBadge>
  188. <CommonBadge v-if="twoFactorMethod.default" variant="info"
  189. >{{ $t('Default') }}
  190. </CommonBadge>
  191. </div>
  192. <CommonLabel
  193. v-if="twoFactorMethod.description"
  194. class="text-stone-200 dark:text-neutral-500"
  195. size="small"
  196. >{{ $t(twoFactorMethod.description) }}
  197. </CommonLabel>
  198. </div>
  199. <CommonActionMenu
  200. :entity="twoFactorMethod"
  201. :custom-menu-button-label="
  202. twoFactorMethod.configurationOptions?.actionButtonA11yLabel
  203. "
  204. :actions="actions"
  205. />
  206. </div>
  207. </div>
  208. </div>
  209. <template v-if="hasConfiguredMethods && hasEnabledRecoveryCodes">
  210. <CommonLabel
  211. >{{
  212. $t(
  213. 'Recovery codes can be used to access your account in the event you lose access to other two-factor authentication methods.',
  214. )
  215. }}
  216. </CommonLabel>
  217. <CommonLabel v-if="hasRecoveryCodes"
  218. >{{
  219. $t(
  220. "If you lose your recovery codes it's possible to generate new ones. This action is going to invalidate previous recovery codes.",
  221. )
  222. }}
  223. </CommonLabel>
  224. <div class="flex justify-end">
  225. <CommonButton
  226. variant="submit"
  227. type="submit"
  228. size="medium"
  229. @click="openTwoFactorConfigurationFlyout('recovery_codes')"
  230. >
  231. {{
  232. hasRecoveryCodes
  233. ? $t('Regenerate Recovery Codes')
  234. : $t('Generate Recovery Codes')
  235. }}
  236. </CommonButton>
  237. </div>
  238. </template>
  239. </div>
  240. </LayoutContent>
  241. </template>