From 1042350f4e0f6ed44b51a1d707de665f71437faa Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Tue, 14 Apr 2026 16:21:49 +0200 Subject: [PATCH] fix(editor): Reset OIDC form dirty state after saving IdP settings (#28388) --- .../sso/components/OidcSettingsForm.vue | 11 ++-- .../sso/components/SamlSettingsForm.vue | 11 ++-- .../src/features/settings/sso/sso.store.ts | 1 + .../src/features/settings/sso/sso.test.ts | 53 +++++++++++++++++++ .../settings/sso/views/SettingsSso.vue | 8 +-- 5 files changed, 71 insertions(+), 13 deletions(-) diff --git a/packages/frontend/editor-ui/src/features/settings/sso/components/OidcSettingsForm.vue b/packages/frontend/editor-ui/src/features/settings/sso/components/OidcSettingsForm.vue index baea49e59b6..bc1b6bf8e71 100644 --- a/packages/frontend/editor-ui/src/features/settings/sso/components/OidcSettingsForm.vue +++ b/packages/frontend/editor-ui/src/features/settings/sso/components/OidcSettingsForm.vue @@ -115,10 +115,10 @@ const cannotSaveOidcSettings = computed(() => { ); }); -async function onOidcSettingsSave(provisioningChangesConfirmed: boolean = false) { +async function onOidcSettingsSave(provisioningChangesConfirmed: boolean = false): Promise { if (!provisioningChangesConfirmed && roleAssignmentTransition.value !== 'none') { showUserRoleProvisioningDialog.value = true; - return; + return false; } const isLoginEnabledChanged = ssoStore.oidcConfig?.loginEnabled !== ssoStore.isOidcLoginEnabled; @@ -140,7 +140,7 @@ async function onOidcSettingsSave(provisioningChangesConfirmed: boolean = false) ), }, ); - if (confirmAction !== MODAL_CONFIRM) return; + if (confirmAction !== MODAL_CONFIRM) return false; } const acrArray = authenticationContextClassReference.value @@ -176,12 +176,13 @@ async function onOidcSettingsSave(provisioningChangesConfirmed: boolean = false) title: i18n.baseText('settings.sso.settings.save.success'), type: 'success', }); + return true; } catch (error) { toast.showError(error, i18n.baseText('settings.sso.settings.save.error_oidc')); - return; + return false; } finally { - savingForm.value = false; await getOidcConfig(); + savingForm.value = false; } } diff --git a/packages/frontend/editor-ui/src/features/settings/sso/components/SamlSettingsForm.vue b/packages/frontend/editor-ui/src/features/settings/sso/components/SamlSettingsForm.vue index 283de582352..6e9af1ca869 100644 --- a/packages/frontend/editor-ui/src/features/settings/sso/components/SamlSettingsForm.vue +++ b/packages/frontend/editor-ui/src/features/settings/sso/components/SamlSettingsForm.vue @@ -199,7 +199,7 @@ const prompTestSamlConnectionBeforeActivating = async () => { return promptOpeningTestConnectionPage; }; -const onSave = async (provisioningChangesConfirmed: boolean = false) => { +const onSave = async (provisioningChangesConfirmed: boolean = false): Promise => { try { savingForm.value = true; validateSamlInput(); @@ -210,13 +210,13 @@ const onSave = async (provisioningChangesConfirmed: boolean = false) => { if (isDisablingSamlLogin) { const confirmDisablingSaml = await promptConfirmDisablingSamlLogin(); if (confirmDisablingSaml !== MODAL_CONFIRM) { - return; + return false; } } if (!provisioningChangesConfirmed && roleAssignmentTransition.value !== 'none') { showUserRoleProvisioningDialog.value = true; - return; + return false; } showUserRoleProvisioningDialog.value = false; @@ -233,7 +233,7 @@ const onSave = async (provisioningChangesConfirmed: boolean = false) => { const confirmTest = await prompTestSamlConnectionBeforeActivating(); if (confirmTest !== MODAL_CONFIRM) { - return; + return false; } } @@ -257,9 +257,10 @@ const onSave = async (provisioningChangesConfirmed: boolean = false) => { title: i18n.baseText('settings.sso.settings.save.success'), type: 'success', }); + return true; } catch (error) { toast.showError(error, i18n.baseText('settings.sso.settings.save.error')); - return; + return false; } finally { savingForm.value = false; } diff --git a/packages/frontend/editor-ui/src/features/settings/sso/sso.store.ts b/packages/frontend/editor-ui/src/features/settings/sso/sso.store.ts index ef14075b282..2a92c0a41a1 100644 --- a/packages/frontend/editor-ui/src/features/settings/sso/sso.store.ts +++ b/packages/frontend/editor-ui/src/features/settings/sso/sso.store.ts @@ -139,6 +139,7 @@ export const useSSOStore = defineStore('sso', () => { const getOidcConfig = async () => { const config = await ssoApi.getOidcConfig(rootStore.restApiContext); oidcConfig.value = config; + oidc.value.loginEnabled = config.loginEnabled; return config; }; diff --git a/packages/frontend/editor-ui/src/features/settings/sso/sso.test.ts b/packages/frontend/editor-ui/src/features/settings/sso/sso.test.ts index 9c180069aa7..4944f148c5e 100644 --- a/packages/frontend/editor-ui/src/features/settings/sso/sso.test.ts +++ b/packages/frontend/editor-ui/src/features/settings/sso/sso.test.ts @@ -1,6 +1,10 @@ +import type { OidcConfigDto } from '@n8n/api-types'; import { createPinia, setActivePinia } from 'pinia'; import { useSSOStore, SupportedProtocols } from '@/features/settings/sso/sso.store'; import type { UserManagementAuthenticationMethod } from '@/Interface'; +import * as ssoApi from '@n8n/rest-api-client/api/sso'; + +vi.mock('@n8n/rest-api-client/api/sso'); let ssoStore: ReturnType; @@ -150,4 +154,53 @@ describe('SSO store', () => { expect(ssoStore.selectedAuthProtocol).toBe(SupportedProtocols.SAML); }); }); + + describe('getOidcConfig', () => { + it('should sync oidc.loginEnabled when fetching config', async () => { + const oidcConfig: OidcConfigDto = { + clientId: 'test-id', + clientSecret: 'test-secret', + discoveryEndpoint: 'https://example.com/.well-known/openid-configuration', + loginEnabled: true, + prompt: 'select_account', + authenticationContextClassReference: [], + }; + + vi.mocked(ssoApi.getOidcConfig).mockResolvedValue(oidcConfig); + + // loginEnabled starts as false (default) + expect(ssoStore.isOidcLoginEnabled).toBe(false); + + await ssoStore.getOidcConfig(); + + // After fetching config, loginEnabled should be synced + expect(ssoStore.isOidcLoginEnabled).toBe(true); + expect(ssoStore.oidcConfig).toEqual(oidcConfig); + }); + + it('should reset oidc.loginEnabled to false when server config has it disabled', async () => { + // Start with loginEnabled = true via initialize + ssoStore.initialize({ + authenticationMethod: 'oidc' as UserManagementAuthenticationMethod, + config: { oidc: { loginEnabled: true } }, + features: { saml: false, ldap: false, oidc: true }, + }); + expect(ssoStore.isOidcLoginEnabled).toBe(true); + + // Server returns config with loginEnabled = false + vi.mocked(ssoApi.getOidcConfig).mockResolvedValue({ + clientId: 'test-id', + clientSecret: 'test-secret', + discoveryEndpoint: 'https://example.com/.well-known/openid-configuration', + loginEnabled: false, + prompt: 'select_account', + authenticationContextClassReference: [], + }); + + await ssoStore.getOidcConfig(); + + // loginEnabled should now be false, matching server state + expect(ssoStore.isOidcLoginEnabled).toBe(false); + }); + }); }); diff --git a/packages/frontend/editor-ui/src/features/settings/sso/views/SettingsSso.vue b/packages/frontend/editor-ui/src/features/settings/sso/views/SettingsSso.vue index fba80a56480..b6c7d66c836 100644 --- a/packages/frontend/editor-ui/src/features/settings/sso/views/SettingsSso.vue +++ b/packages/frontend/editor-ui/src/features/settings/sso/views/SettingsSso.vue @@ -77,9 +77,11 @@ onBeforeRouteLeave((_to, _from, next) => { async function onSaveAndLeave() { showUnsavedChangesDialog.value = false; - await activeForm.value?.onSave(); - pendingNext.value?.(); - pendingNext.value = null; + const saved = await activeForm.value?.onSave(); + if (saved) { + pendingNext.value?.(); + pendingNext.value = null; + } } function onLeaveWithoutSaving() {