From d70ec668f1b0795df6ebcb9d5259cd539a41cd2a Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Thu, 16 Oct 2025 16:25:05 +0100 Subject: [PATCH] remove unused settings and enhance others --- frontend/public/Login/github.svg | 3 + frontend/public/Login/google.svg | 14 + .../public/locales/en-GB/translation.json | 13 + .../shared/config/configNavSections.tsx | 7 - .../AdminConnectionsSection.tsx | 532 +++++++++--------- .../configSections/AdminGeneralSection.tsx | 31 +- .../configSections/AdminSecuritySection.tsx | 44 +- .../config/configSections/ProviderCard.tsx | 191 +++++++ .../configSections/providerDefinitions.ts | 353 ++++++++++++ .../src/components/shared/config/types.ts | 1 - 10 files changed, 850 insertions(+), 339 deletions(-) create mode 100644 frontend/public/Login/github.svg create mode 100644 frontend/public/Login/google.svg create mode 100644 frontend/src/components/shared/config/configSections/ProviderCard.tsx create mode 100644 frontend/src/components/shared/config/configSections/providerDefinitions.ts diff --git a/frontend/public/Login/github.svg b/frontend/public/Login/github.svg new file mode 100644 index 000000000..651eaac2b --- /dev/null +++ b/frontend/public/Login/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/Login/google.svg b/frontend/public/Login/google.svg new file mode 100644 index 000000000..27e4a4ac9 --- /dev/null +++ b/frontend/public/Login/google.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 8a2f91e03..a05338de1 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -3213,6 +3213,8 @@ "admin": { "error": "Error", "success": "Success", + "expand": "Expand", + "close": "Close", "status": { "active": "Active", "inactive": "Inactive" @@ -3223,6 +3225,7 @@ "fetchError": "Failed to load settings", "saveError": "Failed to save settings", "saved": "Settings saved successfully", + "saveSuccess": "Settings saved successfully", "save": "Save Changes", "restartRequired": "Settings changes require a server restart to take effect.", "general": { @@ -3252,6 +3255,10 @@ "security": { "title": "Security", "description": "Configure authentication, login behaviour, and security policies.", + "ssoNotice": { + "title": "Looking for SSO/SAML settings?", + "message": "OAuth2 and SAML2 authentication providers have been moved to the Connections menu for easier management." + }, "authentication": "Authentication", "enableLogin": "Enable Login", "enableLogin.description": "Require users to log in before accessing the application", @@ -3287,6 +3294,12 @@ "connections": { "title": "Connections", "description": "Configure external authentication providers like OAuth2 and SAML.", + "linkedServices": "Linked Services", + "unlinkedServices": "Unlinked Services", + "connect": "Connect", + "disconnect": "Disconnect", + "disconnected": "Provider disconnected successfully", + "disconnectError": "Failed to disconnect provider", "oauth2": "OAuth2", "oauth2.enabled": "Enable OAuth2", "oauth2.enabled.description": "Allow users to authenticate using OAuth2 providers", diff --git a/frontend/src/components/shared/config/configNavSections.tsx b/frontend/src/components/shared/config/configNavSections.tsx index 1c0e51d9f..e129919be 100644 --- a/frontend/src/components/shared/config/configNavSections.tsx +++ b/frontend/src/components/shared/config/configNavSections.tsx @@ -7,7 +7,6 @@ import AdminSecuritySection from './configSections/AdminSecuritySection'; import AdminConnectionsSection from './configSections/AdminConnectionsSection'; import AdminPrivacySection from './configSections/AdminPrivacySection'; import AdminAdvancedSection from './configSections/AdminAdvancedSection'; -import AdminMailSection from './configSections/AdminMailSection'; import AdminLegalSection from './configSections/AdminLegalSection'; import AdminPremiumSection from './configSections/AdminPremiumSection'; import AdminEndpointsSection from './configSections/AdminEndpointsSection'; @@ -93,12 +92,6 @@ export const createConfigNavSections = ( icon: 'link-rounded', component: }, - { - key: 'adminMail', - label: 'Mail', - icon: 'mail-rounded', - component: - }, { key: 'adminLegal', label: 'Legal', diff --git a/frontend/src/components/shared/config/configSections/AdminConnectionsSection.tsx b/frontend/src/components/shared/config/configSections/AdminConnectionsSection.tsx index 3fb2a3620..7fd3b72a1 100644 --- a/frontend/src/components/shared/config/configSections/AdminConnectionsSection.tsx +++ b/frontend/src/components/shared/config/configSections/AdminConnectionsSection.tsx @@ -1,40 +1,49 @@ import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { TextInput, Switch, Button, Stack, Paper, Text, Loader, Group, Select, Badge, PasswordInput } from '@mantine/core'; +import { Stack, Text, Loader, Group, Divider } from '@mantine/core'; import { alert } from '../../../toast'; -import LocalIcon from '../../LocalIcon'; import RestartConfirmationModal from '../RestartConfirmationModal'; import { useRestartServer } from '../useRestartServer'; - -interface OAuth2Settings { - enabled?: boolean; - issuer?: string; - clientId?: string; - clientSecret?: string; - provider?: string; - autoCreateUser?: boolean; - blockRegistration?: boolean; - useAsUsername?: string; - scopes?: string; -} - -interface SAML2Settings { - enabled?: boolean; - provider?: string; - autoCreateUser?: boolean; - blockRegistration?: boolean; - registrationId?: string; -} +import ProviderCard from './ProviderCard'; +import { + ALL_PROVIDERS, + OAUTH2_PROVIDERS, + GENERIC_OAUTH2_PROVIDER, + SAML2_PROVIDER, + Provider, +} from './providerDefinitions'; interface ConnectionsSettingsData { - oauth2?: OAuth2Settings; - saml2?: SAML2Settings; + oauth2?: { + enabled?: boolean; + issuer?: string; + clientId?: string; + clientSecret?: string; + provider?: string; + autoCreateUser?: boolean; + blockRegistration?: boolean; + useAsUsername?: string; + scopes?: string; + client?: { + [key: string]: any; + }; + }; + saml2?: { + [key: string]: any; + }; + mail?: { + enabled?: boolean; + host?: string; + port?: number; + username?: string; + password?: string; + from?: string; + }; } export default function AdminConnectionsSection() { const { t } = useTranslation(); const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); const [settings, setSettings] = useState({}); const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); @@ -44,16 +53,19 @@ export default function AdminConnectionsSection() { const fetchSettings = async () => { try { - // OAuth2 and SAML2 are nested under security section - const response = await fetch('/api/v1/admin/settings/section/security'); - if (response.ok) { - const data = await response.json(); - // Extract oauth2 and saml2 from security section - setSettings({ - oauth2: data.oauth2 || {}, - saml2: data.saml2 || {} - }); - } + // Fetch security settings (oauth2, saml2) + const securityResponse = await fetch('/api/v1/admin/settings/section/security'); + const securityData = securityResponse.ok ? await securityResponse.json() : {}; + + // Fetch mail settings + const mailResponse = await fetch('/api/v1/admin/settings/section/mail'); + const mailData = mailResponse.ok ? await mailResponse.json() : {}; + + setSettings({ + oauth2: securityData.oauth2 || {}, + saml2: securityData.saml2 || {}, + mail: mailData || {} + }); } catch (error) { console.error('Failed to fetch connections settings:', error); alert({ @@ -66,36 +78,111 @@ export default function AdminConnectionsSection() { } }; - const handleSave = async () => { - setSaving(true); + const isProviderConfigured = (provider: Provider): boolean => { + if (provider.id === 'saml2') { + return settings.saml2?.enabled === true; + } + + if (provider.id === 'smtp') { + return settings.mail?.enabled === true; + } + + if (provider.id === 'oauth2-generic') { + return settings.oauth2?.enabled === true; + } + + // Check if specific OAuth2 provider is configured (has clientId) + const providerSettings = settings.oauth2?.client?.[provider.id]; + return !!(providerSettings?.clientId); + }; + + const getProviderSettings = (provider: Provider): Record => { + if (provider.id === 'saml2') { + return settings.saml2 || {}; + } + + if (provider.id === 'smtp') { + return settings.mail || {}; + } + + if (provider.id === 'oauth2-generic') { + // Generic OAuth2 settings are at the root oauth2 level + return { + enabled: settings.oauth2?.enabled, + provider: settings.oauth2?.provider, + issuer: settings.oauth2?.issuer, + clientId: settings.oauth2?.clientId, + clientSecret: settings.oauth2?.clientSecret, + scopes: settings.oauth2?.scopes, + useAsUsername: settings.oauth2?.useAsUsername, + autoCreateUser: settings.oauth2?.autoCreateUser, + blockRegistration: settings.oauth2?.blockRegistration, + }; + } + + // Specific OAuth2 provider settings + return settings.oauth2?.client?.[provider.id] || {}; + }; + + const handleProviderSave = async (provider: Provider, providerSettings: Record) => { try { - // Use delta update endpoint with dot notation for nested oauth2/saml2 settings - const deltaSettings: Record = {}; - - // Convert oauth2 settings to dot notation - if (settings.oauth2) { - Object.keys(settings.oauth2).forEach(key => { - deltaSettings[`security.oauth2.${key}`] = (settings.oauth2 as any)[key]; + if (provider.id === 'smtp') { + // Mail settings use a different endpoint + const response = await fetch('/api/v1/admin/settings/section/mail', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(providerSettings), }); - } - // Convert saml2 settings to dot notation - if (settings.saml2) { - Object.keys(settings.saml2).forEach(key => { - deltaSettings[`security.saml2.${key}`] = (settings.saml2 as any)[key]; - }); - } - - const response = await fetch('/api/v1/admin/settings', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ settings: deltaSettings }), - }); - - if (response.ok) { - showRestartModal(); + if (response.ok) { + await fetchSettings(); // Refresh settings + alert({ + alertType: 'success', + title: t('admin.success', 'Success'), + body: t('admin.settings.saveSuccess', 'Settings saved successfully'), + }); + showRestartModal(); + } else { + throw new Error('Failed to save'); + } } else { - throw new Error('Failed to save'); + // OAuth2/SAML2 use delta settings + const deltaSettings: Record = {}; + + if (provider.id === 'saml2') { + // SAML2 settings + Object.keys(providerSettings).forEach((key) => { + deltaSettings[`security.saml2.${key}`] = providerSettings[key]; + }); + } else if (provider.id === 'oauth2-generic') { + // Generic OAuth2 settings at root level + Object.keys(providerSettings).forEach((key) => { + deltaSettings[`security.oauth2.${key}`] = providerSettings[key]; + }); + } else { + // Specific OAuth2 provider (google, github, keycloak) + Object.keys(providerSettings).forEach((key) => { + deltaSettings[`security.oauth2.client.${provider.id}.${key}`] = providerSettings[key]; + }); + } + + const response = await fetch('/api/v1/admin/settings', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ settings: deltaSettings }), + }); + + if (response.ok) { + await fetchSettings(); // Refresh settings + alert({ + alertType: 'success', + title: t('admin.success', 'Success'), + body: t('admin.settings.saveSuccess', 'Settings saved successfully'), + }); + showRestartModal(); + } else { + throw new Error('Failed to save'); + } } } catch (error) { alert({ @@ -103,8 +190,68 @@ export default function AdminConnectionsSection() { title: t('admin.error', 'Error'), body: t('admin.settings.saveError', 'Failed to save settings'), }); - } finally { - setSaving(false); + } + }; + + const handleProviderDisconnect = async (provider: Provider) => { + try { + if (provider.id === 'smtp') { + // Mail settings use a different endpoint + const response = await fetch('/api/v1/admin/settings/section/mail', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ enabled: false }), + }); + + if (response.ok) { + await fetchSettings(); + alert({ + alertType: 'success', + title: t('admin.success', 'Success'), + body: t('admin.settings.connections.disconnected', 'Provider disconnected successfully'), + }); + showRestartModal(); + } else { + throw new Error('Failed to disconnect'); + } + } else { + const deltaSettings: Record = {}; + + if (provider.id === 'saml2') { + deltaSettings['security.saml2.enabled'] = false; + } else if (provider.id === 'oauth2-generic') { + deltaSettings['security.oauth2.enabled'] = false; + } else { + // Clear all fields for specific OAuth2 provider + provider.fields.forEach((field) => { + deltaSettings[`security.oauth2.client.${provider.id}.${field.key}`] = ''; + }); + } + + const response = await fetch('/api/v1/admin/settings', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ settings: deltaSettings }), + }); + + if (response.ok) { + await fetchSettings(); + alert({ + alertType: 'success', + title: t('admin.success', 'Success'), + body: t('admin.settings.connections.disconnected', 'Provider disconnected successfully'), + }); + showRestartModal(); + } else { + throw new Error('Failed to disconnect'); + } + } + } catch (error) { + alert({ + alertType: 'error', + title: t('admin.error', 'Error'), + body: t('admin.settings.connections.disconnectError', 'Failed to disconnect provider'), + }); } }; @@ -116,225 +263,68 @@ export default function AdminConnectionsSection() { ); } - const getProviderIcon = (provider?: string) => { - switch (provider?.toLowerCase()) { - case 'google': - return ; - case 'github': - return ; - case 'keycloak': - return ; - default: - return ; - } - }; + const linkedProviders = ALL_PROVIDERS.filter((p) => isProviderConfigured(p)); + const availableProviders = ALL_PROVIDERS.filter((p) => !isProviderConfigured(p)); return ( - + + {/* Header */}
- {t('admin.settings.connections.title', 'Connections')} + + {t('admin.settings.connections.title', 'Connections')} + - {t('admin.settings.connections.description', 'Configure external authentication providers like OAuth2 and SAML.')} + {t( + 'admin.settings.connections.description', + 'Configure external authentication providers like OAuth2 and SAML.' + )}
- {/* OAuth2 Settings */} - - - - - - {t('admin.settings.connections.oauth2', 'OAuth2')} - - - {settings.oauth2?.enabled ? t('admin.status.active', 'Active') : t('admin.status.inactive', 'Inactive')} - - - -
-
- {t('admin.settings.connections.oauth2.enabled', 'Enable OAuth2')} - - {t('admin.settings.connections.oauth2.enabled.description', 'Allow users to authenticate using OAuth2 providers')} - -
- setSettings({ ...settings, oauth2: { ...settings.oauth2, enabled: e.target.checked } })} - /> -
- + {/* Linked Services Section - Only show if there are linked providers */} + {linkedProviders.length > 0 && ( + <>
-