+ Choose how to continue +
++ {{ + pendingAccountEmail + ? `Suggested email: ${pendingAccountEmail}` + : 'Choose whether to bind an existing account or create a new one.' + }} +
+Enter an email address to create your account and continue. @@ -275,7 +312,7 @@ const suggestedAvatarUrl = ref('') const adoptDisplayName = ref(true) const adoptAvatar = ref(true) const needsAdoptionConfirmation = ref(false) -const pendingAccountAction = ref<'none' | 'create_account' | 'bind_login'>('none') +const pendingAccountAction = ref<'none' | 'choose_account_action' | 'create_account' | 'bind_login'>('none') const pendingAccountEmail = ref('') const bindLoginEmail = ref('') const bindLoginPassword = ref('') @@ -290,12 +327,17 @@ const totpError = ref('') const totpUserEmailMasked = ref('') const needsCreateAccount = computed(() => pendingAccountAction.value === 'create_account') +const needsChooser = computed(() => pendingAccountAction.value === 'choose_account_action') const needsBindLogin = computed(() => pendingAccountAction.value === 'bind_login') type LinuxDoPendingActionResponse = PendingOAuthExchangeResponse & { step?: string + intent?: string email?: string resolved_email?: string + pending_email?: string + existing_account_email?: string + suggested_email?: string } function persistPendingAuthSession(redirect?: string) { @@ -392,12 +434,34 @@ function hasSuggestedProfile(completion: { return Boolean(completion.suggested_display_name || completion.suggested_avatar_url) } -function extractPendingAccountEmail(completion: LinuxDoPendingActionResponse): string { - return (completion.email || completion.resolved_email || '').trim() +function normalizedPendingState(value: string | null | undefined): string { + return value?.trim().toLowerCase() || '' } -function resolvePendingAccountAction(completion: LinuxDoPendingActionResponse): 'none' | 'create_account' | 'bind_login' { - const raw = (completion.step || completion.error || '').trim().toLowerCase() +function extractPendingAccountEmail(completion: LinuxDoPendingActionResponse): string { + return ( + completion.pending_email || + completion.existing_account_email || + completion.email || + completion.resolved_email || + completion.suggested_email || + '' + ).trim() +} + +function resolvePendingAccountAction( + completion: LinuxDoPendingActionResponse +): 'none' | 'choose_account_action' | 'create_account' | 'bind_login' { + const raw = normalizedPendingState(completion.step || completion.error || completion.intent) + if ( + raw === 'choice' || + raw === 'choose_account_action_required' || + raw === 'choose_account_action' || + raw === 'choose_account' || + raw === 'choose' + ) { + return 'choose_account_action' + } if (raw === 'email_required' || raw === 'create_account_required' || raw === 'create_account') { return 'create_account' } @@ -418,6 +482,14 @@ function applyPendingAccountAction(completion: LinuxDoPendingActionResponse) { totpUserEmailMasked.value = '' const email = extractPendingAccountEmail(completion) + if (action === 'choose_account_action') { + pendingAccountEmail.value = email + bindLoginEmail.value = email + bindLoginPassword.value = '' + canReturnToCreateAccount.value = false + return + } + if (action === 'create_account') { pendingAccountEmail.value = email canReturnToCreateAccount.value = true @@ -470,28 +542,6 @@ function getRequestErrorMessage(error: unknown, fallback: string): string { return err.response?.data?.detail || err.response?.data?.message || err.message || fallback } -function isCreateAccountRecoveryError(error: unknown): boolean { - const data = (error as { - response?: { - data?: { - reason?: string - error?: string - code?: string - step?: string - intent?: string - } - } - }).response?.data - const states = [data?.reason, data?.error, data?.code, data?.step, data?.intent] - .map(value => value?.trim().toLowerCase()) - .filter((value): value is string => Boolean(value)) - - return states.includes('email_exists') || - states.includes('bind_login_required') || - states.includes('bind_login') || - states.includes('adopt_existing_user_by_email') -} - async function finalizeCompletion(completion: PendingOAuthExchangeResponse, redirect: string) { if (getOAuthCompletionKind(completion) === 'bind') { const bindRedirect = sanitizeRedirectPath(completion.redirect || '/profile') @@ -601,10 +651,6 @@ async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) { }) await finalizePendingAccountResponse(data) } catch (e: unknown) { - if (isCreateAccountRecoveryError(e)) { - switchToBindLoginMode(payload.email) - return - } accountActionError.value = getRequestErrorMessage(e, t('auth.loginFailed')) } finally { isSubmitting.value = false diff --git a/frontend/src/views/auth/OidcCallbackView.vue b/frontend/src/views/auth/OidcCallbackView.vue index e15e752f..9614d077 100644 --- a/frontend/src/views/auth/OidcCallbackView.vue +++ b/frontend/src/views/auth/OidcCallbackView.vue @@ -19,6 +19,7 @@ v-if=" needsInvitation || needsAdoptionConfirmation || + needsChooser || needsCreateAccount || needsBindLogin || needsTotpChallenge @@ -118,6 +119,42 @@
+ ++ Choose how to continue +
++ {{ + pendingAccountEmail + ? `Suggested email: ${pendingAccountEmail}` + : `Choose whether to bind an existing ${providerName} account or create a new one.` + }} +
+Enter an email address to create your account and continue. @@ -284,7 +321,7 @@ const suggestedAvatarUrl = ref('') const adoptDisplayName = ref(true) const adoptAvatar = ref(true) const needsAdoptionConfirmation = ref(false) -const pendingAccountAction = ref<'none' | 'create_account' | 'bind_login'>('none') +const pendingAccountAction = ref<'none' | 'choose_account_action' | 'create_account' | 'bind_login'>('none') const pendingAccountEmail = ref('') const bindLoginEmail = ref('') const bindLoginPassword = ref('') @@ -299,6 +336,7 @@ const totpError = ref('') const totpUserEmailMasked = ref('') const needsCreateAccount = computed(() => pendingAccountAction.value === 'create_account') +const needsChooser = computed(() => pendingAccountAction.value === 'choose_account_action') const needsBindLogin = computed(() => pendingAccountAction.value === 'bind_login') type PendingOidcCompletion = PendingOAuthExchangeResponse & { @@ -307,6 +345,7 @@ type PendingOidcCompletion = PendingOAuthExchangeResponse & { resolved_email?: string existing_account_email?: string email?: string + suggested_email?: string provider_fallback?: string intent?: string requires_2fa?: boolean @@ -430,12 +469,24 @@ function extractPendingAccountEmail(completion: PendingOidcCompletion): string { completion.existing_account_email || completion.resolved_email || completion.email || + completion.suggested_email || '' ).trim() } -function resolvePendingAccountAction(completion: PendingOidcCompletion): 'none' | 'create_account' | 'bind_login' { +function resolvePendingAccountAction( + completion: PendingOidcCompletion +): 'none' | 'choose_account_action' | 'create_account' | 'bind_login' { const raw = normalizedPendingState(completion.step || completion.error || completion.intent) + if ( + raw === 'choice' || + raw === 'choose_account_action_required' || + raw === 'choose_account_action' || + raw === 'choose_account' || + raw === 'choose' + ) { + return 'choose_account_action' + } if (raw === 'email_required' || raw === 'create_account_required' || raw === 'create_account') { return 'create_account' } @@ -462,6 +513,14 @@ function applyPendingAccountAction(completion: PendingOidcCompletion) { totpUserEmailMasked.value = '' const email = extractPendingAccountEmail(completion) + if (action === 'choose_account_action') { + pendingAccountEmail.value = email + bindLoginEmail.value = email + bindLoginPassword.value = '' + canReturnToCreateAccount.value = false + return + } + if (action === 'create_account') { pendingAccountEmail.value = email canReturnToCreateAccount.value = true @@ -514,28 +573,6 @@ function getRequestErrorMessage(error: unknown, fallback: string): string { return err.response?.data?.detail || err.response?.data?.message || err.message || fallback } -function isCreateAccountRecoveryError(error: unknown): boolean { - const data = (error as { - response?: { - data?: { - reason?: string - error?: string - code?: string - step?: string - intent?: string - } - } - }).response?.data - const states = [data?.reason, data?.error, data?.code, data?.step, data?.intent] - .map(value => value?.trim().toLowerCase()) - .filter((value): value is string => Boolean(value)) - - return states.includes('email_exists') || - states.includes('bind_login_required') || - states.includes('bind_login') || - states.includes('adopt_existing_user_by_email') -} - async function finalizeCompletion(completion: PendingOAuthExchangeResponse, redirect: string) { if (getOAuthCompletionKind(completion) === 'bind') { const bindRedirect = sanitizeRedirectPath(completion.redirect || '/profile') @@ -645,10 +682,6 @@ async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) { }) await finalizePendingAccountResponse(data) } catch (e: unknown) { - if (isCreateAccountRecoveryError(e)) { - switchToBindLoginMode(payload.email) - return - } accountActionError.value = getRequestErrorMessage(e, t('auth.loginFailed')) } finally { isSubmitting.value = false diff --git a/frontend/src/views/auth/WechatCallbackView.vue b/frontend/src/views/auth/WechatCallbackView.vue index a3efcf9a..54bf0684 100644 --- a/frontend/src/views/auth/WechatCallbackView.vue +++ b/frontend/src/views/auth/WechatCallbackView.vue @@ -18,6 +18,7 @@
+ Choose how to continue +
++ Pick whether to bind an existing account or create a new one. +
+Review the {{ providerName }} profile details before continuing. @@ -168,13 +206,46 @@ @submit="handleCreateAccount" @switch-to-bind="switchToBindLoginMode" /> +
- Log in to an existing account to bind this {{ providerName }} sign-in. + Bind this {{ providerName }} sign-in to an existing account.
-+ Bind the current account +
++ Bind this WeChat identity to the account currently signed in on this browser. +
+- {{ accountActionError }} -
-+ {{ accountActionError }} +
+