fix: 修复邮箱快捷登录前端回调兜底
This commit is contained in:
parent
480fe27b31
commit
7f185422a5
@ -34,6 +34,7 @@ import GoogleMark from './GoogleMark.vue'
|
|||||||
import { resolveAffiliateReferralCode, storeOAuthAffiliateCode } from '@/utils/oauthAffiliate'
|
import { resolveAffiliateReferralCode, storeOAuthAffiliateCode } from '@/utils/oauthAffiliate'
|
||||||
|
|
||||||
type EmailOAuthProvider = 'github' | 'google'
|
type EmailOAuthProvider = 'github' | 'google'
|
||||||
|
const EMAIL_OAUTH_PENDING_PROVIDER_KEY = 'email_oauth_pending_provider'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
@ -73,6 +74,7 @@ function startLogin(provider: EmailOAuthProvider): void {
|
|||||||
const redirectTo = (route.query.redirect as string) || '/dashboard'
|
const redirectTo = (route.query.redirect as string) || '/dashboard'
|
||||||
const affiliateCode = resolveAffiliateReferralCode(props.affCode, route.query.aff, route.query.aff_code)
|
const affiliateCode = resolveAffiliateReferralCode(props.affCode, route.query.aff, route.query.aff_code)
|
||||||
storeOAuthAffiliateCode(affiliateCode)
|
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 apiBase = (import.meta.env.VITE_API_BASE_URL as string | undefined) || '/api/v1'
|
||||||
const normalized = apiBase.replace(/\/$/, '')
|
const normalized = apiBase.replace(/\/$/, '')
|
||||||
const params = new URLSearchParams({ redirect: redirectTo })
|
const params = new URLSearchParams({ redirect: redirectTo })
|
||||||
|
|||||||
@ -57,6 +57,7 @@ describe('EmailOAuthButtons', () => {
|
|||||||
'/api/v1/auth/oauth/github/start?redirect=%2Fbilling%3Fplan%3Dpro&aff_code=AFF123'
|
'/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('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', () => {
|
it('uses a full-width descriptive button when only GitHub is enabled', () => {
|
||||||
|
|||||||
@ -140,6 +140,7 @@ const invitationError = ref('')
|
|||||||
const pendingProvider = ref<'github' | 'google'>('github')
|
const pendingProvider = ref<'github' | 'google'>('github')
|
||||||
const redirectTo = ref('/dashboard')
|
const redirectTo = ref('/dashboard')
|
||||||
const invalidCallback = ref(false)
|
const invalidCallback = ref(false)
|
||||||
|
const EMAIL_OAUTH_PENDING_PROVIDER_KEY = 'email_oauth_pending_provider'
|
||||||
|
|
||||||
type EmailOAuthPendingCompletion = Partial<OAuthTokenResponse> & {
|
type EmailOAuthPendingCompletion = Partial<OAuthTokenResponse> & {
|
||||||
error?: string
|
error?: string
|
||||||
@ -190,9 +191,37 @@ function sanitizeRedirectPath(path: string | null | undefined): string {
|
|||||||
return path
|
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) {
|
async function finalizeTokenResponse(tokenResponse: OAuthTokenResponse, redirect: string) {
|
||||||
persistOAuthTokenContext(tokenResponse)
|
persistOAuthTokenContext(tokenResponse)
|
||||||
await authStore.setToken(tokenResponse.access_token)
|
await authStore.setToken(tokenResponse.access_token)
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.sessionStorage.removeItem(EMAIL_OAUTH_PENDING_PROVIDER_KEY)
|
||||||
|
}
|
||||||
clearAllAffiliateReferralCodes()
|
clearAllAffiliateReferralCodes()
|
||||||
appStore.showSuccess(t('auth.loginSuccess'))
|
appStore.showSuccess(t('auth.loginSuccess'))
|
||||||
await router.replace(sanitizeRedirectPath(redirect))
|
await router.replace(sanitizeRedirectPath(redirect))
|
||||||
@ -274,6 +303,11 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
if (!tokenResponse) {
|
if (!tokenResponse) {
|
||||||
if (route.path === '/auth/oauth/callback') {
|
if (route.path === '/auth/oauth/callback') {
|
||||||
|
const pendingEmailOAuthProvider = readPendingEmailOAuthProvider()
|
||||||
|
if (pendingEmailOAuthProvider && code.value && state.value) {
|
||||||
|
redirectProviderCallbackToBackend(pendingEmailOAuthProvider)
|
||||||
|
return
|
||||||
|
}
|
||||||
await resumePendingEmailOAuth()
|
await resumePendingEmailOAuth()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import OAuthCallbackView from '@/views/auth/OAuthCallbackView.vue'
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
routeState,
|
routeState,
|
||||||
|
locationState,
|
||||||
routerReplaceMock,
|
routerReplaceMock,
|
||||||
showErrorMock,
|
showErrorMock,
|
||||||
showSuccessMock,
|
showSuccessMock,
|
||||||
@ -16,6 +17,12 @@ const {
|
|||||||
path: '/auth/callback',
|
path: '/auth/callback',
|
||||||
query: {} as Record<string, unknown>,
|
query: {} as Record<string, unknown>,
|
||||||
},
|
},
|
||||||
|
locationState: {
|
||||||
|
current: {
|
||||||
|
href: 'http://localhost/auth/callback',
|
||||||
|
hash: '',
|
||||||
|
} as { href: string; hash: string },
|
||||||
|
},
|
||||||
routerReplaceMock: vi.fn(),
|
routerReplaceMock: vi.fn(),
|
||||||
showErrorMock: vi.fn(),
|
showErrorMock: vi.fn(),
|
||||||
showSuccessMock: vi.fn(),
|
showSuccessMock: vi.fn(),
|
||||||
@ -73,7 +80,14 @@ describe('OAuthCallbackView', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
routeState.path = '/auth/callback'
|
routeState.path = '/auth/callback'
|
||||||
routeState.query = {}
|
routeState.query = {}
|
||||||
window.location.hash = ''
|
locationState.current = {
|
||||||
|
href: 'http://localhost/auth/callback',
|
||||||
|
hash: '',
|
||||||
|
}
|
||||||
|
Object.defineProperty(window, 'location', {
|
||||||
|
configurable: true,
|
||||||
|
value: locationState.current,
|
||||||
|
})
|
||||||
routerReplaceMock.mockReset()
|
routerReplaceMock.mockReset()
|
||||||
showErrorMock.mockReset()
|
showErrorMock.mockReset()
|
||||||
showSuccessMock.mockReset()
|
showSuccessMock.mockReset()
|
||||||
@ -124,6 +138,23 @@ describe('OAuthCallbackView', () => {
|
|||||||
expect(wrapper.find('input[readonly]').exists()).toBe(false)
|
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 () => {
|
it('submits stored affiliate code when completing invited email oauth registration', async () => {
|
||||||
routeState.path = '/auth/oauth/callback'
|
routeState.path = '/auth/oauth/callback'
|
||||||
exchangePendingOAuthCompletionMock.mockResolvedValue({
|
exchangePendingOAuthCompletionMock.mockResolvedValue({
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user