RecoveryCodes.vue 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. <script setup lang="ts">
  2. import type { TwoFAStatus } from '@/api/2fa'
  3. import type { RecoveryCode } from '@/api/recovery'
  4. import recovery from '@/api/recovery'
  5. import { use2FAModal } from '@/components/TwoFA'
  6. import { CopyOutlined, WarningOutlined } from '@ant-design/icons-vue'
  7. import { UseClipboard } from '@vueuse/components'
  8. import { message } from 'ant-design-vue'
  9. const props = defineProps<{
  10. recoveryCodes?: RecoveryCode[]
  11. twoFAStatus?: TwoFAStatus
  12. }>()
  13. const emit = defineEmits<{
  14. refresh: [void]
  15. }>()
  16. const _codes = ref<RecoveryCode[]>()
  17. const codes = computed(() => _codes.value ?? props.recoveryCodes)
  18. const newGenerated = ref(false)
  19. const codeSource = computed(() => codes.value?.map(code => code.code).join('\n'))
  20. function clickGenerateRecoveryCodes() {
  21. const otpModal = use2FAModal()
  22. otpModal.open().then(() => {
  23. recovery.generate().then(r => {
  24. _codes.value = r.codes
  25. newGenerated.value = true
  26. emit('refresh')
  27. message.success($gettext('Generate recovery codes successfully'))
  28. })
  29. })
  30. }
  31. function clickViewRecoveryCodes() {
  32. const otpModal = use2FAModal()
  33. otpModal.open().then(() => {
  34. recovery.view().then(r => {
  35. _codes.value = r.codes
  36. })
  37. })
  38. }
  39. const popOpen = ref(false)
  40. function popConfirm() {
  41. popOpen.value = false
  42. clickGenerateRecoveryCodes()
  43. }
  44. function handlePopOpenChange(visible: boolean) {
  45. popOpen.value = visible
  46. if (!visible)
  47. return
  48. if (props.twoFAStatus?.recovery_codes_generated)
  49. popOpen.value = true
  50. else
  51. popConfirm()
  52. }
  53. </script>
  54. <template>
  55. <div>
  56. <h3 class="flex items-center gap-2">
  57. <span>{{ $gettext('Recovery Codes') }}</span>
  58. <ATag v-if="recoveryCodes || twoFAStatus?.recovery_codes_viewed" :color="newGenerated || recoveryCodes ? 'success' : 'processing'">
  59. {{ newGenerated || recoveryCodes ? $gettext('First View') : $gettext('Viewed') }}
  60. </ATag>
  61. </h3>
  62. <p>{{ $gettext('Recovery codes are used to access your account when you lose access to your 2FA device. Each code can only be used once.') }}</p>
  63. <p>{{ $gettext('Keep your recovery codes as safe as your password. We recommend saving them with a password manager.') }}</p>
  64. <AAlert
  65. v-if="!twoFAStatus?.enabled"
  66. class="mb-4"
  67. type="info"
  68. show-icon
  69. :message="$gettext('You have not enabled 2FA yet. Please enable 2FA to generate recovery codes.')"
  70. />
  71. <AAlert
  72. v-else-if="!twoFAStatus?.recovery_codes_generated"
  73. class="mb-4"
  74. type="warning"
  75. show-icon
  76. >
  77. <template #message>
  78. <template v-if="twoFAStatus?.otp_status">
  79. {{ $gettext('Your current recovery code might be outdated and insecure. Please generate new recovery codes at your earliest convenience to ensure security.') }}
  80. </template>
  81. <template v-else>
  82. {{ $gettext('You have not generated recovery codes yet.') }}
  83. </template>
  84. </template>
  85. </AAlert>
  86. <ACard v-if="twoFAStatus?.recovery_codes_generated && codes" class="codes-card mb-4">
  87. <template #title>
  88. <AAlert class="whitespace-normal px-6 py-4 rounded-t-[8px]" type="warning" banner :show-icon="false">
  89. <template #message>
  90. <WarningOutlined class="ant-alert-icon text-lg" />
  91. {{ $gettext('These codes are the last resort for accessing your account in case you lose your password and second factors. If you cannot find these codes, you will lose access to your account.') }}
  92. </template>
  93. </AAlert>
  94. </template>
  95. <ul class="grid grid-cols-2 gap-2 text-lg">
  96. <li v-for="(code, index) in codes" :key="index">
  97. <span :class="{ 'line-through': code.used_time }">
  98. {{ code.code }}
  99. </span>
  100. </li>
  101. </ul>
  102. <div class="mt-4 flex space-x-2">
  103. <UseClipboard v-slot="{ copy, copied }" :source="codeSource">
  104. <AButton @click="copy()">
  105. <template #icon>
  106. <CopyOutlined />
  107. </template>
  108. {{ !copied ? $gettext('Copy Codes') : $gettext('Copied') }}
  109. </AButton>
  110. </UseClipboard>
  111. </div>
  112. </ACard>
  113. <template v-if="twoFAStatus?.enabled">
  114. <AButton
  115. v-if="twoFAStatus?.recovery_codes_generated && !codes"
  116. type="primary"
  117. ghost
  118. @click="clickViewRecoveryCodes"
  119. >
  120. {{ $gettext('View Recovery Codes') }}
  121. </AButton>
  122. <div v-if="twoFAStatus?.recovery_codes_generated" class="mt-4">
  123. <h3>{{ $gettext('Generate New Recovery Codes') }}</h3>
  124. <p>
  125. {{ $gettext('When you generate new recovery codes, you must download or print the new codes.') }}
  126. <b>
  127. {{ $gettext('Your old codes won\'t work anymore.') }}
  128. </b>
  129. </p>
  130. </div>
  131. <APopconfirm
  132. :open="popOpen"
  133. @open-change="handlePopOpenChange"
  134. @confirm="popConfirm"
  135. @cancel="() => popOpen = false"
  136. >
  137. <template #title>
  138. {{ $gettext('Are you sure to generate new recovery codes?') }}<br>
  139. <b>{{ $gettext('Your old codes won\'t work anymore.') }}</b>
  140. </template>
  141. <AButton
  142. type="primary"
  143. ghost
  144. >
  145. {{ twoFAStatus?.recovery_codes_generated ? $gettext('Generate New Recovery Codes') : $gettext('Generate Recovery Codes') }}
  146. </AButton>
  147. </APopconfirm>
  148. </template>
  149. </div>
  150. </template>
  151. <style scoped lang="less">
  152. .codes-card :deep(.ant-card-head) {
  153. padding: 0;
  154. }
  155. </style>