mirror of
https://github.com/ONLYOFFICE/server.git
synced 2026-02-10 18:05:07 +08:00
[feature] Revert validateJWT in schema request
This commit is contained in:
@ -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();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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'});
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user