fix: 修复邮箱快捷登录前端回调兜底

This commit is contained in:
lyen1688 2026-05-06 19:44:45 +08:00 committed by lyen1688
parent 480fe27b31
commit 7f185422a5
4 changed files with 69 additions and 1 deletions

View File

@ -34,6 +34,7 @@ import GoogleMark from './GoogleMark.vue'
import { resolveAffiliateReferralCode, storeOAuthAffiliateCode } from '@/utils/oauthAffiliate'
type EmailOAuthProvider = 'github' | 'google'
const EMAIL_OAUTH_PENDING_PROVIDER_KEY = 'email_oauth_pending_provider'
const props = withDefaults(defineProps<{
disabled?: boolean
@ -73,6 +74,7 @@ function startLogin(provider: EmailOAuthProvider): void {
const redirectTo = (route.query.redirect as string) || '/dashboard'
const affiliateCode = resolveAffiliateReferralCode(props.affCode, route.query.aff, route.query.aff_code)
storeOAuthAffiliateCode(affiliateCode)
window.sessionStorage.setItem(EMAIL_OAUTH_PENDING_PROVIDER_KEY, provider)
const apiBase = (import.meta.env.VITE_API_BASE_URL as string | undefined) || '/api/v1'
const normalized = apiBase.replace(/\/$/, '')
const params = new URLSearchParams({ redirect: redirectTo })

View File

@ -57,6 +57,7 @@ describe('EmailOAuthButtons', () => {
'/api/v1/auth/oauth/github/start?redirect=%2Fbilling%3Fplan%3Dpro&aff_code=AFF123'
)
expect(window.sessionStorage.getItem('oauth_aff_code')).toBe('AFF123')
expect(window.sessionStorage.getItem('email_oauth_pending_provider')).toBe('github')
})
it('uses a full-width descriptive button when only GitHub is enabled', () => {

View File

@ -140,6 +140,7 @@ const invitationError = ref('')
const pendingProvider = ref<'github' | 'google'>('github')
const redirectTo = ref('/dashboard')
const invalidCallback = ref(false)
const EMAIL_OAUTH_PENDING_PROVIDER_KEY = 'email_oauth_pending_provider'
type EmailOAuthPendingCompletion = Partial<OAuthTokenResponse> & {
error?: string
@ -190,9 +191,37 @@ function sanitizeRedirectPath(path: string | null | undefined): string {
return path
}
function readPendingEmailOAuthProvider(): 'github' | 'google' | null {
if (typeof window === 'undefined') return null
const provider = window.sessionStorage.getItem(EMAIL_OAUTH_PENDING_PROVIDER_KEY)
if (provider === 'github' || provider === 'google') return provider
return null
}
function redirectProviderCallbackToBackend(provider: 'github' | 'google'): void {
if (typeof window === 'undefined') return
const apiBase = (import.meta.env.VITE_API_BASE_URL as string | undefined) || '/api/v1'
const normalized = apiBase.replace(/\/$/, '')
const params = new URLSearchParams()
for (const [key, value] of Object.entries(route.query)) {
if (Array.isArray(value)) {
value.forEach((item) => {
if (item != null) params.append(key, String(item))
})
} else if (value != null) {
params.set(key, String(value))
}
}
const suffix = params.toString() ? `?${params.toString()}` : ''
window.location.href = `${normalized}/auth/oauth/${provider}/callback${suffix}`
}
async function finalizeTokenResponse(tokenResponse: OAuthTokenResponse, redirect: string) {
persistOAuthTokenContext(tokenResponse)
await authStore.setToken(tokenResponse.access_token)
if (typeof window !== 'undefined') {
window.sessionStorage.removeItem(EMAIL_OAUTH_PENDING_PROVIDER_KEY)
}
clearAllAffiliateReferralCodes()
appStore.showSuccess(t('auth.loginSuccess'))
await router.replace(sanitizeRedirectPath(redirect))
@ -274,6 +303,11 @@ onMounted(async () => {
}
if (!tokenResponse) {
if (route.path === '/auth/oauth/callback') {
const pendingEmailOAuthProvider = readPendingEmailOAuthProvider()
if (pendingEmailOAuthProvider && code.value && state.value) {
redirectProviderCallbackToBackend(pendingEmailOAuthProvider)
return
}
await resumePendingEmailOAuth()
}
return

View File

@ -4,6 +4,7 @@ import OAuthCallbackView from '@/views/auth/OAuthCallbackView.vue'
const {
routeState,
locationState,
routerReplaceMock,
showErrorMock,
showSuccessMock,
@ -16,6 +17,12 @@ const {
path: '/auth/callback',
query: {} as Record<string, unknown>,
},
locationState: {
current: {
href: 'http://localhost/auth/callback',
hash: '',
} as { href: string; hash: string },
},
routerReplaceMock: vi.fn(),
showErrorMock: vi.fn(),
showSuccessMock: vi.fn(),
@ -73,7 +80,14 @@ describe('OAuthCallbackView', () => {
beforeEach(() => {
routeState.path = '/auth/callback'
routeState.query = {}
window.location.hash = ''
locationState.current = {
href: 'http://localhost/auth/callback',
hash: '',
}
Object.defineProperty(window, 'location', {
configurable: true,
value: locationState.current,
})
routerReplaceMock.mockReset()
showErrorMock.mockReset()
showSuccessMock.mockReset()
@ -124,6 +138,23 @@ describe('OAuthCallbackView', () => {
expect(wrapper.find('input[readonly]').exists()).toBe(false)
})
it('forwards frontend email oauth provider callbacks back to the backend callback endpoint', async () => {
routeState.path = '/auth/oauth/callback'
routeState.query = {
code: 'provider-code',
state: 'provider-state',
}
window.sessionStorage.setItem('email_oauth_pending_provider', 'google')
mount(OAuthCallbackView)
await vi.dynamicImportSettled()
expect(locationState.current.href).toBe(
'/api/v1/auth/oauth/google/callback?code=provider-code&state=provider-state'
)
expect(exchangePendingOAuthCompletionMock).not.toHaveBeenCalled()
})
it('submits stored affiliate code when completing invited email oauth registration', async () => {
routeState.path = '/auth/oauth/callback'
exchangePendingOAuthCompletionMock.mockResolvedValue({