LoginTwoFactor.vue 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { computed, onMounted, ref } from 'vue'
  4. import Form from '#shared/components/Form/Form.vue'
  5. import type {
  6. FormSubmitData,
  7. FormSchemaNode,
  8. } from '#shared/components/Form/types.ts'
  9. import type {
  10. TwoFactorLoginFormData,
  11. LoginCredentials,
  12. TwoFactorPlugin,
  13. } from '#shared/entities/two-factor/types.ts'
  14. import UserError from '#shared/errors/UserError.ts'
  15. import { useTwoFactorMethodInitiateAuthenticationMutation } from '#shared/graphql/mutations/twoFactorMethodInitiateAuthentication.api.ts'
  16. import MutationHandler from '#shared/server/apollo/handler/MutationHandler.ts'
  17. import { useAuthenticationStore } from '#shared/stores/authentication.ts'
  18. import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
  19. import CommonLoader from '#desktop/components/CommonLoader/CommonLoader.vue'
  20. export interface Props {
  21. credentials: LoginCredentials
  22. twoFactor: TwoFactorPlugin
  23. }
  24. const props = defineProps<Props>()
  25. const emit = defineEmits<{
  26. finish: []
  27. error: [error: UserError]
  28. 'clear-error': []
  29. }>()
  30. const twoFactorLoginOptions = computed(() => props.twoFactor.loginOptions)
  31. const schema: FormSchemaNode[] = [
  32. {
  33. type: 'text',
  34. name: 'code',
  35. label: __('Security Code'),
  36. required: true,
  37. props: {
  38. help: computed(() => twoFactorLoginOptions.value.helpMessage),
  39. autocomplete: 'one-time-code',
  40. autofocus: true,
  41. inputmode: 'numeric',
  42. pattern: '[0-9]*',
  43. },
  44. },
  45. ]
  46. const authentication = useAuthenticationStore()
  47. const loading = ref(false)
  48. const error = ref<string | null>(null)
  49. const canRetry = ref(true)
  50. const login = (payload: unknown) => {
  51. emit('clear-error')
  52. const { login, password, rememberMe } = props.credentials
  53. return authentication
  54. .login({
  55. login,
  56. password,
  57. rememberMe,
  58. twoFactorAuthentication: {
  59. payload,
  60. method: props.twoFactor.name,
  61. },
  62. })
  63. .then(() => {
  64. canRetry.value = false
  65. emit('finish')
  66. })
  67. .catch((error: UserError) => {
  68. if (error instanceof UserError) {
  69. emit('error', error)
  70. }
  71. })
  72. }
  73. const tryMethod = async () => {
  74. if (!twoFactorLoginOptions.value.setup) return
  75. const initialDataMutation = new MutationHandler(
  76. useTwoFactorMethodInitiateAuthenticationMutation(),
  77. )
  78. emit('clear-error')
  79. error.value = null
  80. loading.value = true
  81. try {
  82. const initiated = await initialDataMutation.send({
  83. twoFactorMethod: props.twoFactor.name,
  84. password: props.credentials.password,
  85. login: props.credentials.login,
  86. })
  87. if (!initiated?.twoFactorMethodInitiateAuthentication?.initiationData) {
  88. error.value = __(
  89. 'Two-factor authentication method could not be initiated.',
  90. )
  91. return
  92. }
  93. const result = await twoFactorLoginOptions.value.setup(
  94. initiated.twoFactorMethodInitiateAuthentication.initiationData,
  95. )
  96. canRetry.value = result.retry ?? true
  97. if (result?.success) {
  98. await login(result.payload)
  99. } else if (result?.error) {
  100. error.value = result.error
  101. }
  102. } catch (err) {
  103. if (err instanceof UserError) {
  104. error.value = err.errors[0].message
  105. }
  106. } finally {
  107. loading.value = false
  108. }
  109. }
  110. onMounted(async () => {
  111. await tryMethod()
  112. })
  113. </script>
  114. <template>
  115. <Form
  116. v-if="twoFactorLoginOptions.form !== false"
  117. :schema="schema"
  118. @submit="login(($event as FormSubmitData<TwoFactorLoginFormData>).code)"
  119. >
  120. <template #after-fields>
  121. <CommonButton
  122. type="submit"
  123. variant="submit"
  124. size="large"
  125. class="mt-8"
  126. block
  127. :disabled="loading"
  128. >
  129. {{ $t('Sign in') }}
  130. </CommonButton>
  131. </template>
  132. </Form>
  133. <section
  134. v-else-if="twoFactorLoginOptions.setup"
  135. class="flex flex-col items-center justify-center"
  136. >
  137. <CommonLabel
  138. v-if="error && twoFactorLoginOptions.errorHelpMessage"
  139. class="mt-5"
  140. >
  141. {{ $t(twoFactorLoginOptions.errorHelpMessage) }}
  142. </CommonLabel>
  143. <CommonLabel v-else-if="twoFactorLoginOptions.helpMessage" class="mt-5">
  144. {{ $t(twoFactorLoginOptions.helpMessage) }}
  145. </CommonLabel>
  146. <CommonLoader class="mb-3 mt-8" :loading="loading" :error="error" />
  147. <CommonButton
  148. v-if="!loading && canRetry"
  149. size="large"
  150. variant="primary"
  151. class="mt-5"
  152. block
  153. @click="tryMethod"
  154. >
  155. {{ $t('Retry') }}
  156. </CommonButton>
  157. </section>
  158. </template>