diff --git a/frontend/src/views/auth/WechatCallbackView.vue b/frontend/src/views/auth/WechatCallbackView.vue index adb714bc..edc72d9d 100644 --- a/frontend/src/views/auth/WechatCallbackView.vue +++ b/frontend/src/views/auth/WechatCallbackView.vue @@ -388,6 +388,15 @@ function resolveWeChatOAuthMode(): 'open' | 'mp' { return /MicroMessenger/i.test(navigator.userAgent) ? 'mp' : 'open' } +function normalizeWeChatOAuthMode(value: unknown): 'open' | 'mp' | null { + return value === 'open' || value === 'mp' ? value : null +} + +function resolveRequestedWeChatOAuthMode(): 'open' | 'mp' { + const queryMode = normalizeWeChatOAuthMode(route.query.mode) + return queryMode || resolveWeChatOAuthMode() +} + function resolveRedirectTarget(): string { return sanitizeRedirectPath( (route.query.redirect as string | undefined) || redirectTo.value || '/dashboard' @@ -398,7 +407,7 @@ function resolveWeChatStartURL(intent: 'bind_current_user' | 'adopt_existing_use const apiBase = (import.meta.env.VITE_API_BASE_URL as string | undefined) || '/api/v1' const normalized = apiBase.replace(/\/$/, '') const params = new URLSearchParams({ - mode: resolveWeChatOAuthMode(), + mode: resolveRequestedWeChatOAuthMode(), redirect: resolveRedirectTarget(), intent, }) @@ -415,6 +424,7 @@ function buildExistingAccountResumePath(): string { const params = new URLSearchParams({ wechat_bind_existing: '1', redirect: resolveRedirectTarget(), + mode: resolveRequestedWeChatOAuthMode(), }) const email = existingAccountEmail.value.trim() @@ -727,9 +737,21 @@ onMounted(async () => { existingAccountEmail.value = route.query.email } - if (route.query.wechat_bind_existing === '1' && getAuthToken()) { - prepareOAuthBindAccessTokenCookie() - window.location.href = resolveWeChatStartURL('bind_current_user') + if (route.query.wechat_bind_existing === '1') { + 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()}`) return } diff --git a/frontend/src/views/auth/__tests__/WechatCallbackView.spec.ts b/frontend/src/views/auth/__tests__/WechatCallbackView.spec.ts index 91aabf51..80d8b23b 100644 --- a/frontend/src/views/auth/__tests__/WechatCallbackView.spec.ts +++ b/frontend/src/views/auth/__tests__/WechatCallbackView.spec.ts @@ -342,6 +342,7 @@ describe('WechatCallbackView', () => { expect(replaceMock.mock.calls[0]?.[0]).toContain('/login?') expect(replaceMock.mock.calls[0]?.[0]).toContain('wechat_bind_existing%3D1') expect(replaceMock.mock.calls[0]?.[0]).toContain('email=user%40example.com') + expect(replaceMock.mock.calls[0]?.[0]).toContain('mode%3Dopen') }) it('collects email for pending oauth account creation and submits adoption decisions', async () => { @@ -592,7 +593,8 @@ describe('WechatCallbackView', () => { it('restarts the current-user bind flow after returning from login', async () => { routeState.query = { wechat_bind_existing: '1', - redirect: '/profile' + redirect: '/profile', + mode: 'mp', } getAuthTokenMock.mockReturnValue('existing-auth-token') @@ -612,7 +614,38 @@ describe('WechatCallbackView', () => { expect(exchangePendingOAuthCompletionMock).not.toHaveBeenCalled() expect(prepareOAuthBindAccessTokenCookieMock).toHaveBeenCalledTimes(1) expect(locationState.current.href).toContain('/api/v1/auth/oauth/wechat/start?') + expect(locationState.current.href).toContain('mode=mp') expect(locationState.current.href).toContain('intent=bind_current_user') expect(locationState.current.href).toContain('redirect=%2Fprofile') }) + + it('redirects back to login instead of falling through when bind-existing resume has no auth token', async () => { + routeState.query = { + wechat_bind_existing: '1', + redirect: '/profile', + mode: 'mp', + email: 'resume@example.com', + } + getAuthTokenMock.mockReturnValue(null) + + mount(WechatCallbackView, { + global: { + stubs: { + AuthLayout: { template: '
' }, + Icon: true, + RouterLink: { template: '' }, + transition: false, + }, + }, + }) + + await flushPromises() + + expect(exchangePendingOAuthCompletionMock).not.toHaveBeenCalled() + expect(replaceMock).toHaveBeenCalledTimes(1) + expect(replaceMock.mock.calls[0]?.[0]).toContain('/login?') + expect(replaceMock.mock.calls[0]?.[0]).toContain('wechat_bind_existing%3D1') + expect(replaceMock.mock.calls[0]?.[0]).toContain('mode%3Dmp') + expect(replaceMock.mock.calls[0]?.[0]).toContain('email=resume%40example.com') + }) })