diff --git a/AdminPanel/client/src/api/index.js b/AdminPanel/client/src/api/index.js index a9594bcc..1ac555a9 100644 --- a/AdminPanel/client/src/api/index.js +++ b/AdminPanel/client/src/api/index.js @@ -21,7 +21,7 @@ const safeFetch = async (url, options = {}) => { }; export const fetchStatistics = async () => { - const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/stat`); + const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/stat`, {credentials: 'include'}); if (!response.ok) throw new Error('Failed to fetch statistics'); return response.json(); }; @@ -34,7 +34,7 @@ export const fetchConfiguration = async () => { }; export const fetchConfigurationSchema = async () => { - const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/config/schema`); + const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/config/schema`, {credentials: 'include'}); if (!response.ok) throw new Error('Failed to fetch configuration schema'); return response.json(); }; diff --git a/AdminPanel/client/src/components/AuthWrapper/AuthWrapper.js b/AdminPanel/client/src/components/AuthWrapper/AuthWrapper.js index ec254143..1e631169 100644 --- a/AdminPanel/client/src/components/AuthWrapper/AuthWrapper.js +++ b/AdminPanel/client/src/components/AuthWrapper/AuthWrapper.js @@ -1,6 +1,7 @@ import {useEffect, useState} from 'react'; import {useDispatch, useSelector} from 'react-redux'; import {fetchUser, selectUser, selectUserLoading, selectIsAuthenticated} from '../../store/slices/userSlice'; +import {setPasswordSchema} from '../../store/slices/configSlice'; import {checkSetupRequired} from '../../api'; import Spinner from '../../assets/Spinner.svg'; import Login from '../../pages/Login/LoginPage'; @@ -22,6 +23,11 @@ export default function AuthWrapper({children}) { try { const result = await checkSetupRequired(); setSetupRequired(result.setupRequired); + + // Save minimal password schema to Redux for Setup page validation + if (result.passwordValidationSchema) { + dispatch(setPasswordSchema(result.passwordValidationSchema)); + } } catch (error) { if (error.message === 'SERVER_UNAVAILABLE') { setServerUnavailable(true); @@ -32,7 +38,7 @@ export default function AuthWrapper({children}) { }; checkSetup(); - }, []); + }, [dispatch]); useEffect(() => { if (!checkingSetup && !setupRequired && !serverUnavailable) { diff --git a/AdminPanel/client/src/components/PasswordRequirements/PasswordRequirements.js b/AdminPanel/client/src/components/PasswordRequirements/PasswordRequirements.js index fd913535..4264c1ae 100644 --- a/AdminPanel/client/src/components/PasswordRequirements/PasswordRequirements.js +++ b/AdminPanel/client/src/components/PasswordRequirements/PasswordRequirements.js @@ -2,7 +2,7 @@ import SuccessGreenIcon from '../../assets/SuccessGreen.svg'; import FailRedIcon from '../../assets/FailRed.svg'; import styles from './PasswordRequirements.module.scss'; import {useSelector} from 'react-redux'; -import {selectSchema} from '../../store/slices/configSlice'; +import {selectSchema, selectPasswordSchema} from '../../store/slices/configSlice'; import {useMemo} from 'react'; import {usePasswordValidation} from '../../utils/passwordValidation'; @@ -13,12 +13,19 @@ import {usePasswordValidation} from '../../utils/passwordValidation'; * @param {boolean} isVisible - Whether to show the requirements (e.g., on focus) */ function PasswordRequirements({password, isVisible = false}) { - const schema = useSelector(selectSchema); + const fullSchema = useSelector(selectSchema); + const passwordSchema = useSelector(selectPasswordSchema); const {invalidRules, isValid} = usePasswordValidation(password); + // Use fullSchema (admin panel) or passwordSchema (Setup page) + const schema = fullSchema || passwordSchema; const passwordValidation = schema?.properties?.adminPanel?.properties?.passwordValidation; const requirements = useMemo(() => { + if (!passwordValidation?.properties) { + return []; + } + const rules = [ {key: 'minLength', format: 'passlength'}, {key: 'hasDigit', format: 'passdigit'}, @@ -41,10 +48,15 @@ function PasswordRequirements({password, isVisible = false}) { const validRequirements = requirements.filter(req => req.isValid).length; const totalRequirements = requirements.length; - const progress = (validRequirements / totalRequirements) * 100; + const progress = totalRequirements > 0 ? (validRequirements / totalRequirements) * 100 : 0; const shouldShow = isVisible || (!isValid && password); + // Don't show if schema is not loaded yet + if (!schema || !passwordValidation?.properties) { + return null; + } + if (!shouldShow) { return null; } diff --git a/AdminPanel/client/src/hooks/useFieldValidation.js b/AdminPanel/client/src/hooks/useFieldValidation.js index 9bbbbdc3..b88ac57c 100644 --- a/AdminPanel/client/src/hooks/useFieldValidation.js +++ b/AdminPanel/client/src/hooks/useFieldValidation.js @@ -3,22 +3,29 @@ import {useSelector} from 'react-redux'; import Ajv from 'ajv'; import addFormats from 'ajv-formats'; import addErrors from 'ajv-errors'; -import {selectSchema, selectSchemaLoading, selectSchemaError} from '../store/slices/configSlice'; +import {selectSchema, selectPasswordSchema, selectSchemaLoading, selectSchemaError} from '../store/slices/configSlice'; /** * Hook for field validation using backend schema + * Uses passwordSchema (minimal) for Setup page, or schema (full) for admin panel * @returns {Object} { validateField, getFieldError, isLoading, error } */ export const useFieldValidation = () => { const [validator, setValidator] = useState(null); const [fieldErrors, setFieldErrors] = useState({}); + const [cachedSchema, setCachedSchema] = useState(null); - const schema = useSelector(selectSchema); + const fullSchema = useSelector(selectSchema); + const passwordSchema = useSelector(selectPasswordSchema); const isLoading = useSelector(selectSchemaLoading); const error = useSelector(selectSchemaError); + // Prefer fullSchema (admin panel) over passwordSchema (Setup page) + const schema = fullSchema || passwordSchema; + useEffect(() => { - if (schema && !validator) { + // Only recreate validator if schema actually changed + if (schema && schema !== cachedSchema) { try { // Build AJV validator with custom and standard formats const ajv = new Ajv({allErrors: true, strict: false}); @@ -27,11 +34,12 @@ export const useFieldValidation = () => { const validateFn = ajv.compile(schema); setValidator(() => validateFn); + setCachedSchema(schema); } catch (err) { console.error('Failed to initialize field validator:', err); } } - }, [schema, validator]); + }, [schema, cachedSchema]); /** * Validates a single field value against the schema diff --git a/AdminPanel/client/src/hooks/useSchemaLoader.js b/AdminPanel/client/src/hooks/useSchemaLoader.js index 2b7e2940..cab42587 100644 --- a/AdminPanel/client/src/hooks/useSchemaLoader.js +++ b/AdminPanel/client/src/hooks/useSchemaLoader.js @@ -1,9 +1,10 @@ import {useEffect} from 'react'; import {useDispatch, useSelector} from 'react-redux'; import {selectSchema, selectSchemaLoading, selectSchemaError, fetchSchema} from '../store/slices/configSlice'; +import {selectIsAuthenticated} from '../store/slices/userSlice'; /** - * Hook to load schema on app startup + * Hook to load schema for authenticated users * Fetches schema immediately when the hook is first used */ export const useSchemaLoader = () => { @@ -11,13 +12,14 @@ export const useSchemaLoader = () => { const schema = useSelector(selectSchema); const schemaLoading = useSelector(selectSchemaLoading); const schemaError = useSelector(selectSchemaError); + const isAuthenticated = useSelector(selectIsAuthenticated); useEffect(() => { - // Fetch schema if not loaded (always fetch, no auth required) - if (!schema && !schemaLoading && !schemaError) { + // Load schema only for authenticated users + if (isAuthenticated && !schema && !schemaLoading && !schemaError) { dispatch(fetchSchema()); } - }, [schema, schemaLoading, schemaError, dispatch]); + }, [isAuthenticated, schema, schemaLoading, schemaError, dispatch]); return { schema, diff --git a/AdminPanel/client/src/store/slices/configSlice.js b/AdminPanel/client/src/store/slices/configSlice.js index ed193fc2..a2ac6b3f 100644 --- a/AdminPanel/client/src/store/slices/configSlice.js +++ b/AdminPanel/client/src/store/slices/configSlice.js @@ -39,7 +39,8 @@ export const rotateWopiKeysAction = createAsyncThunk('config/rotateWopiKeys', as const initialState = { config: null, - schema: null, + schema: null, // Full schema for admin panel + passwordSchema: null, // Minimal schema for Setup page password validation loading: false, schemaLoading: false, saving: false, @@ -64,6 +65,9 @@ const configSlice = createSlice({ }, clearError: state => { state.error = null; + }, + setPasswordSchema: (state, action) => { + state.passwordSchema = action.payload; } }, extraReducers: builder => { @@ -127,11 +131,12 @@ const configSlice = createSlice({ } }); -export const {updateLocalConfig, clearConfig, clearError} = configSlice.actions; +export const {updateLocalConfig, clearConfig, clearError, setPasswordSchema} = configSlice.actions; // Selectors export const selectConfig = state => state.config.config; 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 selectConfigSaving = state => state.config.saving; diff --git a/AdminPanel/server/sources/routes/adminpanel/router.js b/AdminPanel/server/sources/routes/adminpanel/router.js index e4aad050..6a888a73 100644 --- a/AdminPanel/server/sources/routes/adminpanel/router.js +++ b/AdminPanel/server/sources/routes/adminpanel/router.js @@ -9,6 +9,7 @@ const adminPanelJwtSecret = require('../../jwtSecret'); const tenantManager = require('../../../../../Common/sources/tenantManager'); const commonDefines = require('../../../../../Common/sources/commondefines'); const {validateScoped} = require('../config/config.service'); +const supersetSchema = require('../../../../../Common/config/schemas/config.schema.json'); const router = express.Router(); @@ -83,7 +84,8 @@ function requireAuth(req, res, next) { } /** - * Check if AdminPanel setup is required + * Check if initial setup is required and get password validation schema + * Returns setup status and minimal schema for password validation */ router.get('/setup/required', async (req, res) => { const ctx = new operationContext.Context(); @@ -100,7 +102,22 @@ router.get('/setup/required', async (req, res) => { } } - res.json({setupRequired}); + // Include minimal password validation schema for setup page + const passwordValidationSchema = { + $defs: supersetSchema.$defs, + properties: { + adminPanel: { + properties: { + passwordValidation: supersetSchema.properties.adminPanel.properties.passwordValidation + } + } + } + }; + + res.json({ + setupRequired, + passwordValidationSchema + }); } catch (error) { ctx.logger.error('Setup check error: %s', error.stack); res.status(500).json({error: 'Internal server error'}); diff --git a/AdminPanel/server/sources/routes/config/router.js b/AdminPanel/server/sources/routes/config/router.js index 3e83d80e..37c834f5 100644 --- a/AdminPanel/server/sources/routes/config/router.js +++ b/AdminPanel/server/sources/routes/config/router.js @@ -36,7 +36,7 @@ router.get('/', validateJWT, async (req, res) => { } }); -router.get('/schema', async (_req, res) => { +router.get('/schema', validateJWT, async (_req, res) => { res.json(supersetSchema); });