[feature] Revert validateJWT in schema request

This commit is contained in:
Sergey Konovalov
2025-10-21 18:36:20 +03:00
parent 72c0036841
commit 028e119edd
8 changed files with 69 additions and 19 deletions

View File

@ -21,7 +21,7 @@ const safeFetch = async (url, options = {}) => {
}; };
export const fetchStatistics = async () => { 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'); if (!response.ok) throw new Error('Failed to fetch statistics');
return response.json(); return response.json();
}; };
@ -34,7 +34,7 @@ export const fetchConfiguration = async () => {
}; };
export const fetchConfigurationSchema = 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'); if (!response.ok) throw new Error('Failed to fetch configuration schema');
return response.json(); return response.json();
}; };

View File

@ -1,6 +1,7 @@
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
import {useDispatch, useSelector} from 'react-redux'; import {useDispatch, useSelector} from 'react-redux';
import {fetchUser, selectUser, selectUserLoading, selectIsAuthenticated} from '../../store/slices/userSlice'; import {fetchUser, selectUser, selectUserLoading, selectIsAuthenticated} from '../../store/slices/userSlice';
import {setPasswordSchema} from '../../store/slices/configSlice';
import {checkSetupRequired} from '../../api'; import {checkSetupRequired} from '../../api';
import Spinner from '../../assets/Spinner.svg'; import Spinner from '../../assets/Spinner.svg';
import Login from '../../pages/Login/LoginPage'; import Login from '../../pages/Login/LoginPage';
@ -22,6 +23,11 @@ export default function AuthWrapper({children}) {
try { try {
const result = await checkSetupRequired(); const result = await checkSetupRequired();
setSetupRequired(result.setupRequired); setSetupRequired(result.setupRequired);
// Save minimal password schema to Redux for Setup page validation
if (result.passwordValidationSchema) {
dispatch(setPasswordSchema(result.passwordValidationSchema));
}
} catch (error) { } catch (error) {
if (error.message === 'SERVER_UNAVAILABLE') { if (error.message === 'SERVER_UNAVAILABLE') {
setServerUnavailable(true); setServerUnavailable(true);
@ -32,7 +38,7 @@ export default function AuthWrapper({children}) {
}; };
checkSetup(); checkSetup();
}, []); }, [dispatch]);
useEffect(() => { useEffect(() => {
if (!checkingSetup && !setupRequired && !serverUnavailable) { if (!checkingSetup && !setupRequired && !serverUnavailable) {

View File

@ -2,7 +2,7 @@ import SuccessGreenIcon from '../../assets/SuccessGreen.svg';
import FailRedIcon from '../../assets/FailRed.svg'; import FailRedIcon from '../../assets/FailRed.svg';
import styles from './PasswordRequirements.module.scss'; import styles from './PasswordRequirements.module.scss';
import {useSelector} from 'react-redux'; import {useSelector} from 'react-redux';
import {selectSchema} from '../../store/slices/configSlice'; import {selectSchema, selectPasswordSchema} from '../../store/slices/configSlice';
import {useMemo} from 'react'; import {useMemo} from 'react';
import {usePasswordValidation} from '../../utils/passwordValidation'; 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) * @param {boolean} isVisible - Whether to show the requirements (e.g., on focus)
*/ */
function PasswordRequirements({password, isVisible = false}) { function PasswordRequirements({password, isVisible = false}) {
const schema = useSelector(selectSchema); const fullSchema = useSelector(selectSchema);
const passwordSchema = useSelector(selectPasswordSchema);
const {invalidRules, isValid} = usePasswordValidation(password); 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 passwordValidation = schema?.properties?.adminPanel?.properties?.passwordValidation;
const requirements = useMemo(() => { const requirements = useMemo(() => {
if (!passwordValidation?.properties) {
return [];
}
const rules = [ const rules = [
{key: 'minLength', format: 'passlength'}, {key: 'minLength', format: 'passlength'},
{key: 'hasDigit', format: 'passdigit'}, {key: 'hasDigit', format: 'passdigit'},
@ -41,10 +48,15 @@ function PasswordRequirements({password, isVisible = false}) {
const validRequirements = requirements.filter(req => req.isValid).length; const validRequirements = requirements.filter(req => req.isValid).length;
const totalRequirements = requirements.length; const totalRequirements = requirements.length;
const progress = (validRequirements / totalRequirements) * 100; const progress = totalRequirements > 0 ? (validRequirements / totalRequirements) * 100 : 0;
const shouldShow = isVisible || (!isValid && password); const shouldShow = isVisible || (!isValid && password);
// Don't show if schema is not loaded yet
if (!schema || !passwordValidation?.properties) {
return null;
}
if (!shouldShow) { if (!shouldShow) {
return null; return null;
} }

View File

@ -3,22 +3,29 @@ import {useSelector} from 'react-redux';
import Ajv from 'ajv'; import Ajv from 'ajv';
import addFormats from 'ajv-formats'; import addFormats from 'ajv-formats';
import addErrors from 'ajv-errors'; 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 * 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 } * @returns {Object} { validateField, getFieldError, isLoading, error }
*/ */
export const useFieldValidation = () => { export const useFieldValidation = () => {
const [validator, setValidator] = useState(null); const [validator, setValidator] = useState(null);
const [fieldErrors, setFieldErrors] = useState({}); 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 isLoading = useSelector(selectSchemaLoading);
const error = useSelector(selectSchemaError); const error = useSelector(selectSchemaError);
// Prefer fullSchema (admin panel) over passwordSchema (Setup page)
const schema = fullSchema || passwordSchema;
useEffect(() => { useEffect(() => {
if (schema && !validator) { // Only recreate validator if schema actually changed
if (schema && schema !== cachedSchema) {
try { try {
// Build AJV validator with custom and standard formats // Build AJV validator with custom and standard formats
const ajv = new Ajv({allErrors: true, strict: false}); const ajv = new Ajv({allErrors: true, strict: false});
@ -27,11 +34,12 @@ export const useFieldValidation = () => {
const validateFn = ajv.compile(schema); const validateFn = ajv.compile(schema);
setValidator(() => validateFn); setValidator(() => validateFn);
setCachedSchema(schema);
} catch (err) { } catch (err) {
console.error('Failed to initialize field validator:', err); console.error('Failed to initialize field validator:', err);
} }
} }
}, [schema, validator]); }, [schema, cachedSchema]);
/** /**
* Validates a single field value against the schema * Validates a single field value against the schema

View File

@ -1,9 +1,10 @@
import {useEffect} from 'react'; import {useEffect} from 'react';
import {useDispatch, useSelector} from 'react-redux'; import {useDispatch, useSelector} from 'react-redux';
import {selectSchema, selectSchemaLoading, selectSchemaError, fetchSchema} from '../store/slices/configSlice'; 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 * Fetches schema immediately when the hook is first used
*/ */
export const useSchemaLoader = () => { export const useSchemaLoader = () => {
@ -11,13 +12,14 @@ export const useSchemaLoader = () => {
const schema = useSelector(selectSchema); const schema = useSelector(selectSchema);
const schemaLoading = useSelector(selectSchemaLoading); const schemaLoading = useSelector(selectSchemaLoading);
const schemaError = useSelector(selectSchemaError); const schemaError = useSelector(selectSchemaError);
const isAuthenticated = useSelector(selectIsAuthenticated);
useEffect(() => { useEffect(() => {
// Fetch schema if not loaded (always fetch, no auth required) // Load schema only for authenticated users
if (!schema && !schemaLoading && !schemaError) { if (isAuthenticated && !schema && !schemaLoading && !schemaError) {
dispatch(fetchSchema()); dispatch(fetchSchema());
} }
}, [schema, schemaLoading, schemaError, dispatch]); }, [isAuthenticated, schema, schemaLoading, schemaError, dispatch]);
return { return {
schema, schema,

View File

@ -39,7 +39,8 @@ export const rotateWopiKeysAction = createAsyncThunk('config/rotateWopiKeys', as
const initialState = { const initialState = {
config: null, config: null,
schema: null, schema: null, // Full schema for admin panel
passwordSchema: null, // Minimal schema for Setup page password validation
loading: false, loading: false,
schemaLoading: false, schemaLoading: false,
saving: false, saving: false,
@ -64,6 +65,9 @@ const configSlice = createSlice({
}, },
clearError: state => { clearError: state => {
state.error = null; state.error = null;
},
setPasswordSchema: (state, action) => {
state.passwordSchema = action.payload;
} }
}, },
extraReducers: builder => { 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 // Selectors
export const selectConfig = state => state.config.config; export const selectConfig = state => state.config.config;
export const selectSchema = state => state.config.schema; export const selectSchema = state => state.config.schema;
export const selectPasswordSchema = state => state.config.passwordSchema;
export const selectConfigLoading = state => state.config.loading; export const selectConfigLoading = state => state.config.loading;
export const selectSchemaLoading = state => state.config.schemaLoading; export const selectSchemaLoading = state => state.config.schemaLoading;
export const selectConfigSaving = state => state.config.saving; export const selectConfigSaving = state => state.config.saving;

View File

@ -9,6 +9,7 @@ const adminPanelJwtSecret = require('../../jwtSecret');
const tenantManager = require('../../../../../Common/sources/tenantManager'); const tenantManager = require('../../../../../Common/sources/tenantManager');
const commonDefines = require('../../../../../Common/sources/commondefines'); const commonDefines = require('../../../../../Common/sources/commondefines');
const {validateScoped} = require('../config/config.service'); const {validateScoped} = require('../config/config.service');
const supersetSchema = require('../../../../../Common/config/schemas/config.schema.json');
const router = express.Router(); 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) => { router.get('/setup/required', async (req, res) => {
const ctx = new operationContext.Context(); 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) { } catch (error) {
ctx.logger.error('Setup check error: %s', error.stack); ctx.logger.error('Setup check error: %s', error.stack);
res.status(500).json({error: 'Internal server error'}); res.status(500).json({error: 'Internal server error'});

View File

@ -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); res.json(supersetSchema);
}); });