fix(auth): require explicit choice for third-party signup

This commit is contained in:
IanShaw027
2026-04-21 20:36:58 +08:00
parent 2cebb0dc60
commit 4c21320d1b
8 changed files with 638 additions and 296 deletions

View File

@@ -15,6 +15,7 @@
v-if="
needsInvitation ||
needsAdoptionConfirmation ||
needsChooser ||
needsCreateAccount ||
needsBindLogin ||
needsTotpChallenge
@@ -109,6 +110,42 @@
</button>
</template>
<template v-else-if="needsChooser">
<div class="rounded-xl border border-gray-200 bg-gray-50 p-4 dark:border-dark-600 dark:bg-dark-800/60">
<div class="space-y-4">
<div class="space-y-1">
<p class="text-sm font-medium text-gray-900 dark:text-white">
Choose how to continue
</p>
<p class="text-xs text-gray-500 dark:text-dark-400">
{{
pendingAccountEmail
? `Suggested email: ${pendingAccountEmail}`
: 'Choose whether to bind an existing account or create a new one.'
}}
</p>
</div>
<div class="grid gap-3 sm:grid-cols-2">
<button
class="btn btn-secondary w-full"
:disabled="isSubmitting"
@click="switchToBindLoginMode()"
>
Bind existing account
</button>
<button
class="btn btn-primary w-full"
:disabled="isSubmitting"
@click="switchToCreateAccountMode"
>
Create new account
</button>
</div>
</div>
</div>
</template>
<template v-else-if="needsCreateAccount">
<p class="text-sm text-gray-700 dark:text-gray-300">
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