feat: support linuxdo pending bind 2fa callback

This commit is contained in:
IanShaw027
2026-04-20 19:53:22 +08:00
parent fb6204ea8b
commit 7826e9880c
3 changed files with 158 additions and 2 deletions

View File

@@ -12,7 +12,13 @@
<transition name="fade">
<div
v-if="needsInvitation || needsAdoptionConfirmation || needsCreateAccount || needsBindLogin"
v-if="
needsInvitation ||
needsAdoptionConfirmation ||
needsCreateAccount ||
needsBindLogin ||
needsTotpChallenge
"
class="space-y-4"
>
<div
@@ -186,6 +192,40 @@
</p>
</transition>
</template>
<template v-else-if="needsTotpChallenge">
<p class="text-sm text-gray-700 dark:text-gray-300">
Enter the 6-digit verification code for
<span class="font-medium">{{ totpUserEmailMasked || 'your account' }}</span>
to finish binding this LinuxDo sign-in.
</p>
<div class="space-y-3">
<input
v-model="totpCode"
data-testid="linuxdo-bind-login-totp"
type="text"
inputmode="numeric"
maxlength="6"
class="input w-full"
placeholder="123456"
:disabled="isSubmitting"
@keyup.enter="handleSubmitTotpChallenge"
/>
<button
data-testid="linuxdo-bind-login-totp-submit"
class="btn btn-primary w-full"
:disabled="isSubmitting || totpCode.trim().length !== 6"
@click="handleSubmitTotpChallenge"
>
{{ isSubmitting ? t('common.processing') : 'Verify and continue' }}
</button>
</div>
<transition name="fade">
<p v-if="totpError" class="text-sm text-red-600 dark:text-red-400">
{{ totpError }}
</p>
</transition>
</template>
</div>
</transition>
@@ -226,6 +266,7 @@ import {
exchangePendingOAuthCompletion,
getOAuthCompletionKind,
isOAuthLoginCompletion,
login2FA,
persistOAuthTokenContext,
type OAuthAdoptionDecision,
type PendingOAuthExchangeResponse
@@ -260,6 +301,11 @@ const bindLoginPassword = ref('')
const accountActionError = ref('')
const canReturnToCreateAccount = ref(false)
const bindSuccessMessage = t('profile.authBindings.bindSuccess')
const needsTotpChallenge = ref(false)
const totpTempToken = ref('')
const totpCode = ref('')
const totpError = ref('')
const totpUserEmailMasked = ref('')
const needsCreateAccount = computed(() => pendingAccountAction.value === 'create_account')
const needsBindLogin = computed(() => pendingAccountAction.value === 'bind_login')
@@ -346,6 +392,11 @@ function applyPendingAccountAction(completion: LinuxDoPendingActionResponse) {
const action = resolvePendingAccountAction(completion)
pendingAccountAction.value = action
accountActionError.value = ''
needsTotpChallenge.value = false
totpTempToken.value = ''
totpCode.value = ''
totpError.value = ''
totpUserEmailMasked.value = ''
const email = extractPendingAccountEmail(completion)
if (action === 'create_account') {
@@ -364,6 +415,23 @@ function applyPendingAccountAction(completion: LinuxDoPendingActionResponse) {
canReturnToCreateAccount.value = false
}
function applyTotpChallenge(completion: LinuxDoPendingActionResponse): boolean {
if (completion.requires_2fa !== true || !completion.temp_token) {
return false
}
pendingAccountAction.value = 'none'
needsInvitation.value = false
needsAdoptionConfirmation.value = false
needsTotpChallenge.value = true
totpTempToken.value = completion.temp_token
totpCode.value = ''
totpError.value = ''
totpUserEmailMasked.value = completion.user_email_masked || ''
isProcessing.value = false
return true
}
function switchToBindLoginMode() {
pendingAccountAction.value = 'bind_login'
bindLoginEmail.value = bindLoginEmail.value.trim() || pendingAccountEmail.value.trim()
@@ -412,6 +480,10 @@ async function finalizePendingAccountResponse(completion: LinuxDoPendingActionRe
return
}
if (applyTotpChallenge(completion)) {
return
}
applyPendingAccountAction(completion)
if (pendingAccountAction.value !== 'none') {
needsInvitation.value = false
@@ -501,6 +573,27 @@ async function handleBindLogin() {
}
}
async function handleSubmitTotpChallenge() {
totpError.value = ''
const code = totpCode.value.trim()
if (!totpTempToken.value || code.length !== 6) return
isSubmitting.value = true
try {
const completion = await login2FA({
temp_token: totpTempToken.value,
totp_code: code
})
await authStore.setToken(completion.access_token)
appStore.showSuccess(t('auth.loginSuccess'))
await router.replace(redirectTo.value)
} catch (e: unknown) {
totpError.value = getRequestErrorMessage(e, t('auth.loginFailed'))
} finally {
isSubmitting.value = false
}
}
onMounted(async () => {
const params = parseFragmentParams()
const error = params.get('error')
@@ -527,6 +620,10 @@ onMounted(async () => {
return
}
if (applyTotpChallenge(completion as LinuxDoPendingActionResponse)) {
return
}
applyPendingAccountAction(completion as LinuxDoPendingActionResponse)
if (pendingAccountAction.value !== 'none') {
isProcessing.value = false