TOTP.vue 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. <script setup lang="ts">
  2. import twoFA from '@/api/2fa'
  3. import otp from '@/api/otp'
  4. import OTPInput from '@/components/OTPInput/OTPInput.vue'
  5. import { CheckCircleOutlined } from '@ant-design/icons-vue'
  6. import { UseClipboard } from '@vueuse/components'
  7. import { message } from 'ant-design-vue'
  8. const status = ref(false)
  9. const enrolling = ref(false)
  10. const resetting = ref(false)
  11. const generatedUrl = ref('')
  12. const secret = ref('')
  13. const passcode = ref('')
  14. const refOtp = useTemplateRef('refOtp')
  15. const recoveryCode = ref('')
  16. const inputRecoveryCode = ref('')
  17. function clickEnable2FA() {
  18. enrolling.value = true
  19. generateSecret()
  20. }
  21. function generateSecret() {
  22. otp.generate_secret().then(r => {
  23. secret.value = r.secret
  24. generatedUrl.value = r.url
  25. refOtp.value?.clearInput()
  26. })
  27. }
  28. function enroll(code: string) {
  29. otp.enroll_otp(secret.value, code).then(r => {
  30. enrolling.value = false
  31. recoveryCode.value = r.recovery_code
  32. get2FAStatus()
  33. message.success($gettext('Enable 2FA successfully'))
  34. }).catch(() => {
  35. refOtp.value?.clearInput()
  36. })
  37. }
  38. function get2FAStatus() {
  39. twoFA.status().then(r => {
  40. status.value = r.otp_status
  41. })
  42. }
  43. get2FAStatus()
  44. function clickReset2FA() {
  45. resetting.value = true
  46. inputRecoveryCode.value = ''
  47. }
  48. function reset2FA() {
  49. otp.reset(inputRecoveryCode.value).then(() => {
  50. resetting.value = false
  51. recoveryCode.value = ''
  52. get2FAStatus()
  53. clickEnable2FA()
  54. })
  55. }
  56. </script>
  57. <template>
  58. <div>
  59. <h3>{{ $gettext('TOTP') }}</h3>
  60. <p>{{ $gettext('TOTP is a two-factor authentication method that uses a time-based one-time password algorithm.') }}</p>
  61. <p>{{ $gettext('To enable it, you need to install the Google or Microsoft Authenticator app on your mobile phone.') }}</p>
  62. <p>{{ $gettext('Scan the QR code with your mobile phone to add the account to the app.') }}</p>
  63. <AAlert v-if="!status" type="warning" :message="$gettext('Current account is not enabled TOTP.')" show-icon />
  64. <div v-else>
  65. <p><CheckCircleOutlined class="mr-2 text-green-600" />{{ $gettext('Current account is enabled TOTP.') }}</p>
  66. </div>
  67. <AAlert
  68. v-if="recoveryCode"
  69. :message="$gettext('Recovery Code')"
  70. class="mb-4"
  71. type="info"
  72. show-icon
  73. >
  74. <template #description>
  75. <div>
  76. <p>{{ $gettext('If you lose your mobile phone, you can use the recovery code to reset your 2FA.') }}</p>
  77. <p>{{ $gettext('The recovery code is only displayed once, please save it in a safe place.') }}</p>
  78. <p>{{ $gettext('Recovery Code:') }}</p>
  79. <span class="ml-2">{{ recoveryCode }}</span>
  80. </div>
  81. </template>
  82. </AAlert>
  83. <AButton
  84. v-if="!status && !enrolling"
  85. type="primary"
  86. ghost
  87. @click="clickEnable2FA"
  88. >
  89. {{ $gettext('Enable TOTP') }}
  90. </AButton>
  91. <AButton
  92. v-if="status && !resetting"
  93. type="primary"
  94. ghost
  95. @click="clickReset2FA"
  96. >
  97. {{ $gettext('Reset 2FA') }}
  98. </AButton>
  99. <template v-if="enrolling">
  100. <div class="flex flex-col items-center">
  101. <div class="mt-4 mb-2">
  102. <AQrcode
  103. v-if="generatedUrl"
  104. :value="generatedUrl"
  105. :size="256"
  106. />
  107. <div class="w-64 flex justify-center">
  108. <UseClipboard v-slot="{ copy, copied }">
  109. <a
  110. class="mr-2"
  111. @click="() => copy(secret)"
  112. >
  113. {{ copied ? $gettext('Secret has been copied')
  114. : $gettext('Can\'t scan? Use text key binding') }}
  115. </a>
  116. </UseClipboard>
  117. </div>
  118. </div>
  119. <div>
  120. <p>{{ $gettext('Input the code from the app:') }}</p>
  121. <OTPInput
  122. ref="refOtp"
  123. v-model="passcode"
  124. @on-complete="enroll"
  125. />
  126. </div>
  127. </div>
  128. </template>
  129. <div
  130. v-if="resetting"
  131. class="mt-2"
  132. >
  133. <p>{{ $gettext('Input the recovery code:') }}</p>
  134. <AInputGroup compact>
  135. <AInput v-model:value="inputRecoveryCode" />
  136. <AButton
  137. type="primary"
  138. @click="reset2FA"
  139. >
  140. {{ $gettext('Recovery') }}
  141. </AButton>
  142. </AInputGroup>
  143. </div>
  144. </div>
  145. </template>
  146. <style scoped lang="less">
  147. :deep(.ant-input-group.ant-input-group-compact) {
  148. display: flex;
  149. }
  150. </style>