diff --git a/AdminPanel/client/src/api/index.js b/AdminPanel/client/src/api/index.js index b39ebe7c..43e700a5 100644 --- a/AdminPanel/client/src/api/index.js +++ b/AdminPanel/client/src/api/index.js @@ -41,6 +41,13 @@ export const fetchConfigurationSchema = async () => { return response.json(); }; +export const fetchBaseConfiguration = async () => { + const response = await safeFetch(`${API_BASE_PATH}/config/baseconfig`, {credentials: 'include'}); + if (response.status === 401) throw new Error('UNAUTHORIZED'); + if (!response.ok) throw new Error('Failed to fetch base configuration'); + return response.json(); +}; + export const updateConfiguration = async configData => { const response = await safeFetch(`${API_BASE_PATH}/config`, { method: 'PATCH', diff --git a/AdminPanel/client/src/hooks/useSchemaLoader.js b/AdminPanel/client/src/hooks/useSchemaLoader.js index cab42587..844c236b 100644 --- a/AdminPanel/client/src/hooks/useSchemaLoader.js +++ b/AdminPanel/client/src/hooks/useSchemaLoader.js @@ -1,29 +1,47 @@ import {useEffect} from 'react'; import {useDispatch, useSelector} from 'react-redux'; -import {selectSchema, selectSchemaLoading, selectSchemaError, fetchSchema} from '../store/slices/configSlice'; +import { + selectSchema, + selectSchemaLoading, + selectSchemaError, + selectBaseConfig, + selectBaseConfigLoading, + selectBaseConfigError, + fetchSchema, + fetchBaseConfig +} from '../store/slices/configSlice'; import {selectIsAuthenticated} from '../store/slices/userSlice'; /** - * Hook to load schema for authenticated users - * Fetches schema immediately when the hook is first used + * Hook to load schema and baseConfig for authenticated users + * Fetches both immediately when the hook is first used */ export const useSchemaLoader = () => { const dispatch = useDispatch(); const schema = useSelector(selectSchema); const schemaLoading = useSelector(selectSchemaLoading); const schemaError = useSelector(selectSchemaError); + const baseConfig = useSelector(selectBaseConfig); + const baseConfigLoading = useSelector(selectBaseConfigLoading); + const baseConfigError = useSelector(selectBaseConfigError); const isAuthenticated = useSelector(selectIsAuthenticated); useEffect(() => { - // Load schema only for authenticated users + // Load schema and baseConfig only for authenticated users if (isAuthenticated && !schema && !schemaLoading && !schemaError) { dispatch(fetchSchema()); } - }, [isAuthenticated, schema, schemaLoading, schemaError, dispatch]); + if (isAuthenticated && !baseConfig && !baseConfigLoading && !baseConfigError) { + dispatch(fetchBaseConfig()); + } + }, [isAuthenticated, schema, schemaLoading, schemaError, baseConfig, baseConfigLoading, baseConfigError, dispatch]); return { schema, schemaLoading, - schemaError + schemaError, + baseConfig, + baseConfigLoading, + baseConfigError }; }; diff --git a/AdminPanel/client/src/pages/Expiration/Expiration.js b/AdminPanel/client/src/pages/Expiration/Expiration.js index 5f9f521f..4ff8defe 100644 --- a/AdminPanel/client/src/pages/Expiration/Expiration.js +++ b/AdminPanel/client/src/pages/Expiration/Expiration.js @@ -1,6 +1,6 @@ import {useState, useRef} from 'react'; import {useSelector, useDispatch} from 'react-redux'; -import {saveConfig, selectConfig} from '../../store/slices/configSlice'; +import {saveConfig, selectConfig, selectBaseConfig} from '../../store/slices/configSlice'; import {getNestedValue} from '../../utils/getNestedValue'; import {mergeNestedObjects} from '../../utils/mergeNestedObjects'; import {useFieldValidation} from '../../hooks/useFieldValidation'; @@ -8,7 +8,7 @@ import PageHeader from '../../components/PageHeader/PageHeader'; import PageDescription from '../../components/PageDescription/PageDescription'; import Tabs from '../../components/Tabs/Tabs'; import Input from '../../components/Input/Input'; -import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton'; +import FixedSaveButtonGroup from '../../components/FixedSaveButtonGroup/FixedSaveButtonGroup'; import Section from '../../components/Section/Section'; import styles from './Expiration.module.scss'; @@ -20,6 +20,7 @@ const expirationTabs = [ function Expiration() { const dispatch = useDispatch(); const config = useSelector(selectConfig); + const baseConfig = useSelector(selectBaseConfig); const {validateField, getFieldError, hasValidationErrors, clearFieldError} = useFieldValidation(); const [activeTab, setActiveTab] = useState('garbage-collection'); @@ -46,12 +47,27 @@ function Expiration() { sessionabsolute: 'services.CoAuthoring.expire.sessionabsolute' }; + const TAB_FIELDS = { + 'garbage-collection': ['filesCron', 'documentsCron', 'files', 'filesremovedatonce'], + 'session-management': ['sessionidle', 'sessionabsolute'] + }; + + const computeHasChanges = (nextSettings = localSettings) => { + if (!config) return false; + + return Object.keys(CONFIG_PATHS).some(key => { + const currentValue = nextSettings[key]; + const originalFieldValue = getNestedValue(config, CONFIG_PATHS[key]); + return currentValue.toString() !== originalFieldValue.toString(); + }); + }; + // Reset state and errors to global config const resetToGlobalConfig = () => { if (config) { const settings = {}; Object.keys(CONFIG_PATHS).forEach(key => { - const value = getNestedValue(config, CONFIG_PATHS[key], ''); + const value = getNestedValue(config, CONFIG_PATHS[key]); settings[key] = value; }); setLocalSettings(settings); @@ -77,11 +93,6 @@ function Expiration() { // Handle field changes const handleFieldChange = (field, value) => { - setLocalSettings(prev => ({ - ...prev, - [field]: value - })); - // Validate fields with schema validation if (value !== '' && CONFIG_PATHS[field]) { let validationValue = value; @@ -97,14 +108,28 @@ function Expiration() { } } - // Check if there are changes - const hasFieldChanges = Object.keys(CONFIG_PATHS).some(key => { - const currentValue = key === field ? value : localSettings[key]; - const originalFieldValue = getNestedValue(config, CONFIG_PATHS[key], ''); - return currentValue.toString() !== originalFieldValue.toString(); + setLocalSettings(prev => { + const updatedSettings = { + ...prev, + [field]: value + }; + setHasChanges(computeHasChanges(updatedSettings)); + return updatedSettings; + }); + }; + + const resetToBaseConfig = () => { + const fieldsToReset = TAB_FIELDS[activeTab] || []; + const updatedSettings = {...localSettings}; + + fieldsToReset.forEach(key => { + const value = getNestedValue(baseConfig, CONFIG_PATHS[key]); + updatedSettings[key] = value; + clearFieldError(CONFIG_PATHS[key]); }); - setHasChanges(hasFieldChanges); + setLocalSettings(updatedSettings); + setHasChanges(computeHasChanges(updatedSettings)); }; // Handle save @@ -225,9 +250,19 @@ function Expiration() { {renderTabContent()} - - Save Changes - + ); } diff --git a/AdminPanel/client/src/pages/FileLimits/FileLimits.js b/AdminPanel/client/src/pages/FileLimits/FileLimits.js index 6b9c7d86..be341f37 100644 --- a/AdminPanel/client/src/pages/FileLimits/FileLimits.js +++ b/AdminPanel/client/src/pages/FileLimits/FileLimits.js @@ -1,20 +1,21 @@ import {useState, useEffect} from 'react'; import {useSelector, useDispatch} from 'react-redux'; -import {saveConfig, selectConfig} from '../../store/slices/configSlice'; +import {saveConfig, selectConfig, selectBaseConfig} from '../../store/slices/configSlice'; import {getNestedValue} from '../../utils/getNestedValue'; import {mergeNestedObjects} from '../../utils/mergeNestedObjects'; import {useFieldValidation} from '../../hooks/useFieldValidation'; import PageHeader from '../../components/PageHeader/PageHeader'; import PageDescription from '../../components/PageDescription/PageDescription'; import Input from '../../components/Input/Input'; -import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton'; +import FixedSaveButtonGroup from '../../components/FixedSaveButtonGroup/FixedSaveButtonGroup'; import Section from '../../components/Section/Section'; import styles from './FileLimits.module.scss'; function FileLimits() { const dispatch = useDispatch(); const config = useSelector(selectConfig); - const {validateField, getFieldError, hasValidationErrors} = useFieldValidation(); + const baseConfig = useSelector(selectBaseConfig); + const {validateField, getFieldError, hasValidationErrors, clearFieldError} = useFieldValidation(); // Local state for form fields const [localSettings, setLocalSettings] = useState({ @@ -35,16 +36,46 @@ function FileLimits() { vsdxUncompressed: 'FileConverter.converter.inputLimits.3.zip.uncompressed' }; + const computeHasChanges = (nextSettings = localSettings) => { + if (!config) return false; + + return Object.keys(nextSettings).some(key => { + const currentValue = nextSettings[key]; + let originalValue; + + if (key === 'maxDownloadBytes') { + originalValue = getNestedValue(config, 'FileConverter.converter.maxDownloadBytes'); + } else { + const inputLimits = getNestedValue(config, 'FileConverter.converter.inputLimits'); + if (key === 'docxUncompressed') { + const docxLimit = inputLimits.find(limit => limit.type && limit.type.includes('docx')); + originalValue = docxLimit?.zip?.uncompressed; + } else if (key === 'xlsxUncompressed') { + const xlsxLimit = inputLimits.find(limit => limit.type && limit.type.includes('xlsx')); + originalValue = xlsxLimit?.zip?.uncompressed; + } else if (key === 'pptxUncompressed') { + const pptxLimit = inputLimits.find(limit => limit.type && limit.type.includes('pptx')); + originalValue = pptxLimit?.zip?.uncompressed; + } else if (key === 'vsdxUncompressed') { + const vsdxLimit = inputLimits.find(limit => limit.type && limit.type.includes('vsdx')); + originalValue = vsdxLimit?.zip?.uncompressed; + } + } + + return currentValue.toString() !== originalValue.toString(); + }); + }; + // Load config data when component mounts useEffect(() => { if (config) { const settings = {}; // Get max download bytes - settings.maxDownloadBytes = getNestedValue(config, 'FileConverter.converter.maxDownloadBytes', ''); + settings.maxDownloadBytes = getNestedValue(config, 'FileConverter.converter.maxDownloadBytes'); // Get input limits - need to handle array structure - const inputLimits = getNestedValue(config, 'FileConverter.converter.inputLimits', []); + const inputLimits = getNestedValue(config, 'FileConverter.converter.inputLimits'); // Find limits by document type const docxLimit = inputLimits.find(limit => limit.type && limit.type.includes('docx')); @@ -77,35 +108,38 @@ function FileLimits() { } } - // Check if there are changes - const hasFieldChanges = Object.keys(localSettings).some(key => { - const currentValue = key === field ? value : localSettings[key]; - let originalValue; + setLocalSettings(prev => { + const updatedSettings = { + ...prev, + [field]: value + }; + setHasChanges(computeHasChanges(updatedSettings)); + return updatedSettings; + }); + }; - if (key === 'maxDownloadBytes') { - originalValue = getNestedValue(config, 'FileConverter.converter.maxDownloadBytes', ''); - } else { - // Handle input limits array structure for comparison - const inputLimits = getNestedValue(config, 'FileConverter.converter.inputLimits', []); - if (key === 'docxUncompressed') { - const docxLimit = inputLimits.find(limit => limit.type && limit.type.includes('docx')); - originalValue = docxLimit?.zip?.uncompressed || ''; - } else if (key === 'xlsxUncompressed') { - const xlsxLimit = inputLimits.find(limit => limit.type && limit.type.includes('xlsx')); - originalValue = xlsxLimit?.zip?.uncompressed || ''; - } else if (key === 'pptxUncompressed') { - const pptxLimit = inputLimits.find(limit => limit.type && limit.type.includes('pptx')); - originalValue = pptxLimit?.zip?.uncompressed || ''; - } else if (key === 'vsdxUncompressed') { - const vsdxLimit = inputLimits.find(limit => limit.type && limit.type.includes('vsdx')); - originalValue = vsdxLimit?.zip?.uncompressed || ''; - } - } + const resetToBaseConfig = () => { + const settings = {}; - return currentValue.toString() !== originalValue.toString(); + settings.maxDownloadBytes = getNestedValue(baseConfig, 'FileConverter.converter.maxDownloadBytes'); + + const inputLimits = getNestedValue(baseConfig, 'FileConverter.converter.inputLimits'); + const docxLimit = inputLimits.find(limit => limit.type && limit.type.includes('docx')); + const xlsxLimit = inputLimits.find(limit => limit.type && limit.type.includes('xlsx')); + const pptxLimit = inputLimits.find(limit => limit.type && limit.type.includes('pptx')); + const vsdxLimit = inputLimits.find(limit => limit.type && limit.type.includes('vsdx')); + + settings.docxUncompressed = docxLimit?.zip?.uncompressed || ''; + settings.xlsxUncompressed = xlsxLimit?.zip?.uncompressed || ''; + settings.pptxUncompressed = pptxLimit?.zip?.uncompressed || ''; + settings.vsdxUncompressed = vsdxLimit?.zip?.uncompressed || ''; + + Object.values(CONFIG_PATHS).forEach(path => { + clearFieldError(path); }); - setHasChanges(hasFieldChanges); + setLocalSettings(settings); + setHasChanges(computeHasChanges(settings)); }; // Handle save @@ -119,7 +153,7 @@ function FileLimits() { configUpdate['FileConverter.converter.maxDownloadBytes'] = parseInt(localSettings.maxDownloadBytes); // Update input limits - we need to preserve the existing structure - const currentInputLimits = getNestedValue(config, 'FileConverter.converter.inputLimits', []); + const currentInputLimits = getNestedValue(config, 'FileConverter.converter.inputLimits'); const updatedInputLimits = currentInputLimits.map(limit => { if (limit.type && limit.type.includes('docx')) { return { @@ -229,9 +263,19 @@ function FileLimits() { - - Save Changes - + ); } diff --git a/AdminPanel/client/src/pages/LoggerConfig/LoggerConfig.js b/AdminPanel/client/src/pages/LoggerConfig/LoggerConfig.js index 45140bfd..039060ee 100644 --- a/AdminPanel/client/src/pages/LoggerConfig/LoggerConfig.js +++ b/AdminPanel/client/src/pages/LoggerConfig/LoggerConfig.js @@ -1,13 +1,13 @@ import {useState, useRef} from 'react'; import {useSelector, useDispatch} from 'react-redux'; -import {saveConfig, selectConfig} from '../../store/slices/configSlice'; +import {saveConfig, selectConfig, selectBaseConfig} from '../../store/slices/configSlice'; import {getNestedValue} from '../../utils/getNestedValue'; import {mergeNestedObjects} from '../../utils/mergeNestedObjects'; import {useFieldValidation} from '../../hooks/useFieldValidation'; import PageHeader from '../../components/PageHeader/PageHeader'; import PageDescription from '../../components/PageDescription/PageDescription'; import Select from '../../components/Select/Select'; -import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton'; +import FixedSaveButtonGroup from '../../components/FixedSaveButtonGroup/FixedSaveButtonGroup'; import Section from '../../components/Section/Section'; import styles from './LoggerConfig.module.scss'; @@ -25,6 +25,7 @@ const LOG_LEVELS = [ function LoggerConfig() { const dispatch = useDispatch(); const config = useSelector(selectConfig); + const baseConfig = useSelector(selectBaseConfig); const {validateField, getFieldError, hasValidationErrors, clearFieldError} = useFieldValidation(); // Local state for form fields @@ -39,21 +40,36 @@ function LoggerConfig() { logLevel: 'log.options.categories.default.level' }; + const computeHasChanges = (nextSettings = localSettings) => { + if (!config) return false; + return Object.keys(CONFIG_PATHS).some(key => { + const currentValue = nextSettings[key]; + const originalValue = getNestedValue(config, CONFIG_PATHS[key]); + return currentValue !== originalValue; + }); + }; + // Reset state and errors to global config const resetToGlobalConfig = () => { if (config) { const settings = { - logLevel: getNestedValue(config, CONFIG_PATHS.logLevel, 'INFO') + logLevel: getNestedValue(config, CONFIG_PATHS.logLevel) }; setLocalSettings(settings); setHasChanges(false); - // Clear validation errors for all fields Object.values(CONFIG_PATHS).forEach(path => { clearFieldError(path); }); } }; + const resetToBaseConfig = () => { + const baseValue = getNestedValue(baseConfig, CONFIG_PATHS.logLevel); + setLocalSettings({logLevel: baseValue}); + clearFieldError(CONFIG_PATHS.logLevel); + setHasChanges(computeHasChanges({logLevel: baseValue})); + }; + // Initialize settings from config when component loads (only once) if (config && !hasInitialized.current) { resetToGlobalConfig(); @@ -62,25 +78,18 @@ function LoggerConfig() { // Handle field changes const handleFieldChange = (field, value) => { - setLocalSettings(prev => ({ - ...prev, - [field]: value - })); - - // Validate fields with schema validation if (CONFIG_PATHS[field]) { validateField(CONFIG_PATHS[field], value); } - // Check if there are changes - const hasFieldChanges = Object.keys(CONFIG_PATHS).some(key => { - const currentValue = key === field ? value : localSettings[key]; - const originalFieldValue = getNestedValue(config, CONFIG_PATHS[key], 'INFO'); - - return currentValue.toString() !== originalFieldValue.toString(); + setLocalSettings(prev => { + const updatedSettings = { + ...prev, + [field]: value + }; + setHasChanges(computeHasChanges(updatedSettings)); + return updatedSettings; }); - - setHasChanges(hasFieldChanges); }; // Handle save @@ -121,9 +130,19 @@ function LoggerConfig() { - - Save Changes - + ); } diff --git a/AdminPanel/client/src/pages/NotitifcationConfig/NotificationConfig.js b/AdminPanel/client/src/pages/NotitifcationConfig/NotificationConfig.js index 57f692ee..a3a8ca26 100644 --- a/AdminPanel/client/src/pages/NotitifcationConfig/NotificationConfig.js +++ b/AdminPanel/client/src/pages/NotitifcationConfig/NotificationConfig.js @@ -1,6 +1,6 @@ import {useState, useRef} from 'react'; import {useSelector, useDispatch} from 'react-redux'; -import {saveConfig, selectConfig} from '../../store/slices/configSlice'; +import {saveConfig, selectConfig, selectBaseConfig} from '../../store/slices/configSlice'; import {getNestedValue} from '../../utils/getNestedValue'; import {mergeNestedObjects} from '../../utils/mergeNestedObjects'; import {useFieldValidation} from '../../hooks/useFieldValidation'; @@ -9,7 +9,7 @@ import PageDescription from '../../components/PageDescription/PageDescription'; import Tabs from '../../components/Tabs/Tabs'; import Input from '../../components/Input/Input'; import Checkbox from '../../components/Checkbox/Checkbox'; -import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton'; +import FixedSaveButtonGroup from '../../components/FixedSaveButtonGroup/FixedSaveButtonGroup'; import Section from '../../components/Section/Section'; import styles from './NotificationConfig.module.scss'; @@ -22,6 +22,7 @@ const emailConfigTabs = [ function EmailConfig() { const dispatch = useDispatch(); const config = useSelector(selectConfig); + const baseConfig = useSelector(selectBaseConfig); const {validateField, getFieldError, hasValidationErrors, clearFieldError} = useFieldValidation(); const [activeTab, setActiveTab] = useState('notifications'); @@ -64,12 +65,44 @@ function EmailConfig() { licenseLimitLiveViewerRepeatInterval: 'notification.rules.licenseLimitLiveViewer.policies.repeatInterval' }; + const TAB_FIELDS = { + notifications: [ + 'licenseExpirationWarningEnable', + 'licenseExpirationWarningRepeatInterval', + 'licenseExpirationErrorEnable', + 'licenseExpirationErrorRepeatInterval', + 'licenseLimitEditEnable', + 'licenseLimitEditRepeatInterval', + 'licenseLimitLiveViewerEnable', + 'licenseLimitLiveViewerRepeatInterval' + ], + 'smtp-server': ['smtpHost', 'smtpPort', 'smtpUsername', 'smtpPassword'], + defaults: ['defaultFromEmail', 'defaultToEmail'] + }; + + const computeHasChanges = (nextSettings = localSettings) => { + if (!config) return false; + + return Object.keys(CONFIG_PATHS).some(key => { + const currentValue = nextSettings[key]; + const originalValue = getNestedValue(config, CONFIG_PATHS[key]); + + if (typeof originalValue === 'boolean') { + return currentValue !== originalValue; + } + + const normalizedCurrent = currentValue === undefined || currentValue === null ? '' : currentValue.toString(); + const normalizedOriginal = originalValue === undefined || originalValue === null ? '' : originalValue.toString(); + return normalizedCurrent !== normalizedOriginal; + }); + }; + // Reset state and errors to global config const resetToGlobalConfig = () => { if (config) { const settings = {}; Object.keys(CONFIG_PATHS).forEach(key => { - const value = getNestedValue(config, CONFIG_PATHS[key], ''); + const value = getNestedValue(config, CONFIG_PATHS[key]); settings[key] = value; }); setLocalSettings(settings); @@ -95,41 +128,40 @@ function EmailConfig() { // Handle field changes const handleFieldChange = (field, value) => { - setLocalSettings(prev => ({ - ...prev, - [field]: value - })); - - // Validate fields with schema validation if (CONFIG_PATHS[field]) { let validationValue = value; - // Convert port to integer for validation if (field === 'smtpPort' && value !== '') { validationValue = parseInt(value); if (!isNaN(validationValue)) { validateField(CONFIG_PATHS[field], validationValue); } - } else if (typeof value === 'string') { - validateField(CONFIG_PATHS[field], value); - } else if (typeof value === 'boolean') { + } else if (typeof value === 'string' || typeof value === 'boolean') { validateField(CONFIG_PATHS[field], value); } } - // Check if there are changes - const hasFieldChanges = Object.keys(CONFIG_PATHS).some(key => { - const currentValue = key === field ? value : localSettings[key]; - const originalFieldValue = getNestedValue(config, CONFIG_PATHS[key], ''); + setLocalSettings(prev => { + const updatedSettings = { + ...prev, + [field]: value + }; + setHasChanges(computeHasChanges(updatedSettings)); + return updatedSettings; + }); + }; - // Handle different data types properly - if (typeof originalFieldValue === 'boolean') { - return currentValue !== originalFieldValue; - } - return currentValue.toString() !== originalFieldValue.toString(); + const resetToBaseConfig = () => { + const fieldsToReset = TAB_FIELDS[activeTab]; + const updatedSettings = {...localSettings}; + + fieldsToReset.forEach(key => { + updatedSettings[key] = getNestedValue(baseConfig, CONFIG_PATHS[key]); + clearFieldError(CONFIG_PATHS[key]); }); - setHasChanges(hasFieldChanges); + setLocalSettings(updatedSettings); + setHasChanges(computeHasChanges(updatedSettings)); }; // Handle save @@ -144,7 +176,7 @@ function EmailConfig() { // Convert port to integer if (key === 'smtpPort') { - value = value ? parseInt(value) : 587; + value = parseInt(value); } configUpdate[path] = value; @@ -340,9 +372,19 @@ function EmailConfig() { {renderTabContent()} - - Save Changes - + ); } diff --git a/AdminPanel/client/src/pages/SecuritySettings/SecuritySettings.js b/AdminPanel/client/src/pages/SecuritySettings/SecuritySettings.js index 3e8a4e01..78a394de 100644 --- a/AdminPanel/client/src/pages/SecuritySettings/SecuritySettings.js +++ b/AdminPanel/client/src/pages/SecuritySettings/SecuritySettings.js @@ -1,6 +1,6 @@ import {useState, useRef} from 'react'; import {useSelector, useDispatch} from 'react-redux'; -import {saveConfig, selectConfig} from '../../store/slices/configSlice'; +import {saveConfig, selectConfig, selectBaseConfig} from '../../store/slices/configSlice'; import {getNestedValue} from '../../utils/getNestedValue'; import {mergeNestedObjects} from '../../utils/mergeNestedObjects'; import {useFieldValidation} from '../../hooks/useFieldValidation'; @@ -10,7 +10,7 @@ import Tabs from '../../components/Tabs/Tabs'; import AccessRules from '../../components/AccessRules/AccessRules'; import Section from '../../components/Section/Section'; import Checkbox from '../../components/Checkbox/Checkbox'; -import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton'; +import FixedSaveButtonGroup from '../../components/FixedSaveButtonGroup/FixedSaveButtonGroup'; import styles from './SecuritySettings.module.scss'; const securityTabs = [ @@ -27,6 +27,7 @@ const CONFIG_PATHS = { function SecuritySettings() { const dispatch = useDispatch(); const config = useSelector(selectConfig); + const baseConfig = useSelector(selectBaseConfig); const {validateField, getFieldError, hasValidationErrors, clearFieldError} = useFieldValidation(); const [activeTab, setActiveTab] = useState('ip-rules'); @@ -43,7 +44,7 @@ function SecuritySettings() { const computeHasChanges = (nextSettings = localSettings, nextRules = localRules) => { if (!config) return false; - const originalRules = getNestedValue(config, 'services.CoAuthoring.ipfilter.rules', []); + const originalRules = getNestedValue(config, 'services.CoAuthoring.ipfilter.rules'); const normalizedOriginalRules = originalRules.map(rule => ({ address: rule.address, allowed: !!rule.allowed @@ -55,7 +56,7 @@ function SecuritySettings() { const rulesChanged = JSON.stringify(normalizedOriginalRules) !== JSON.stringify(nextBackendRules); const settingsChanged = Object.keys(CONFIG_PATHS).some(key => { - const originalValue = getNestedValue(config, CONFIG_PATHS[key], false); + const originalValue = getNestedValue(config, CONFIG_PATHS[key]); return originalValue !== nextSettings[key]; }); @@ -65,16 +66,16 @@ function SecuritySettings() { const resetToGlobalConfig = () => { if (!config) return; - const ipFilterRules = getNestedValue(config, 'services.CoAuthoring.ipfilter.rules', []); - const uiRules = ipFilterRules.map(rule => ({ + const ipFilterRules = getNestedValue(config, 'services.CoAuthoring.ipfilter.rules'); + const rules = ipFilterRules.map(rule => ({ type: rule.allowed ? 'Allow' : 'Deny', value: rule.address })); - setLocalRules(uiRules); + setLocalRules(rules); const newSettings = {}; Object.keys(CONFIG_PATHS).forEach(key => { - newSettings[key] = getNestedValue(config, CONFIG_PATHS[key], false); + newSettings[key] = getNestedValue(config, CONFIG_PATHS[key]); clearFieldError(CONFIG_PATHS[key]); }); setLocalSettings(newSettings); @@ -82,6 +83,27 @@ function SecuritySettings() { setHasChanges(false); }; + const resetToBaseConfig = () => { + if (activeTab === 'ip-rules') { + const ipFilterRules = getNestedValue(baseConfig, 'services.CoAuthoring.ipfilter.rules'); + const newRules = ipFilterRules.map(rule => ({ + type: rule.allowed ? 'Allow' : 'Deny', + value: rule.address + })); + setLocalRules(newRules); + clearFieldError('services.CoAuthoring.ipfilter.rules'); + setHasChanges(computeHasChanges(localSettings, newRules)); + } else if (activeTab === 'request-filtering') { + const newSettings = {}; + Object.keys(CONFIG_PATHS).forEach(key => { + newSettings[key] = getNestedValue(baseConfig, CONFIG_PATHS[key]); + clearFieldError(CONFIG_PATHS[key]); + }); + setLocalSettings(newSettings); + setHasChanges(computeHasChanges(newSettings, localRules)); + } + }; + const handleTabChange = newTab => { setActiveTab(newTab); resetToGlobalConfig(); @@ -132,7 +154,7 @@ function SecuritySettings() { address: rule.value, allowed: rule.type === 'Allow' })); - const originalRules = getNestedValue(config, 'services.CoAuthoring.ipfilter.rules', []); + const originalRules = getNestedValue(config, 'services.CoAuthoring.ipfilter.rules'); if (JSON.stringify(originalRules) !== JSON.stringify(backendRules)) { updates.push({ @@ -141,7 +163,7 @@ function SecuritySettings() { } const settingsChanged = Object.keys(CONFIG_PATHS).some(key => { - const originalValue = getNestedValue(config, CONFIG_PATHS[key], false); + const originalValue = getNestedValue(config, CONFIG_PATHS[key]); return originalValue !== localSettings[key]; }); @@ -224,9 +246,19 @@ function SecuritySettings() { {renderTabContent()} - - Save Changes - + ); } diff --git a/AdminPanel/client/src/pages/WOPISettings/WOPISettings.js b/AdminPanel/client/src/pages/WOPISettings/WOPISettings.js index c9ed5067..140a79cf 100644 --- a/AdminPanel/client/src/pages/WOPISettings/WOPISettings.js +++ b/AdminPanel/client/src/pages/WOPISettings/WOPISettings.js @@ -1,6 +1,6 @@ import {useState, useRef} from 'react'; import {useSelector, useDispatch} from 'react-redux'; -import {saveConfig, selectConfig, rotateWopiKeysAction} from '../../store/slices/configSlice'; +import {saveConfig, selectConfig, selectBaseConfig, rotateWopiKeysAction} from '../../store/slices/configSlice'; import {getNestedValue} from '../../utils/getNestedValue'; import {mergeNestedObjects} from '../../utils/mergeNestedObjects'; import {useFieldValidation} from '../../hooks/useFieldValidation'; @@ -10,7 +10,7 @@ import PageDescription from '../../components/PageDescription/PageDescription'; import ToggleSwitch from '../../components/ToggleSwitch/ToggleSwitch'; import Input from '../../components/Input/Input'; import Checkbox from '../../components/Checkbox/Checkbox'; -import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton'; +import FixedSaveButtonGroup from '../../components/FixedSaveButtonGroup/FixedSaveButtonGroup'; import Note from '../../components/Note/Note'; import Section from '../../components/Section/Section'; import styles from './WOPISettings.module.scss'; @@ -18,6 +18,7 @@ import styles from './WOPISettings.module.scss'; function WOPISettings() { const dispatch = useDispatch(); const config = useSelector(selectConfig); + const baseConfig = useSelector(selectBaseConfig); const {validateField, hasValidationErrors} = useFieldValidation(); // Local state for WOPI settings @@ -28,9 +29,16 @@ function WOPISettings() { const hasInitialized = useRef(false); // Get the actual config values - const configWopiEnabled = getNestedValue(config, 'wopi.enable', false); - const wopiPublicKey = getNestedValue(config, 'wopi.publicKey', ''); - const configRefreshLockInterval = getNestedValue(config, 'wopi.refreshLockInterval', '10m'); + const configWopiEnabled = getNestedValue(config, 'wopi.enable'); + const wopiPublicKey = getNestedValue(config, 'wopi.publicKey'); + const configRefreshLockInterval = getNestedValue(config, 'wopi.refreshLockInterval'); + + const computeHasChanges = ({wopiEnabled = localWopiEnabled, rotateKeys = localRotateKeys, refreshLockInterval = localRefreshLockInterval} = {}) => { + const enableChanged = wopiEnabled !== configWopiEnabled; + const refreshChanged = refreshLockInterval !== configRefreshLockInterval; + const rotateChanged = !!rotateKeys; + return enableChanged || refreshChanged || rotateChanged; + }; const resetToGlobalConfig = () => { if (config) { @@ -43,6 +51,19 @@ function WOPISettings() { } }; + const resetToBaseConfig = () => { + const baseEnabled = getNestedValue(baseConfig, 'wopi.enable'); + const baseRefreshInterval = getNestedValue(baseConfig, 'wopi.refreshLockInterval'); + + setLocalWopiEnabled(baseEnabled); + setLocalRotateKeys(false); + setLocalRefreshLockInterval(baseRefreshInterval); + setHasChanges(computeHasChanges({wopiEnabled: baseEnabled, rotateKeys: false, refreshLockInterval: baseRefreshInterval})); + + validateField('wopi.enable', baseEnabled); + validateField('wopi.refreshLockInterval', baseRefreshInterval); + }; + // Initialize settings from config when component loads (only once) if (config && !hasInitialized.current) { resetToGlobalConfig(); @@ -50,12 +71,10 @@ function WOPISettings() { } const handleWopiEnabledChange = enabled => { + const nextRotateKeys = enabled ? localRotateKeys : false; setLocalWopiEnabled(enabled); - // If WOPI is disabled, uncheck rotate keys - if (!enabled) { - setLocalRotateKeys(false); - } - setHasChanges(enabled !== configWopiEnabled || localRotateKeys || localRefreshLockInterval !== configRefreshLockInterval); + setLocalRotateKeys(nextRotateKeys); + setHasChanges(computeHasChanges({wopiEnabled: enabled, rotateKeys: nextRotateKeys})); // Validate the boolean field validateField('wopi.enable', enabled); @@ -63,12 +82,12 @@ function WOPISettings() { const handleRotateKeysChange = checked => { setLocalRotateKeys(checked); - setHasChanges(localWopiEnabled !== configWopiEnabled || checked || localRefreshLockInterval !== configRefreshLockInterval); + setHasChanges(computeHasChanges({rotateKeys: checked})); }; const handleRefreshLockIntervalChange = value => { setLocalRefreshLockInterval(value); - setHasChanges(localWopiEnabled !== configWopiEnabled || localRotateKeys || value !== configRefreshLockInterval); + setHasChanges(computeHasChanges({refreshLockInterval: value})); validateField('wopi.refreshLockInterval', value); }; @@ -171,9 +190,19 @@ function WOPISettings() { )} - - Save Changes - + ); } diff --git a/AdminPanel/client/src/store/slices/configSlice.js b/AdminPanel/client/src/store/slices/configSlice.js index fae1fc26..688dc2a3 100644 --- a/AdminPanel/client/src/store/slices/configSlice.js +++ b/AdminPanel/client/src/store/slices/configSlice.js @@ -1,5 +1,12 @@ import {createSlice, createAsyncThunk} from '@reduxjs/toolkit'; -import {fetchConfiguration, fetchConfigurationSchema, updateConfiguration, rotateWopiKeys, resetConfiguration} from '../../api'; +import { + fetchConfiguration, + fetchConfigurationSchema, + fetchBaseConfiguration, + updateConfiguration, + rotateWopiKeys, + resetConfiguration +} from '../../api'; export const fetchConfig = createAsyncThunk('config/fetchConfig', async (_, {rejectWithValue}) => { try { @@ -19,6 +26,15 @@ export const fetchSchema = createAsyncThunk('config/fetchSchema', async (_, {rej } }); +export const fetchBaseConfig = createAsyncThunk('config/fetchBaseConfig', async (_, {rejectWithValue}) => { + try { + const baseConfig = await fetchBaseConfiguration(); + return {baseConfig}; + } catch (error) { + return rejectWithValue(error.message); + } +}); + export const saveConfig = createAsyncThunk('config/saveConfig', async (configData, {rejectWithValue}) => { try { const newConfig = await updateConfiguration(configData); @@ -48,13 +64,16 @@ export const resetConfig = createAsyncThunk('config/resetConfig', async (paths, const initialState = { config: null, + baseConfig: null, schema: null, // Full schema for admin panel passwordSchema: null, // Minimal schema for Setup page password validation loading: false, schemaLoading: false, + baseConfigLoading: false, saving: false, error: null, - schemaError: null + schemaError: null, + baseConfigError: null }; const configSlice = createSlice({ @@ -109,6 +128,19 @@ const configSlice = createSlice({ state.schemaLoading = false; state.schemaError = action.payload; }) + .addCase(fetchBaseConfig.pending, state => { + state.baseConfigLoading = true; + state.baseConfigError = null; + }) + .addCase(fetchBaseConfig.fulfilled, (state, action) => { + state.baseConfigLoading = false; + state.baseConfig = action.payload.baseConfig; + state.baseConfigError = null; + }) + .addCase(fetchBaseConfig.rejected, (state, action) => { + state.baseConfigLoading = false; + state.baseConfigError = action.payload; + }) // Save config cases .addCase(saveConfig.pending, state => { state.saving = true; @@ -156,12 +188,15 @@ export const {updateLocalConfig, clearConfig, clearError, setPasswordSchema} = c // Selectors export const selectConfig = state => state.config.config; +export const selectBaseConfig = state => state.config.baseConfig; export const selectSchema = state => state.config.schema; export const selectPasswordSchema = state => state.config.passwordSchema; export const selectConfigLoading = state => state.config.loading; export const selectSchemaLoading = state => state.config.schemaLoading; +export const selectBaseConfigLoading = state => state.config.baseConfigLoading; export const selectConfigSaving = state => state.config.saving; export const selectConfigError = state => state.config.error; export const selectSchemaError = state => state.config.schemaError; +export const selectBaseConfigError = state => state.config.baseConfigError; export default configSlice.reducer; diff --git a/AdminPanel/server/sources/routes/config/config.service.js b/AdminPanel/server/sources/routes/config/config.service.js index ac90c459..127eb341 100644 --- a/AdminPanel/server/sources/routes/config/config.service.js +++ b/AdminPanel/server/sources/routes/config/config.service.js @@ -36,6 +36,8 @@ const addFormats = require('ajv-formats'); const addErrors = require('ajv-errors'); const logger = require('../../../../../Common/sources/logger'); const tenantManager = require('../../../../../Common/sources/tenantManager'); +const moduleReloader = require('../../../../../Common/sources/moduleReloader'); +const utils = require('../../../../../Common/sources/utils'); const supersetSchema = require('../../../../../Common/config/schemas/config.schema.json'); const {deriveSchemaForScope, X_SCOPE_KEYWORD} = require('./config.schema.utils'); @@ -114,4 +116,20 @@ function getScopedConfig(ctx) { return configCopy; } -module.exports = {validateScoped, getScopedConfig}; +/** + * Filters base configuration to include only fields defined in the appropriate schema + * @returns {Object} Filtered base configuration object + */ +function getScopedBaseConfig() { + const baseConfig = utils.deepMergeObjects({}, moduleReloader.getBaseConfig()); + + if (!baseConfig.log) { + baseConfig.log = {}; + } + baseConfig.log.options = logger.getInitialLoggerConfig(); + + filterAdmin(baseConfig); + return baseConfig; +} + +module.exports = {validateScoped, getScopedConfig, getScopedBaseConfig}; diff --git a/AdminPanel/server/sources/routes/config/router.js b/AdminPanel/server/sources/routes/config/router.js index be510f80..e0813333 100644 --- a/AdminPanel/server/sources/routes/config/router.js +++ b/AdminPanel/server/sources/routes/config/router.js @@ -4,7 +4,7 @@ const express = require('express'); const bodyParser = require('body-parser'); const tenantManager = require('../../../../../Common/sources/tenantManager'); const runtimeConfigManager = require('../../../../../Common/sources/runtimeConfigManager'); -const {getScopedConfig, validateScoped} = require('./config.service'); +const {getScopedConfig, getScopedBaseConfig, validateScoped} = require('./config.service'); const {validateJWT} = require('../../middleware/auth'); const cookieParser = require('cookie-parser'); const utils = require('../../../../../Common/sources/utils'); @@ -40,6 +40,21 @@ router.get('/schema', validateJWT, async (_req, res) => { res.json(supersetSchema); }); +router.get('/baseconfig', validateJWT, async (req, res) => { + const ctx = req.ctx; + try { + ctx.logger.info('baseconfig get start'); + const scopedBaseConfig = getScopedBaseConfig(); + res.setHeader('Content-Type', 'application/json'); + res.json(scopedBaseConfig); + } catch (error) { + ctx.logger.error('Baseconfig get error: %s', error.stack); + res.status(500).json({error: 'Internal server error'}); + } finally { + ctx.logger.info('baseconfig get end'); + } +}); + router.patch('/', validateJWT, rawFileParser, async (req, res) => { const ctx = req.ctx; try { diff --git a/Common/sources/logger.js b/Common/sources/logger.js index d933e6ba..92f735ee 100644 --- a/Common/sources/logger.js +++ b/Common/sources/logger.js @@ -126,5 +126,8 @@ exports.shutdown = function (callback) { }; exports.configureLogger = configureLogger; exports.getLoggerConfig = function () { - return curLogConfig; + return config.util.extendDeep({}, curLogConfig); +}; +exports.getInitialLoggerConfig = function () { + return config.util.extendDeep({}, cachedLogConfig); };