main
vue 118 lines 3.07 KB
Raw
1 <template>
2 <div class="flex flex-col">
3 <div class="min-h-17">
4 <div class="mb-2 text-sm">Two-Factor Authentication</div>
5
6 <n-collapse-transition :show="!showBackupInput">
7 <n-input-otp
8 ref="twoFaCodeRef"
9 v-model:value="twoFaCode"
10 block
11 size="large"
12 :input-props="{ autocomplete: 'one-time-code', inputmode: 'numeric' }"
13 @finish="verify2fa()"
14 @keydown.enter="verify2fa()"
15 />
16 </n-collapse-transition>
17 <n-collapse-transition :show="showBackupInput">
18 <n-input
19 v-model:value="backupCode"
20 placeholder="Backup code (e.g. ABCD1234EF)"
21 size="large"
22 @keydown.enter="verify2fa({ useBackupCode: true })"
23 />
24 </n-collapse-transition>
25 </div>
26
27 <div class="h-6 w-full"></div>
28
29 <n-button
30 type="primary"
31 class="w-full!"
32 size="large"
33 :loading="twoFaLoading"
34 :disabled="!isValid"
35 @click="verify2fa({ useBackupCode: showBackupInput })"
36 >
37 {{ showBackupInput ? "Use backup code" : "Verify" }}
38 </n-button>
39
40 <div class="mt-4 flex items-center justify-between gap-2">
41 <n-button text size="small" @click="cancel2fa()">← Back to login</n-button>
42 <n-button text size="small" @click="showBackupInput = !showBackupInput">
43 {{ showBackupInput ? "Use authenticator code" : "Use a backup code instead" }}
44 </n-button>
45 </div>
46 </div>
47 </template>
48
49 <script lang="ts" setup>
50 import type { InputOtpInst } from "naive-ui"
51 import type { TOTPValidateRequest } from "@/api/endpoints/totp"
52 import { NButton, NCollapseTransition, NInput, NInputOtp, useMessage } from "naive-ui"
53 import { computed, onMounted, ref, watch } from "vue"
54 import { useRouter } from "vue-router"
55 import { useAuthStore } from "@/stores/auth"
56
57 const emit = defineEmits<{
58 (e: "cancel"): void
59 }>()
60
61 const twoFaTempToken = defineModel<string>("twoFaTempToken", { default: "" })
62
63 const router = useRouter()
64 const message = useMessage()
65 const twoFaCodeRef = ref<InputOtpInst | null>(null)
66 const authStore = useAuthStore()
67
68 const twoFaCode = ref<string[]>([])
69 const backupCode = ref("")
70 const twoFaLoading = ref(false)
71 const showBackupInput = ref(false)
72
73 watch(showBackupInput, () => {
74 twoFaCode.value = []
75 backupCode.value = ""
76 })
77
78 const isValid = computed(() => {
79 if (showBackupInput.value) {
80 return !!backupCode.value
81 }
82 return twoFaCode.value.join("").length === 6
83 })
84
85 function cancel2fa() {
86 twoFaTempToken.value = ""
87 twoFaCode.value = []
88 backupCode.value = ""
89 showBackupInput.value = false
90 emit("cancel")
91 }
92
93 async function verify2fa(params?: { useBackupCode?: boolean }) {
94 twoFaLoading.value = true
95
96 const payload: TOTPValidateRequest = {
97 temp_token: twoFaTempToken.value,
98 code: params?.useBackupCode ? undefined : twoFaCode.value.join(""),
99 backup_code: params?.useBackupCode ? backupCode.value : undefined
100 }
101
102 authStore
103 .verify2fa(payload)
104 .then(() => {
105 router.push({ path: "/", replace: true })
106 })
107 .catch(err => {
108 message.error(err?.message || err.response?.data?.detail || "An error occurred. Please try again later.")
109 })
110 .finally(() => {
111 twoFaLoading.value = false
112 })
113 }
114
115 onMounted(() => {
116 twoFaCodeRef.value?.focusOnChar(0)
117 })
118 </script>