feat: add oauth callback email binding ui

This commit is contained in:
IanShaw027
2026-04-20 19:30:19 +08:00
parent 6a75bd77e3
commit 6ea3f42e2f
10 changed files with 916 additions and 36 deletions

View File

@@ -97,6 +97,40 @@
: t('auth.oidc.completeRegistration')
}}
</button>
<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-3">
<div class="space-y-1">
<p class="text-sm font-medium text-gray-900 dark:text-white">
{{ t('auth.alreadyHaveAccount') }}
</p>
<p class="text-xs text-gray-500 dark:text-dark-400">
Sign in to an existing account, then bind this WeChat identity to it.
</p>
</div>
<input
v-model="existingAccountEmail"
data-testid="existing-account-email"
type="email"
class="input w-full"
:placeholder="t('auth.emailPlaceholder')"
:disabled="isSubmitting"
/>
<button
data-testid="existing-account-submit"
type="button"
class="btn btn-secondary w-full"
:disabled="isSubmitting"
@click="handleExistingAccountBinding"
>
{{ t('auth.signIn') }}
</button>
</div>
</div>
</template>
<template v-else-if="needsAdoptionConfirmation">
@@ -144,8 +178,10 @@ import { useAuthStore, useAppStore } from '@/stores'
import {
completeWeChatOAuthRegistration,
exchangePendingOAuthCompletion,
getAuthToken,
getOAuthCompletionKind,
isOAuthLoginCompletion,
prepareOAuthBindAccessTokenCookie,
persistOAuthTokenContext,
type OAuthAdoptionDecision,
type PendingOAuthExchangeResponse
@@ -168,6 +204,7 @@ const redirectTo = ref('/dashboard')
const adoptionRequired = ref(false)
const suggestedDisplayName = ref('')
const suggestedAvatarUrl = ref('')
const existingAccountEmail = ref('')
const adoptDisplayName = ref(true)
const adoptAvatar = ref(true)
const needsAdoptionConfirmation = ref(false)
@@ -190,6 +227,50 @@ function sanitizeRedirectPath(path: string | null | undefined): string {
return path
}
function resolveWeChatOAuthMode(): 'open' | 'mp' {
if (typeof navigator === 'undefined') {
return 'open'
}
return /MicroMessenger/i.test(navigator.userAgent) ? 'mp' : 'open'
}
function resolveRedirectTarget(): string {
return sanitizeRedirectPath(
(route.query.redirect as string | undefined) || redirectTo.value || '/dashboard'
)
}
function resolveWeChatStartURL(intent: 'bind_current_user' | 'adopt_existing_user_by_email'): string {
const apiBase = (import.meta.env.VITE_API_BASE_URL as string | undefined) || '/api/v1'
const normalized = apiBase.replace(/\/$/, '')
const params = new URLSearchParams({
mode: resolveWeChatOAuthMode(),
redirect: resolveRedirectTarget(),
intent,
})
const email = existingAccountEmail.value.trim()
if (email) {
params.set('email', email)
}
return `${normalized}/auth/oauth/wechat/start?${params.toString()}`
}
function buildExistingAccountResumePath(): string {
const params = new URLSearchParams({
wechat_bind_existing: '1',
redirect: resolveRedirectTarget(),
})
const email = existingAccountEmail.value.trim()
if (email) {
params.set('email', email)
}
return `/auth/wechat/callback?${params.toString()}`
}
function currentAdoptionDecision(): OAuthAdoptionDecision {
return {
adoptDisplayName: adoptDisplayName.value,
@@ -197,6 +278,23 @@ function currentAdoptionDecision(): OAuthAdoptionDecision {
}
}
async function handleExistingAccountBinding() {
if (getAuthToken()) {
prepareOAuthBindAccessTokenCookie()
window.location.href = resolveWeChatStartURL('bind_current_user')
return
}
const params = new URLSearchParams({
redirect: buildExistingAccountResumePath(),
})
const email = existingAccountEmail.value.trim()
if (email) {
params.set('email', email)
}
await router.replace(`/login?${params.toString()}`)
}
function applyAdoptionSuggestionState(completion: PendingOAuthExchangeResponse) {
adoptionRequired.value = completion.adoption_required === true
suggestedDisplayName.value = completion.suggested_display_name || ''
@@ -275,6 +373,16 @@ async function handleContinueLogin() {
}
onMounted(async () => {
if (typeof route.query.email === 'string') {
existingAccountEmail.value = route.query.email
}
if (route.query.wechat_bind_existing === '1' && getAuthToken()) {
prepareOAuthBindAccessTokenCookie()
window.location.href = resolveWeChatStartURL('bind_current_user')
return
}
const params = parseFragmentParams()
const error = params.get('error')
const errorDesc = params.get('error_description') || params.get('error_message') || ''