Authorization.vue 3.1 KB

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