Authorization.vue 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. <script setup lang="ts">
  2. import type { TwoFAStatusResponse } from '@/api/2fa'
  3. import twoFA from '@/api/2fa'
  4. import OTPInput from '@/components/OTPInput/OTPInput.vue'
  5. import { useUserStore } from '@/pinia'
  6. import { KeyOutlined } from '@ant-design/icons-vue'
  7. import { startAuthentication } from '@simplewebauthn/browser'
  8. import { message } from 'ant-design-vue'
  9. defineProps<{
  10. twoFAStatus: TwoFAStatusResponse
  11. }>()
  12. const emit = defineEmits(['submitOTP', 'submitSecureSessionID'])
  13. const user = useUserStore()
  14. const refOTP = ref()
  15. const useRecoveryCode = ref(false)
  16. const passcode = ref('')
  17. const recoveryCode = ref('')
  18. const passkeyLoading = ref(false)
  19. function clickUseRecoveryCode() {
  20. passcode.value = ''
  21. useRecoveryCode.value = true
  22. }
  23. function clickUseOTP() {
  24. passcode.value = ''
  25. useRecoveryCode.value = false
  26. }
  27. function onSubmit() {
  28. emit('submitOTP', passcode.value, recoveryCode.value)
  29. }
  30. function clearInput() {
  31. refOTP.value?.clearInput()
  32. }
  33. defineExpose({
  34. clearInput,
  35. })
  36. async function passkeyAuthenticate() {
  37. passkeyLoading.value = true
  38. try {
  39. const begin = await twoFA.begin_start_secure_session_by_passkey()
  40. const asseResp = await startAuthentication({ optionsJSON: begin.options.publicKey })
  41. const r = await twoFA.finish_start_secure_session_by_passkey({
  42. session_id: begin.session_id,
  43. options: asseResp,
  44. })
  45. emit('submitSecureSessionID', r.session_id)
  46. }
  47. // eslint-disable-next-line ts/no-explicit-any
  48. catch (e: any) {
  49. message.error($gettext(e.message ?? 'Server error'))
  50. }
  51. passkeyLoading.value = false
  52. }
  53. onMounted(() => {
  54. if (user.passkeyLoginAvailable)
  55. passkeyAuthenticate()
  56. })
  57. </script>
  58. <template>
  59. <div>
  60. <div v-if="twoFAStatus.otp_status">
  61. <div v-if="!useRecoveryCode">
  62. <p>{{ $gettext('Please enter the OTP code:') }}</p>
  63. <OTPInput
  64. ref="refOTP"
  65. v-model="passcode"
  66. class="justify-center mb-6"
  67. @on-complete="onSubmit"
  68. />
  69. </div>
  70. <div
  71. v-else
  72. class="mt-2 mb-4"
  73. >
  74. <p>{{ $gettext('Input the recovery code:') }}</p>
  75. <AInputGroup compact>
  76. <AInput v-model:value="recoveryCode" />
  77. <AButton
  78. type="primary"
  79. @click="onSubmit"
  80. >
  81. {{ $gettext('Recovery') }}
  82. </AButton>
  83. </AInputGroup>
  84. </div>
  85. <div class="flex justify-center">
  86. <a
  87. v-if="!useRecoveryCode"
  88. @click="clickUseRecoveryCode"
  89. >{{ $gettext('Use recovery code') }}</a>
  90. <a
  91. v-else
  92. @click="clickUseOTP"
  93. >{{ $gettext('Use OTP') }}</a>
  94. </div>
  95. </div>
  96. <div
  97. v-if="twoFAStatus.passkey_status"
  98. class="flex flex-col justify-center"
  99. >
  100. <ADivider v-if="twoFAStatus.otp_status">
  101. <div class="text-sm font-normal opacity-75">
  102. {{ $gettext('Or') }}
  103. </div>
  104. </ADivider>
  105. <AButton
  106. :loading="passkeyLoading"
  107. @click="passkeyAuthenticate"
  108. >
  109. <KeyOutlined />
  110. {{ $gettext('Authenticate with a passkey') }}
  111. </AButton>
  112. </div>
  113. </div>
  114. </template>
  115. <style scoped lang="less">
  116. :deep(.ant-input-group.ant-input-group-compact) {
  117. display: flex;
  118. }
  119. </style>