diff --git a/backend/internal/handler/dto/settings.go b/backend/internal/handler/dto/settings.go index cc3f8496..d67b29a0 100644 --- a/backend/internal/handler/dto/settings.go +++ b/backend/internal/handler/dto/settings.go @@ -200,6 +200,8 @@ type PublicSettings struct { CustomEndpoints []CustomEndpoint `json:"custom_endpoints"` LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"` WeChatOAuthEnabled bool `json:"wechat_oauth_enabled"` + WeChatOAuthOpenEnabled bool `json:"wechat_oauth_open_enabled"` + WeChatOAuthMPEnabled bool `json:"wechat_oauth_mp_enabled"` OIDCOAuthEnabled bool `json:"oidc_oauth_enabled"` OIDCOAuthProviderName string `json:"oidc_oauth_provider_name"` SoraClientEnabled bool `json:"sora_client_enabled"` diff --git a/backend/internal/handler/setting_handler.go b/backend/internal/handler/setting_handler.go index 9925f066..522290ec 100644 --- a/backend/internal/handler/setting_handler.go +++ b/backend/internal/handler/setting_handler.go @@ -58,6 +58,8 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) { CustomEndpoints: dto.ParseCustomEndpoints(settings.CustomEndpoints), LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled, WeChatOAuthEnabled: settings.WeChatOAuthEnabled, + WeChatOAuthOpenEnabled: settings.WeChatOAuthOpenEnabled, + WeChatOAuthMPEnabled: settings.WeChatOAuthMPEnabled, OIDCOAuthEnabled: settings.OIDCOAuthEnabled, OIDCOAuthProviderName: settings.OIDCOAuthProviderName, BackendModeEnabled: settings.BackendModeEnabled, diff --git a/backend/internal/handler/setting_handler_public_test.go b/backend/internal/handler/setting_handler_public_test.go index 114c7245..b50c982c 100644 --- a/backend/internal/handler/setting_handler_public_test.go +++ b/backend/internal/handler/setting_handler_public_test.go @@ -81,3 +81,35 @@ func TestSettingHandler_GetPublicSettings_ExposesForceEmailOnThirdPartySignup(t require.Equal(t, 0, resp.Code) require.True(t, resp.Data.ForceEmailOnThirdPartySignup) } + +func TestSettingHandler_GetPublicSettings_ExposesWeChatOAuthModeCapabilities(t *testing.T) { + gin.SetMode(gin.TestMode) + t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app") + t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret") + t.Setenv("WECHAT_OAUTH_MP_APP_ID", "") + t.Setenv("WECHAT_OAUTH_MP_APP_SECRET", "") + + h := NewSettingHandler(service.NewSettingService(&settingHandlerPublicRepoStub{}, &config.Config{}), "test-version") + + recorder := httptest.NewRecorder() + c, _ := gin.CreateTestContext(recorder) + c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/settings/public", nil) + + h.GetPublicSettings(c) + + require.Equal(t, http.StatusOK, recorder.Code) + + var resp struct { + Code int `json:"code"` + Data struct { + WeChatOAuthEnabled bool `json:"wechat_oauth_enabled"` + WeChatOAuthOpenEnabled bool `json:"wechat_oauth_open_enabled"` + WeChatOAuthMPEnabled bool `json:"wechat_oauth_mp_enabled"` + } `json:"data"` + } + require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp)) + require.Equal(t, 0, resp.Code) + require.True(t, resp.Data.WeChatOAuthEnabled) + require.True(t, resp.Data.WeChatOAuthOpenEnabled) + require.False(t, resp.Data.WeChatOAuthMPEnabled) +} diff --git a/backend/internal/service/setting_service.go b/backend/internal/service/setting_service.go index 02a64c1c..58246111 100644 --- a/backend/internal/service/setting_service.go +++ b/backend/internal/service/setting_service.go @@ -274,7 +274,9 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings if oidcProviderName == "" { oidcProviderName = "OIDC" } - weChatEnabled := isWeChatOAuthConfigured() + weChatOpenEnabled := isWeChatOAuthOpenConfigured() + weChatMPEnabled := isWeChatOAuthMPConfigured() + weChatEnabled := weChatOpenEnabled || weChatMPEnabled // Password reset requires email verification to be enabled emailVerifyEnabled := settings[SettingKeyEmailVerifyEnabled] == "true" @@ -319,6 +321,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings CustomEndpoints: settings[SettingKeyCustomEndpoints], LinuxDoOAuthEnabled: linuxDoEnabled, WeChatOAuthEnabled: weChatEnabled, + WeChatOAuthOpenEnabled: weChatOpenEnabled, + WeChatOAuthMPEnabled: weChatMPEnabled, BackendModeEnabled: settings[SettingKeyBackendModeEnabled] == "true", PaymentEnabled: settings[SettingPaymentEnabled] == "true", OIDCOAuthEnabled: oidcEnabled, @@ -376,6 +380,8 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any CustomEndpoints json.RawMessage `json:"custom_endpoints"` LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"` WeChatOAuthEnabled bool `json:"wechat_oauth_enabled"` + WeChatOAuthOpenEnabled bool `json:"wechat_oauth_open_enabled"` + WeChatOAuthMPEnabled bool `json:"wechat_oauth_mp_enabled"` BackendModeEnabled bool `json:"backend_mode_enabled"` PaymentEnabled bool `json:"payment_enabled"` OIDCOAuthEnabled bool `json:"oidc_oauth_enabled"` @@ -411,6 +417,8 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any CustomEndpoints: safeRawJSONArray(settings.CustomEndpoints), LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled, WeChatOAuthEnabled: settings.WeChatOAuthEnabled, + WeChatOAuthOpenEnabled: settings.WeChatOAuthOpenEnabled, + WeChatOAuthMPEnabled: settings.WeChatOAuthMPEnabled, BackendModeEnabled: settings.BackendModeEnabled, PaymentEnabled: settings.PaymentEnabled, OIDCOAuthEnabled: settings.OIDCOAuthEnabled, @@ -460,11 +468,17 @@ func filterUserVisibleMenuItems(raw string) json.RawMessage { } func isWeChatOAuthConfigured() bool { - openConfigured := strings.TrimSpace(os.Getenv("WECHAT_OAUTH_OPEN_APP_ID")) != "" && + return isWeChatOAuthOpenConfigured() || isWeChatOAuthMPConfigured() +} + +func isWeChatOAuthOpenConfigured() bool { + return strings.TrimSpace(os.Getenv("WECHAT_OAUTH_OPEN_APP_ID")) != "" && strings.TrimSpace(os.Getenv("WECHAT_OAUTH_OPEN_APP_SECRET")) != "" - mpConfigured := strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_ID")) != "" && +} + +func isWeChatOAuthMPConfigured() bool { + return strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_ID")) != "" && strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_SECRET")) != "" - return openConfigured || mpConfigured } // safeRawJSONArray returns raw as json.RawMessage if it's valid JSON, otherwise "[]". diff --git a/backend/internal/service/setting_service_public_test.go b/backend/internal/service/setting_service_public_test.go index bb97c2aa..4cfa9f0c 100644 --- a/backend/internal/service/setting_service_public_test.go +++ b/backend/internal/service/setting_service_public_test.go @@ -90,3 +90,18 @@ func TestSettingService_GetPublicSettings_ExposesForceEmailOnThirdPartySignup(t require.NoError(t, err) require.True(t, settings.ForceEmailOnThirdPartySignup) } + +func TestSettingService_GetPublicSettings_ExposesWeChatOAuthModeCapabilities(t *testing.T) { + t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app") + t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret") + t.Setenv("WECHAT_OAUTH_MP_APP_ID", "") + t.Setenv("WECHAT_OAUTH_MP_APP_SECRET", "") + + svc := NewSettingService(&settingPublicRepoStub{}, &config.Config{}) + + settings, err := svc.GetPublicSettings(context.Background()) + require.NoError(t, err) + require.True(t, settings.WeChatOAuthEnabled) + require.True(t, settings.WeChatOAuthOpenEnabled) + require.False(t, settings.WeChatOAuthMPEnabled) +} diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go index 9bd461f9..1b859dbd 100644 --- a/backend/internal/service/settings_view.go +++ b/backend/internal/service/settings_view.go @@ -161,13 +161,15 @@ type PublicSettings struct { CustomMenuItems string // JSON array of custom menu items CustomEndpoints string // JSON array of custom endpoints - LinuxDoOAuthEnabled bool - WeChatOAuthEnabled bool - BackendModeEnabled bool - PaymentEnabled bool - OIDCOAuthEnabled bool - OIDCOAuthProviderName string - Version string + LinuxDoOAuthEnabled bool + WeChatOAuthEnabled bool + WeChatOAuthOpenEnabled bool + WeChatOAuthMPEnabled bool + BackendModeEnabled bool + PaymentEnabled bool + OIDCOAuthEnabled bool + OIDCOAuthProviderName string + Version string BalanceLowNotifyEnabled bool AccountQuotaNotifyEnabled bool diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts index 6c877d76..383bdcce 100644 --- a/frontend/src/api/auth.ts +++ b/frontend/src/api/auth.ts @@ -363,7 +363,7 @@ export interface ResolvedWeChatOAuthStart { unavailableReason: WeChatOAuthUnavailableReason | null } -type WeChatOAuthPublicSettings = { +export type WeChatOAuthPublicSettings = { wechat_oauth_enabled?: boolean wechat_oauth_open_enabled?: boolean wechat_oauth_mp_enabled?: boolean diff --git a/frontend/src/api/user.ts b/frontend/src/api/user.ts index c7b1e503..41ebea05 100644 --- a/frontend/src/api/user.ts +++ b/frontend/src/api/user.ts @@ -4,7 +4,11 @@ */ import { apiClient } from './client' -import { prepareOAuthBindAccessTokenCookie } from './auth' +import { + prepareOAuthBindAccessTokenCookie, + resolveWeChatOAuthStart, + type WeChatOAuthPublicSettings, +} from './auth' import type { User, ChangePasswordRequest, NotifyEmailEntry, UserAuthProvider } from '@/types' /** @@ -89,6 +93,7 @@ export type BindableOAuthProvider = Exclude interface BuildOAuthBindingStartURLOptions { redirectTo?: string + wechatOAuthSettings?: WeChatOAuthPublicSettings | null } export function resolveWeChatOAuthMode(): 'open' | 'mp' { @@ -98,10 +103,19 @@ export function resolveWeChatOAuthMode(): 'open' | 'mp' { return /MicroMessenger/i.test(navigator.userAgent) ? 'mp' : 'open' } +function resolveWeChatOAuthBindingMode( + settings?: WeChatOAuthPublicSettings | null +): 'open' | 'mp' | null { + if (settings) { + return resolveWeChatOAuthStart(settings).mode + } + return resolveWeChatOAuthMode() +} + export function buildOAuthBindingStartURL( provider: BindableOAuthProvider, options: BuildOAuthBindingStartURLOptions = {} -): string { +): string | null { const redirectTo = options.redirectTo?.trim() || '/profile' const apiBase = (import.meta.env.VITE_API_BASE_URL as string | undefined) || '/api/v1' const normalized = apiBase.replace(/\/$/, '') @@ -111,7 +125,11 @@ export function buildOAuthBindingStartURL( }) if (provider === 'wechat') { - params.set('mode', resolveWeChatOAuthMode()) + const mode = resolveWeChatOAuthBindingMode(options.wechatOAuthSettings) + if (!mode) { + return null + } + params.set('mode', mode) } return `${normalized}/auth/oauth/${provider}/start?${params.toString()}` @@ -124,8 +142,12 @@ export function startOAuthBinding( if (typeof window === 'undefined') { return } + const startURL = buildOAuthBindingStartURL(provider, options) + if (!startURL) { + return + } prepareOAuthBindAccessTokenCookie() - window.location.href = buildOAuthBindingStartURL(provider, options) + window.location.href = startURL } export const userAPI = { diff --git a/frontend/src/components/user/profile/ProfileIdentityBindingsSection.vue b/frontend/src/components/user/profile/ProfileIdentityBindingsSection.vue index b767b2f3..d3d9814d 100644 --- a/frontend/src/components/user/profile/ProfileIdentityBindingsSection.vue +++ b/frontend/src/components/user/profile/ProfileIdentityBindingsSection.vue @@ -52,7 +52,9 @@ import { computed } from 'vue' import { useI18n } from 'vue-i18n' import { useRoute } from 'vue-router' +import { resolveWeChatOAuthStart, type WeChatOAuthPublicSettings } from '@/api/auth' import { startOAuthBinding } from '@/api/user' +import { useAppStore } from '@/stores' import type { User, UserAuthBindingStatus, UserAuthProvider } from '@/types' const props = withDefaults( @@ -62,17 +64,44 @@ const props = withDefaults( oidcEnabled?: boolean oidcProviderName?: string wechatEnabled?: boolean + wechatOpenEnabled?: boolean + wechatMpEnabled?: boolean }>(), { linuxdoEnabled: false, oidcEnabled: false, oidcProviderName: 'OIDC', wechatEnabled: false, + wechatOpenEnabled: undefined, + wechatMpEnabled: undefined, } ) const { t } = useI18n() const route = useRoute() +const appStore = useAppStore() + +const wechatOAuthSettings = computed(() => { + if (appStore.cachedPublicSettings) { + return appStore.cachedPublicSettings + } + + if ( + typeof props.wechatEnabled === 'boolean' || + typeof props.wechatOpenEnabled === 'boolean' || + typeof props.wechatMpEnabled === 'boolean' + ) { + return { + wechat_oauth_enabled: props.wechatEnabled, + wechat_oauth_open_enabled: props.wechatOpenEnabled, + wechat_oauth_mp_enabled: props.wechatMpEnabled, + } + } + + return null +}) + +const resolvedWeChatBinding = computed(() => resolveWeChatOAuthStart(wechatOAuthSettings.value)) function normalizeBindingStatus(binding: boolean | UserAuthBindingStatus | undefined): boolean | null { if (typeof binding === 'boolean') { @@ -129,7 +158,7 @@ const providerItems = computed(() => [ provider: 'wechat' as const, label: t('profile.authBindings.providers.wechat'), bound: getBindingStatus('wechat'), - canBind: props.wechatEnabled && !getBindingStatus('wechat'), + canBind: resolvedWeChatBinding.value.mode !== null && !getBindingStatus('wechat'), }, ]) @@ -139,6 +168,7 @@ function startBinding(provider: UserAuthProvider): void { } startOAuthBinding(provider, { redirectTo: route.fullPath || '/profile', + wechatOAuthSettings: provider === 'wechat' ? wechatOAuthSettings.value : null, }) } diff --git a/frontend/src/components/user/profile/__tests__/ProfileIdentityBindingsSection.spec.ts b/frontend/src/components/user/profile/__tests__/ProfileIdentityBindingsSection.spec.ts index 1c9531e3..7db2ecd7 100644 --- a/frontend/src/components/user/profile/__tests__/ProfileIdentityBindingsSection.spec.ts +++ b/frontend/src/components/user/profile/__tests__/ProfileIdentityBindingsSection.spec.ts @@ -1,6 +1,8 @@ import { mount } from '@vue/test-utils' +import { createPinia, setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import ProfileIdentityBindingsSection from '@/components/user/profile/ProfileIdentityBindingsSection.vue' +import { useAppStore } from '@/stores' import type { User } from '@/types' const routeState = vi.hoisted(() => ({ @@ -11,6 +13,8 @@ const locationState = vi.hoisted(() => ({ current: { href: 'http://localhost/profile' } as { href: string }, })) +let pinia: ReturnType + vi.mock('vue-router', () => ({ useRoute: () => routeState, })) @@ -57,6 +61,8 @@ function createUser(overrides: Partial = {}): User { describe('ProfileIdentityBindingsSection', () => { beforeEach(() => { + pinia = createPinia() + setActivePinia(pinia) routeState.fullPath = '/profile' locationState.current = { href: 'http://localhost/profile' } Object.defineProperty(window, 'location', { @@ -67,6 +73,9 @@ describe('ProfileIdentityBindingsSection', () => { configurable: true, value: 'Mozilla/5.0', }) + const appStore = useAppStore() + appStore.cachedPublicSettings = null + appStore.publicSettingsLoaded = false }) afterEach(() => { @@ -75,6 +84,9 @@ describe('ProfileIdentityBindingsSection', () => { it('renders provider binding states and provider-specific bind actions', () => { const wrapper = mount(ProfileIdentityBindingsSection, { + global: { + plugins: [pinia], + }, props: { user: createUser({ auth_bindings: { @@ -102,11 +114,16 @@ describe('ProfileIdentityBindingsSection', () => { it('starts the WeChat bind flow for the current profile page', async () => { const wrapper = mount(ProfileIdentityBindingsSection, { + global: { + plugins: [pinia], + }, props: { user: createUser(), linuxdoEnabled: false, oidcEnabled: false, wechatEnabled: true, + wechatOpenEnabled: true, + wechatMpEnabled: false, }, }) @@ -117,4 +134,22 @@ describe('ProfileIdentityBindingsSection', () => { expect(locationState.current.href).toContain('intent=bind_current_user') expect(locationState.current.href).toContain('redirect=%2Fprofile') }) + + it('hides the WeChat bind action outside the WeChat browser when only mp mode is configured', () => { + const wrapper = mount(ProfileIdentityBindingsSection, { + global: { + plugins: [pinia], + }, + props: { + user: createUser(), + linuxdoEnabled: false, + oidcEnabled: false, + wechatEnabled: true, + wechatOpenEnabled: false, + wechatMpEnabled: true, + }, + }) + + expect(wrapper.find('[data-testid="profile-binding-wechat-action"]').exists()).toBe(false) + }) }) diff --git a/frontend/src/stores/app.ts b/frontend/src/stores/app.ts index a8e03a51..0ed3f37f 100644 --- a/frontend/src/stores/app.ts +++ b/frontend/src/stores/app.ts @@ -338,6 +338,8 @@ export const useAppStore = defineStore('app', () => { custom_endpoints: [], linuxdo_oauth_enabled: false, wechat_oauth_enabled: false, + wechat_oauth_open_enabled: false, + wechat_oauth_mp_enabled: false, oidc_oauth_enabled: false, oidc_oauth_provider_name: 'OIDC', backend_mode_enabled: false, diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 5a2e3184..07341919 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -165,6 +165,8 @@ export interface PublicSettings { custom_endpoints: CustomEndpoint[] linuxdo_oauth_enabled: boolean wechat_oauth_enabled: boolean + wechat_oauth_open_enabled?: boolean + wechat_oauth_mp_enabled?: boolean oidc_oauth_enabled: boolean oidc_oauth_provider_name: string backend_mode_enabled: boolean diff --git a/frontend/src/views/auth/WechatCallbackView.vue b/frontend/src/views/auth/WechatCallbackView.vue index 10b83b1c..a5da35e5 100644 --- a/frontend/src/views/auth/WechatCallbackView.vue +++ b/frontend/src/views/auth/WechatCallbackView.vue @@ -297,6 +297,7 @@ import { login2FA, prepareOAuthBindAccessTokenCookie, persistOAuthTokenContext, + resolveWeChatOAuthStart, type OAuthAdoptionDecision, type PendingOAuthExchangeResponse } from '@/api/auth' @@ -378,7 +379,47 @@ function normalizeWeChatOAuthMode(value: unknown): 'open' | 'mp' | null { return value === 'open' || value === 'mp' ? value : null } -function resolveRequestedWeChatOAuthMode(): 'open' | 'mp' { +async function ensurePublicSettingsLoaded(): Promise { + if (appStore.cachedPublicSettings || appStore.publicSettingsLoaded) { + return + } + + try { + await appStore.fetchPublicSettings() + } catch { + // Fall back to legacy mode selection when public settings are unavailable. + } +} + +function resolveConfiguredWeChatOAuthMode(): 'open' | 'mp' | null { + if (!appStore.cachedPublicSettings && !appStore.publicSettingsLoaded) { + return null + } + + return resolveWeChatOAuthStart(appStore.cachedPublicSettings).mode +} + +function resolveWeChatOAuthUnavailableMessage(): string { + const resolved = resolveWeChatOAuthStart(appStore.cachedPublicSettings) + + switch (resolved.unavailableReason) { + case 'external_browser_required': + return 'This WeChat sign-in flow is only available in your system browser.' + case 'wechat_browser_required': + return 'This WeChat sign-in flow is only available inside the WeChat browser.' + case 'not_configured': + return 'WeChat sign-in is not configured yet.' + default: + return t('auth.loginFailed') + } +} + +function resolveRequestedWeChatOAuthMode(): 'open' | 'mp' | null { + const configuredMode = resolveConfiguredWeChatOAuthMode() + if (configuredMode) { + return configuredMode + } + const queryMode = normalizeWeChatOAuthMode(route.query.mode) return queryMode || resolveWeChatOAuthMode() } @@ -389,11 +430,15 @@ function resolveRedirectTarget(): string { ) } -function resolveWeChatStartURL(intent: 'bind_current_user' | 'adopt_existing_user_by_email'): string { +function resolveWeChatStartURL(intent: 'bind_current_user' | 'adopt_existing_user_by_email'): string | null { const apiBase = (import.meta.env.VITE_API_BASE_URL as string | undefined) || '/api/v1' const normalized = apiBase.replace(/\/$/, '') + const mode = resolveRequestedWeChatOAuthMode() + if (!mode) { + return null + } const params = new URLSearchParams({ - mode: resolveRequestedWeChatOAuthMode(), + mode, redirect: resolveRedirectTarget(), intent, }) @@ -406,11 +451,15 @@ function resolveWeChatStartURL(intent: 'bind_current_user' | 'adopt_existing_use return `${normalized}/auth/oauth/wechat/start?${params.toString()}` } -function buildExistingAccountResumePath(): string { +function buildExistingAccountResumePath(): string | null { + const mode = resolveRequestedWeChatOAuthMode() + if (!mode) { + return null + } const params = new URLSearchParams({ wechat_bind_existing: '1', redirect: resolveRedirectTarget(), - mode: resolveRequestedWeChatOAuthMode(), + mode, }) const email = existingAccountEmail.value.trim() @@ -444,14 +493,31 @@ function serializeAdoptionDecision(decision: OAuthAdoptionDecision): Record { + await ensurePublicSettingsLoaded() + if (typeof route.query.email === 'string') { existingAccountEmail.value = route.query.email } if (route.query.wechat_bind_existing === '1') { if (getAuthToken()) { + const startURL = resolveWeChatStartURL('bind_current_user') + if (!startURL) { + errorMessage.value = resolveWeChatOAuthUnavailableMessage() + appStore.showError(errorMessage.value) + isProcessing.value = false + return + } prepareOAuthBindAccessTokenCookie() - window.location.href = resolveWeChatStartURL('bind_current_user') + window.location.href = startURL + return + } + + const resumePath = buildExistingAccountResumePath() + if (!resumePath) { + errorMessage.value = resolveWeChatOAuthUnavailableMessage() + appStore.showError(errorMessage.value) + isProcessing.value = false return } const params = new URLSearchParams({ - redirect: buildExistingAccountResumePath(), + redirect: resumePath, }) const email = existingAccountEmail.value.trim() if (email) { diff --git a/frontend/src/views/auth/__tests__/WechatCallbackView.spec.ts b/frontend/src/views/auth/__tests__/WechatCallbackView.spec.ts index aa673238..7f26f3c8 100644 --- a/frontend/src/views/auth/__tests__/WechatCallbackView.spec.ts +++ b/frontend/src/views/auth/__tests__/WechatCallbackView.spec.ts @@ -14,8 +14,10 @@ const { setTokenMock, showSuccessMock, showErrorMock, + fetchPublicSettingsMock, routeState, locationState, + appStoreState, } = vi.hoisted(() => ({ exchangePendingOAuthCompletionMock: vi.fn(), completeWeChatOAuthRegistrationMock: vi.fn(), @@ -28,6 +30,7 @@ const { setTokenMock: vi.fn(), showSuccessMock: vi.fn(), showErrorMock: vi.fn(), + fetchPublicSettingsMock: vi.fn(), routeState: { query: {} as Record, }, @@ -39,6 +42,10 @@ const { pathname: '/auth/wechat/callback' } as { href: string; hash: string; search: string; pathname: string }, }, + appStoreState: { + cachedPublicSettings: null as null | Record, + publicSettingsLoaded: false, + }, })) vi.mock('vue-router', () => ({ @@ -102,8 +109,10 @@ vi.mock('@/stores', () => ({ setToken: setTokenMock, }), useAppStore: () => ({ + ...appStoreState, showSuccess: showSuccessMock, showError: showErrorMock, + fetchPublicSettings: fetchPublicSettingsMock, }), })) @@ -139,7 +148,10 @@ describe('WechatCallbackView', () => { showErrorMock.mockReset() prepareOAuthBindAccessTokenCookieMock.mockReset() getAuthTokenMock.mockReset() + fetchPublicSettingsMock.mockReset() routeState.query = {} + appStoreState.cachedPublicSettings = null + appStoreState.publicSettingsLoaded = false localStorage.clear() locationState.current = { href: 'http://localhost/auth/wechat/callback', @@ -157,6 +169,38 @@ describe('WechatCallbackView', () => { }) }) + it('overrides an incompatible query mode with the configured open capability during bind recovery', async () => { + routeState.query = { + wechat_bind_existing: '1', + mode: 'mp', + redirect: '/profile', + } + appStoreState.cachedPublicSettings = { + wechat_oauth_enabled: true, + wechat_oauth_open_enabled: true, + wechat_oauth_mp_enabled: false, + } + appStoreState.publicSettingsLoaded = true + getAuthTokenMock.mockReturnValue('current-auth-token') + + mount(WechatCallbackView, { + global: { + stubs: { + AuthLayout: { template: '
' }, + Icon: true, + RouterLink: { template: '' }, + transition: false, + }, + }, + }) + + await flushPromises() + + expect(prepareOAuthBindAccessTokenCookieMock).toHaveBeenCalledTimes(1) + expect(locationState.current.href).toContain('mode=open') + expect(locationState.current.href).not.toContain('mode=mp') + }) + it('does not send adoption decisions during the initial exchange', async () => { exchangePendingOAuthCompletionMock.mockResolvedValue({ access_token: 'access-token', diff --git a/frontend/src/views/user/ProfileView.vue b/frontend/src/views/user/ProfileView.vue index f7418be9..14d7efea 100644 --- a/frontend/src/views/user/ProfileView.vue +++ b/frontend/src/views/user/ProfileView.vue @@ -67,7 +67,6 @@