[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 () => {
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();
};

View File

@ -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) {

View File

@ -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;
}

View File

@ -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

View File

@ -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,

View File

@ -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;

View File

@ -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'});

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);
});