mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-04 23:18:44 +08:00
fix: render OTP QR code reliably
This commit is contained in:
@@ -41,42 +41,69 @@ const otpPassword = ref('')
|
|||||||
|
|
||||||
const allowPasskeyWithoutOtp = computed(() => globalSettingsStore.get('PASSKEY_ALLOW_REGISTER_WITHOUT_OTP') === true)
|
const allowPasskeyWithoutOtp = computed(() => globalSettingsStore.get('PASSKEY_ALLOW_REGISTER_WITHOUT_OTP') === true)
|
||||||
|
|
||||||
|
// OTP 初始化加载状态
|
||||||
|
const otpLoading = ref(false)
|
||||||
|
|
||||||
|
// OTP 初始化失败信息
|
||||||
|
const otpGenerateError = ref('')
|
||||||
|
|
||||||
// 二维码图片 base64
|
// 二维码图片 base64
|
||||||
const qrCodeImage = ref('')
|
const qrCodeImage = ref('')
|
||||||
|
|
||||||
// 二维码信息
|
// 二维码信息
|
||||||
const qrCode = ref('')
|
const qrCode = ref('')
|
||||||
|
|
||||||
// 为当前用户获取Otp Uri
|
// 清空当前 OTP 设置流程的临时数据。
|
||||||
|
function resetOtpSetupState() {
|
||||||
|
qrCodeImage.value = ''
|
||||||
|
qrCode.value = ''
|
||||||
|
otpUri.value = ''
|
||||||
|
secret.value = ''
|
||||||
|
otpGenerateError.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记 OTP 初始化失败,并向用户显示明确错误。
|
||||||
|
function setOtpGenerateError(message?: string) {
|
||||||
|
const errorMessage = message || t('common.error')
|
||||||
|
otpGenerateError.value = t('profile.otpGenerateFailed', { message: errorMessage })
|
||||||
|
$toast.error(otpGenerateError.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为当前用户获取 OTP URI 并生成二维码图片。
|
||||||
async function getOtpUri() {
|
async function getOtpUri() {
|
||||||
|
resetOtpSetupState()
|
||||||
// 如果已经启用OTP,只打开对话框,不生成新的二维码
|
// 如果已经启用OTP,只打开对话框,不生成新的二维码
|
||||||
if (props.isOtp) {
|
if (props.isOtp) {
|
||||||
qrCode.value = '' // 清空二维码,这样对话框会显示清除界面
|
|
||||||
qrCodeImage.value = ''
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 未启用OTP,生成新的二维码
|
// 未启用OTP,生成新的二维码
|
||||||
|
otpLoading.value = true
|
||||||
try {
|
try {
|
||||||
const result = (await api.post('mfa/otp/generate')) as ApiResponse<{
|
const result = (await api.post('mfa/otp/generate')) as ApiResponse<{
|
||||||
uri: string
|
uri: string
|
||||||
secret: string
|
secret: string
|
||||||
}>
|
}>
|
||||||
if (result.success) {
|
const uri = result.data?.uri?.trim()
|
||||||
otpUri.value = result.data.uri
|
const otpSecret = result.data?.secret?.trim()
|
||||||
secret.value = result.data.secret
|
|
||||||
qrCode.value = result.data.uri
|
if (result.success && uri) {
|
||||||
|
otpUri.value = uri
|
||||||
|
secret.value = otpSecret || ''
|
||||||
|
qrCode.value = uri
|
||||||
// 生成二维码图片
|
// 生成二维码图片
|
||||||
qrCodeImage.value = await QRCode.toDataURL(result.data.uri, {
|
qrCodeImage.value = await QRCode.toDataURL(uri, {
|
||||||
width: 200,
|
width: 200,
|
||||||
margin: 1,
|
margin: 1,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
$toast.error(t('profile.otpGenerateFailed', { message: result.message }))
|
setOtpGenerateError(result.message || 'empty otp uri')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
$toast.error(t('profile.otpGenerateFailed', { message: error instanceof Error ? error.message : String(error) }))
|
setOtpGenerateError(error instanceof Error ? error.message : String(error))
|
||||||
|
} finally {
|
||||||
|
otpLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,10 +172,8 @@ watch(
|
|||||||
otpPassword.value = ''
|
otpPassword.value = ''
|
||||||
} else {
|
} else {
|
||||||
// 弹窗关闭时,清空数据
|
// 弹窗关闭时,清空数据
|
||||||
qrCodeImage.value = ''
|
resetOtpSetupState()
|
||||||
qrCode.value = ''
|
otpLoading.value = false
|
||||||
otpUri.value = ''
|
|
||||||
secret.value = ''
|
|
||||||
otpPassword.value = ''
|
otpPassword.value = ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -194,16 +219,29 @@ watch(
|
|||||||
|
|
||||||
<!-- 设置新的OTP -->
|
<!-- 设置新的OTP -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="my-6 rounded text-center p-3 border" style="width: fit-content; margin: 0 auto">
|
<div
|
||||||
<VImg class="mx-auto" :src="qrCodeImage" width="200" height="200">
|
class="my-6 rounded text-center p-3 border d-flex align-center justify-center"
|
||||||
<template #placeholder>
|
style="width: 226px; height: 226px; margin: 0 auto"
|
||||||
<div class="w-full h-full">
|
>
|
||||||
<VSkeletonLoader class="object-cover aspect-w-1 aspect-h-1" />
|
<img
|
||||||
</div>
|
v-if="qrCodeImage"
|
||||||
</template>
|
class="mx-auto d-block otp-qrcode-image"
|
||||||
</VImg>
|
:src="qrCodeImage"
|
||||||
|
:alt="t('profile.setupAuthenticator')"
|
||||||
|
width="200"
|
||||||
|
height="200"
|
||||||
|
/>
|
||||||
|
<VProgressCircular v-else-if="otpLoading" indeterminate color="primary" />
|
||||||
|
<div v-else class="w-100">
|
||||||
|
<VAlert type="error" variant="tonal" density="compact" class="mb-3">
|
||||||
|
{{ otpGenerateError || t('profile.otpGenerateFailed', { message: t('common.error') }) }}
|
||||||
|
</VAlert>
|
||||||
|
<VBtn size="small" variant="tonal" prepend-icon="mdi-refresh" @click="getOtpUri">
|
||||||
|
{{ t('common.retry') }}
|
||||||
|
</VBtn>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<VAlert :title="secret" variant="tonal" type="warning" class="my-4" :text="t('profile.secretKeyTip')">
|
<VAlert v-if="secret" :title="secret" variant="tonal" type="warning" class="my-4" :text="t('profile.secretKeyTip')">
|
||||||
<template #prepend />
|
<template #prepend />
|
||||||
</VAlert>
|
</VAlert>
|
||||||
<VForm @submit.prevent="judgeOtpPassword">
|
<VForm @submit.prevent="judgeOtpPassword">
|
||||||
@@ -221,7 +259,7 @@ watch(
|
|||||||
<VBtn variant="outlined" color="secondary" @click="show = false">
|
<VBtn variant="outlined" color="secondary" @click="show = false">
|
||||||
{{ t('common.cancel') }}
|
{{ t('common.cancel') }}
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<VBtn type="submit">
|
<VBtn type="submit" :disabled="!otpUri || otpLoading">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<VIcon icon="mdi-check" />
|
<VIcon icon="mdi-check" />
|
||||||
</template>
|
</template>
|
||||||
@@ -234,3 +272,10 @@ watch(
|
|||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
</VDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.otp-qrcode-image {
|
||||||
|
inline-size: 200px;
|
||||||
|
block-size: 200px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user