diff --git a/AdminPanel/client/src/App.js b/AdminPanel/client/src/App.js
index 5524b64b..ecf953a0 100644
--- a/AdminPanel/client/src/App.js
+++ b/AdminPanel/client/src/App.js
@@ -4,9 +4,35 @@ import './App.css';
import {store} from './store';
import AuthWrapper from './components/AuthWrapper/AuthWrapper';
import ConfigLoader from './components/ConfigLoader/ConfigLoader';
+import {useSchemaLoader} from './hooks/useSchemaLoader';
import Menu from './components/Menu/Menu';
import {menuItems} from './config/menuItems';
+function AppContent() {
+ useSchemaLoader();
+
+ return (
+
+
+
+
+
+
+
+ } />
+ } />
+ {menuItems.map(item => (
+ } />
+ ))}
+
+
+
+
+
+
+ );
+}
+
/**
* Simple basename computation from URL path.
* Basename is everything before the last path segment.
@@ -35,24 +61,7 @@ function App() {
return (
-
-
-
-
-
-
-
- } />
- } />
- {menuItems.map(item => (
- } />
- ))}
-
-
-
-
-
-
+
);
diff --git a/AdminPanel/client/src/api/index.js b/AdminPanel/client/src/api/index.js
index 8b0e08a5..a9594bcc 100644
--- a/AdminPanel/client/src/api/index.js
+++ b/AdminPanel/client/src/api/index.js
@@ -34,8 +34,7 @@ export const fetchConfiguration = async () => {
};
export const fetchConfigurationSchema = async () => {
- const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/config/schema`, {credentials: 'include'});
- if (response.status === 401) throw new Error('UNAUTHORIZED');
+ const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/config/schema`);
if (!response.ok) throw new Error('Failed to fetch configuration schema');
return response.json();
};
diff --git a/AdminPanel/client/src/components/ConfigLoader/ConfigLoader.js b/AdminPanel/client/src/components/ConfigLoader/ConfigLoader.js
index 37b05d63..8c1abb7e 100644
--- a/AdminPanel/client/src/components/ConfigLoader/ConfigLoader.js
+++ b/AdminPanel/client/src/components/ConfigLoader/ConfigLoader.js
@@ -1,15 +1,7 @@
import {useEffect} from 'react';
import {useSelector, useDispatch} from 'react-redux';
-import {
- selectConfig,
- selectConfigLoading,
- selectConfigError,
- selectSchema,
- selectSchemaLoading,
- selectSchemaError,
- fetchConfig,
- fetchSchema
-} from '../../store/slices/configSlice';
+import {selectConfig, selectConfigLoading, selectConfigError, selectSchema, fetchConfig} from '../../store/slices/configSlice';
+import {selectIsAuthenticated} from '../../store/slices/userSlice';
import Button from '../Button/Button';
const ConfigLoader = ({children}) => {
@@ -18,23 +10,16 @@ const ConfigLoader = ({children}) => {
const configLoading = useSelector(selectConfigLoading);
const configError = useSelector(selectConfigError);
const schema = useSelector(selectSchema);
- const schemaLoading = useSelector(selectSchemaLoading);
- const schemaError = useSelector(selectSchemaError);
+ const isAuthenticated = useSelector(selectIsAuthenticated);
- const loading = configLoading || schemaLoading;
- const error = configError || schemaError;
+ const loading = configLoading;
+ const error = configError;
useEffect(() => {
- // Fetch config if not loaded
- if (!config && !configLoading && !configError) {
+ if (isAuthenticated && !config && !configLoading && !configError) {
dispatch(fetchConfig());
}
-
- // Fetch schema if not loaded (only once per session)
- if (!schema && !schemaLoading && !schemaError) {
- dispatch(fetchSchema());
- }
- }, [config, configLoading, configError, schema, schemaLoading, schemaError, dispatch]);
+ }, [config, configLoading, configError, isAuthenticated, dispatch]);
if (loading) {
return (
diff --git a/AdminPanel/client/src/components/PasswordInputWithRequirements/PasswordInputWithRequirements.js b/AdminPanel/client/src/components/PasswordInputWithRequirements/PasswordInputWithRequirements.js
index 8b4bc2ae..e803a3d1 100644
--- a/AdminPanel/client/src/components/PasswordInputWithRequirements/PasswordInputWithRequirements.js
+++ b/AdminPanel/client/src/components/PasswordInputWithRequirements/PasswordInputWithRequirements.js
@@ -1,31 +1,22 @@
-import React, {useState} from 'react';
+import {useState} from 'react';
import Input from '../Input/Input';
import PasswordRequirements from '../PasswordRequirements/PasswordRequirements';
-import {validatePasswordStrength} from '../../utils/passwordValidation';
+import {usePasswordValidation} from '../../utils/passwordValidation';
/**
* Password input component with requirements display on focus
* Matches DocSpace PasswordInput standard behavior
*/
-function PasswordInputWithRequirements({
- label,
- value,
- onChange,
- placeholder,
- description,
- error,
- settings,
- ...props
-}) {
+function PasswordInputWithRequirements({label, value, onChange, placeholder, description, error, ...props}) {
const [isFocused, setIsFocused] = useState(false);
+ const {isValid} = usePasswordValidation(value);
const handleFocus = () => {
setIsFocused(true);
};
const handleBlur = () => {
- const passwordValidation = validatePasswordStrength(value || '');
- if (passwordValidation.isValid || !value) {
+ if (isValid || !value) {
setIsFocused(false);
}
};
@@ -35,7 +26,7 @@ function PasswordInputWithRequirements({
);
diff --git a/AdminPanel/client/src/components/PasswordRequirements/PasswordRequirements.js b/AdminPanel/client/src/components/PasswordRequirements/PasswordRequirements.js
index 222170fe..fd913535 100644
--- a/AdminPanel/client/src/components/PasswordRequirements/PasswordRequirements.js
+++ b/AdminPanel/client/src/components/PasswordRequirements/PasswordRequirements.js
@@ -1,19 +1,47 @@
-import React, {useState} from 'react';
-import {getPasswordRequirementsStatus} from '../../utils/passwordValidation';
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 {useMemo} from 'react';
+import {usePasswordValidation} from '../../utils/passwordValidation';
/**
* Component that displays password requirements with progress bar
* Matches DocSpace PasswordInput standard
* @param {string} password - Password to validate
* @param {boolean} isVisible - Whether to show the requirements (e.g., on focus)
- * @param {Object} settings - Password settings (optional)
*/
-function PasswordRequirements({password, isVisible = false, settings}) {
- const requirementsStatus = getPasswordRequirementsStatus(password || '', settings);
- const {requirements, progress, isValid} = requirementsStatus;
+function PasswordRequirements({password, isVisible = false}) {
+ const schema = useSelector(selectSchema);
+ const {invalidRules, isValid} = usePasswordValidation(password);
+
+ const passwordValidation = schema?.properties?.adminPanel?.properties?.passwordValidation;
+
+ const requirements = useMemo(() => {
+ const rules = [
+ {key: 'minLength', format: 'passlength'},
+ {key: 'hasDigit', format: 'passdigit'},
+ {key: 'hasUppercase', format: 'passupper'},
+ {key: 'hasSpecialChar', format: 'passspecial'}
+ ];
+
+ const invalidRulesSet = new Set(invalidRules);
+
+ return rules.map(rule => {
+ const property = passwordValidation.properties[rule.key];
+ const isValid = !invalidRulesSet.has(rule.key);
+
+ return {
+ text: property?.description,
+ isValid
+ };
+ });
+ }, [passwordValidation, invalidRules]);
+
+ const validRequirements = requirements.filter(req => req.isValid).length;
+ const totalRequirements = requirements.length;
+ const progress = (validRequirements / totalRequirements) * 100;
const shouldShow = isVisible || (!isValid && password);
@@ -24,25 +52,15 @@ function PasswordRequirements({password, isVisible = false, settings}) {
return (
-
-
Password must contain:
+
+
Password must:
{requirements.map((requirement, index) => (
- -
+
-
- {requirement.isValid ? (
-
- ) : (
-
- )}
+ {requirement.isValid ?
:
}
{requirement.text}
@@ -53,4 +71,3 @@ function PasswordRequirements({password, isVisible = false, settings}) {
}
export default PasswordRequirements;
-
diff --git a/AdminPanel/client/src/components/PasswordRequirements/PasswordRequirements.module.scss b/AdminPanel/client/src/components/PasswordRequirements/PasswordRequirements.module.scss
index 5d85b705..7a50a9ad 100644
--- a/AdminPanel/client/src/components/PasswordRequirements/PasswordRequirements.module.scss
+++ b/AdminPanel/client/src/components/PasswordRequirements/PasswordRequirements.module.scss
@@ -36,7 +36,7 @@
}
.requirementsTitle {
- font-size: 11px;
+ font-size: 10px;
font-weight: 500;
color: #666;
margin-bottom: 6px;
@@ -54,7 +54,7 @@
display: flex;
align-items: center;
margin-bottom: 4px;
- font-size: 12px;
+ font-size: 10px;
line-height: 1.4;
}
@@ -63,9 +63,9 @@
}
.bullet {
- margin-right: 8px;
- width: 14px;
- height: 14px;
+ margin-right: 6px;
+ width: 12px;
+ height: 12px;
display: flex;
align-items: center;
justify-content: center;
diff --git a/AdminPanel/client/src/hooks/useFieldValidation.js b/AdminPanel/client/src/hooks/useFieldValidation.js
index 43be1765..abc70c87 100644
--- a/AdminPanel/client/src/hooks/useFieldValidation.js
+++ b/AdminPanel/client/src/hooks/useFieldValidation.js
@@ -4,9 +4,6 @@ import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import {selectSchema, selectSchemaLoading, selectSchemaError} from '../store/slices/configSlice';
-// Cron expression with 6 space-separated fields (server-compatible)
-const CRON6_REGEX = /^\s*\S+(?:\s+\S+){5}\s*$/;
-
/**
* Hook for field validation using backend schema
* @returns {Object} { validateField, getFieldError, isLoading, error }
@@ -25,7 +22,19 @@ export const useFieldValidation = () => {
// Build AJV validator with custom and standard formats
const ajv = new Ajv({allErrors: true, strict: false});
addFormats(ajv); // Add standard formats including email
- ajv.addFormat('cron6', CRON6_REGEX); // Add custom cron6 format
+
+ // Register formats from schema $defs.formats (regex strings)
+ const formats = schema?.$defs?.formats;
+ if (formats && typeof formats === 'object') {
+ for (const [name, patternString] of Object.entries(formats)) {
+ try {
+ const re = new RegExp(patternString);
+ ajv.addFormat(name, re);
+ } catch (e) {
+ console.warn('Invalid format regex in schema $defs.formats:', name, e.message);
+ }
+ }
+ }
const validateFn = ajv.compile(schema);
setValidator(() => validateFn);
diff --git a/AdminPanel/client/src/hooks/useSchemaLoader.js b/AdminPanel/client/src/hooks/useSchemaLoader.js
new file mode 100644
index 00000000..2b7e2940
--- /dev/null
+++ b/AdminPanel/client/src/hooks/useSchemaLoader.js
@@ -0,0 +1,27 @@
+import {useEffect} from 'react';
+import {useDispatch, useSelector} from 'react-redux';
+import {selectSchema, selectSchemaLoading, selectSchemaError, fetchSchema} from '../store/slices/configSlice';
+
+/**
+ * Hook to load schema on app startup
+ * Fetches schema 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);
+
+ useEffect(() => {
+ // Fetch schema if not loaded (always fetch, no auth required)
+ if (!schema && !schemaLoading && !schemaError) {
+ dispatch(fetchSchema());
+ }
+ }, [schema, schemaLoading, schemaError, dispatch]);
+
+ return {
+ schema,
+ schemaLoading,
+ schemaError
+ };
+};
diff --git a/AdminPanel/client/src/pages/ChangePassword/ChangePassword.js b/AdminPanel/client/src/pages/ChangePassword/ChangePassword.js
index 80d4101f..1e0f282b 100644
--- a/AdminPanel/client/src/pages/ChangePassword/ChangePassword.js
+++ b/AdminPanel/client/src/pages/ChangePassword/ChangePassword.js
@@ -5,7 +5,7 @@ import PageDescription from '../../components/PageDescription/PageDescription';
import Input from '../../components/Input/Input';
import FixedSaveButton from '../../components/FixedSaveButton/FixedSaveButton';
import PasswordInputWithRequirements from '../../components/PasswordInputWithRequirements/PasswordInputWithRequirements';
-import {validatePasswordStrength} from '../../utils/passwordValidation';
+import {usePasswordValidation} from '../../utils/passwordValidation';
import styles from './ChangePassword.module.scss';
function ChangePassword() {
@@ -15,21 +15,22 @@ function ChangePassword() {
const [passwordError, setPasswordError] = useState('');
const [passwordSuccess, setPasswordSuccess] = useState(false);
+ const {isValid: newPasswordIsValid, isLoading} = usePasswordValidation(newPassword);
+
// Check if form can be submitted
const canSubmit = () => {
- if (!currentPassword || !newPassword || !confirmPassword) {
+ if (!currentPassword || !newPassword || !confirmPassword || isLoading) {
return false;
}
-
- const passwordValidation = validatePasswordStrength(newPassword);
- if (!passwordValidation.isValid) {
+
+ if (!newPasswordIsValid) {
return false;
}
-
+
if (newPassword !== confirmPassword) {
return false;
}
-
+
return true;
};
@@ -37,28 +38,6 @@ function ChangePassword() {
setPasswordError('');
setPasswordSuccess(false);
- // Validation
- if (!currentPassword) {
- setPasswordError('Current password is required');
- throw new Error('Validation failed');
- }
-
- if (!newPassword) {
- setPasswordError('New password is required');
- throw new Error('Validation failed');
- }
-
- const passwordValidation = validatePasswordStrength(newPassword);
- if (!passwordValidation.isValid) {
- setPasswordError(passwordValidation.errorMessage);
- throw new Error('Validation failed');
- }
-
- if (newPassword !== confirmPassword) {
- setPasswordError('New passwords do not match');
- throw new Error('Validation failed');
- }
-
try {
await changePassword({currentPassword, newPassword});
setPasswordSuccess(true);
@@ -111,9 +90,7 @@ function ChangePassword() {
description='Re-enter your new password'
/>
- {newPassword && confirmPassword && newPassword !== confirmPassword && validatePasswordStrength(newPassword).isValid && (
- 'Passwords don\'t match'
- )}
+ {newPassword && confirmPassword && newPassword !== confirmPassword && newPasswordIsValid && "Passwords don't match"}
diff --git a/AdminPanel/client/src/pages/Setup/SetupPage.js b/AdminPanel/client/src/pages/Setup/SetupPage.js
index 219bcb3b..4a8d8170 100644
--- a/AdminPanel/client/src/pages/Setup/SetupPage.js
+++ b/AdminPanel/client/src/pages/Setup/SetupPage.js
@@ -5,7 +5,7 @@ import {fetchUser} from '../../store/slices/userSlice';
import Input from '../../components/Input/Input';
import Button from '../../components/Button/Button';
import PasswordInputWithRequirements from '../../components/PasswordInputWithRequirements/PasswordInputWithRequirements';
-import {validatePasswordStrength} from '../../utils/passwordValidation';
+import {usePasswordValidation} from '../../utils/passwordValidation';
import styles from './styles.module.css';
export default function Setup() {
@@ -16,55 +16,28 @@ export default function Setup() {
const dispatch = useDispatch();
const buttonRef = useRef();
+ const {isValid: passwordIsValid, isLoading} = usePasswordValidation(password);
+
// Check if form can be submitted
const canSubmit = () => {
- if (!bootstrapToken.trim() || !password || !confirmPassword) {
+ if (!bootstrapToken.trim() || !password || !confirmPassword || isLoading) {
return false;
}
-
- const passwordValidation = validatePasswordStrength(password);
- if (!passwordValidation.isValid) {
+
+ // Check if password meets all requirements using hook
+ if (!passwordIsValid) {
return false;
}
-
+
if (password !== confirmPassword) {
return false;
}
-
+
return true;
};
const handleSubmit = async () => {
setErrors({});
-
- // Validate all fields
- const newErrors = {};
-
- if (!bootstrapToken.trim()) {
- newErrors.bootstrapToken = 'Bootstrap token is required';
- }
-
- if (!password) {
- newErrors.password = 'Password is required';
- } else {
- const passwordValidation = validatePasswordStrength(password);
- if (!passwordValidation.isValid) {
- newErrors.password = passwordValidation.errorMessage;
- }
- }
-
- if (!confirmPassword) {
- newErrors.confirmPassword = 'Please confirm your password';
- } else if (password !== confirmPassword) {
- newErrors.confirmPassword = 'Passwords do not match';
- }
-
- // If there are validation errors, show them
- if (Object.keys(newErrors).length > 0) {
- setErrors(newErrors);
- throw new Error('Validation failed');
- }
-
try {
await setupAdminPassword({bootstrapToken, password});
// Wait for cookie to be set and verify authentication works
@@ -129,9 +102,7 @@ export default function Setup() {
onKeyDown={handleKeyDown}
/>
- {password && confirmPassword && password !== confirmPassword && validatePasswordStrength(password).isValid && (
- 'Passwords don\'t match'
- )}
+ {password && confirmPassword && password !== confirmPassword && passwordIsValid && "Passwords don't match"}
diff --git a/AdminPanel/client/src/utils/passwordValidation.js b/AdminPanel/client/src/utils/passwordValidation.js
index 7aa9f847..138596ab 100644
--- a/AdminPanel/client/src/utils/passwordValidation.js
+++ b/AdminPanel/client/src/utils/passwordValidation.js
@@ -1,113 +1,31 @@
-/**
- * Password validation utility
- * Validates password strength according to security requirements
- */
-
-// Default password settings matching DocSpace standard
-const DEFAULT_PASSWORD_SETTINGS = {
- minLength: 8,
- maxLength: 128,
- upperCase: true,
- digits: true,
- specSymbols: true
-};
+import {useFieldValidation} from '../hooks/useFieldValidation';
+import {useMemo} from 'react';
/**
- * Validates password strength
+ * Hook for password validation
* @param {string} password - Password to validate
- * @param {Object} settings - Password settings (optional)
- * @returns {Object} Validation result with isValid boolean and error message
+ * @returns {Object} { isValid, errorMessage, invalidRules, isLoading, error }
*/
-export function validatePasswordStrength(password, settings = DEFAULT_PASSWORD_SETTINGS) {
- const errors = [];
+export function usePasswordValidation(password) {
+ const {validateField, isLoading} = useFieldValidation();
- // Check minimum length
- if (password.length < settings.minLength) {
- errors.push(`Password must be at least ${settings.minLength} characters long`);
- }
+ const validationResult = useMemo(() => {
+ const rules = ['minLength', 'hasDigit', 'hasUppercase', 'hasSpecialChar'];
- // Check maximum length
- if (password.length > settings.maxLength) {
- errors.push(`Password must not exceed ${settings.maxLength} characters`);
- }
+ const invalidRules = rules.filter(rule => {
+ const fieldPath = `adminPanel.passwordValidation.${rule}`;
+ const error = validateField(fieldPath, password || '');
+ return !!error;
+ });
- // Check for at least one digit
- if (settings.digits && !/\d/.test(password)) {
- errors.push('Password must contain at least one digit');
- }
-
- // Check for at least one uppercase letter
- if (settings.upperCase && !/[A-Z]/.test(password)) {
- errors.push('Password must contain at least one uppercase letter');
- }
-
- // Check for at least one special character
- if (settings.specSymbols && !/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) {
- errors.push('Password must contain at least one special character (!@#$%^&*()_+-=[]{}|;:,.<>?)');
- }
+ return {
+ isValid: invalidRules.length === 0,
+ invalidRules
+ };
+ }, [validateField, password]);
return {
- isValid: errors.length === 0,
- errors: errors,
- errorMessage: errors.length > 0 ? errors.join('. ') : null
+ ...validationResult,
+ isLoading
};
}
-
-/**
- * Gets detailed password requirements with validation status
- * @param {string} password - Password to validate
- * @param {Object} settings - Password settings (optional)
- * @returns {Object} Requirements status with progress and requirements array
- */
-export function getPasswordRequirementsStatus(password, settings = DEFAULT_PASSWORD_SETTINGS) {
- const requirements = [];
-
- // Length requirement
- requirements.push({
- text: `from ${settings.minLength} to ${settings.maxLength} characters`,
- isValid: password.length >= settings.minLength && password.length <= settings.maxLength
- });
-
- // Digits requirement
- if (settings.digits) {
- requirements.push({
- text: 'digits',
- isValid: /\d/.test(password)
- });
- }
-
- // Uppercase requirement
- if (settings.upperCase) {
- requirements.push({
- text: 'capital letters',
- isValid: /[A-Z]/.test(password)
- });
- }
-
- // Special symbols requirement
- if (settings.specSymbols) {
- requirements.push({
- text: 'special characters (!@#$%^&*)',
- isValid: /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)
- });
- }
-
- // Calculate progress (0-100)
- const validRequirements = requirements.filter(req => req.isValid).length;
- const totalRequirements = requirements.length;
- const progress = totalRequirements > 0 ? (validRequirements / totalRequirements) * 100 : 0;
-
- return {
- requirements,
- progress,
- isValid: progress === 100
- };
-}
-
-/**
- * Gets password strength requirements as a readable string
- * @returns {string} Requirements description
- */
-export function getPasswordRequirements() {
- return 'Password must be 8-128 characters long and contain at least one uppercase letter, one lowercase letter, one digit, and one special character';
-}
diff --git a/AdminPanel/server/sources/routes/adminpanel/router.js b/AdminPanel/server/sources/routes/adminpanel/router.js
index 1e3954fe..53a6b19e 100644
--- a/AdminPanel/server/sources/routes/adminpanel/router.js
+++ b/AdminPanel/server/sources/routes/adminpanel/router.js
@@ -8,12 +8,44 @@ const bootstrap = require('../../bootstrap');
const adminPanelJwtSecret = require('../../jwtSecret');
const tenantManager = require('../../../../../Common/sources/tenantManager');
const commonDefines = require('../../../../../Common/sources/commondefines');
+const {validateScoped} = require('../config/config.service');
const router = express.Router();
router.use(express.json());
router.use(cookieParser());
+/**
+ * Validates password against all requirements using existing config service
+ * @param {operationContext} ctx - Operation context
+ * @param {string} password - Password to validate
+ * @returns {Object} Validation result with isValid boolean and error messages
+ */
+function validatePassword(ctx, password) {
+ const testData = {
+ adminPanel: {
+ passwordValidation: {
+ minLength: password,
+ hasDigit: password,
+ hasUppercase: password,
+ hasSpecialChar: password
+ }
+ }
+ };
+
+ const result = validateScoped(ctx, testData);
+
+ if (result.value) {
+ return {
+ isValid: true
+ };
+ }
+
+ return {
+ isValid: false
+ };
+}
+
/**
* Create session cookie with standard options
* @param {import('express').Response} res - Express response
@@ -106,12 +138,9 @@ router.post('/setup', async (req, res) => {
return res.status(400).json({error: 'Password is required'});
}
- if (password.length < passwordManager.PASSWORD_MIN_LENGTH) {
- return res.status(400).json({error: `Password must be at least ${passwordManager.PASSWORD_MIN_LENGTH} characters long`});
- }
-
- if (password.length > passwordManager.PASSWORD_MAX_LENGTH) {
- return res.status(400).json({error: `Password must not exceed ${passwordManager.PASSWORD_MAX_LENGTH} characters`});
+ const passwordValidationResult = validatePassword(ctx, password);
+ if (!passwordValidationResult.isValid) {
+ return res.status(400).json({error: 'Password is too weak'});
}
await passwordManager.saveAdminPassword(ctx, password);
@@ -143,12 +172,9 @@ router.post('/change-password', requireAuth, async (req, res) => {
return res.status(400).json({error: 'Current password and new password are required'});
}
- if (newPassword.length < passwordManager.PASSWORD_MIN_LENGTH) {
- return res.status(400).json({error: `Password must be at least ${passwordManager.PASSWORD_MIN_LENGTH} characters long`});
- }
-
- if (newPassword.length > passwordManager.PASSWORD_MAX_LENGTH) {
- return res.status(400).json({error: `Password must not exceed ${passwordManager.PASSWORD_MAX_LENGTH} characters`});
+ const passwordValidationResult = validatePassword(ctx, newPassword);
+ if (!passwordValidationResult.isValid) {
+ return res.status(400).json({error: 'Password is too weak'});
}
if (currentPassword === newPassword) {
diff --git a/AdminPanel/server/sources/routes/config/config.service.js b/AdminPanel/server/sources/routes/config/config.service.js
index a5fbd3c9..1f15531a 100644
--- a/AdminPanel/server/sources/routes/config/config.service.js
+++ b/AdminPanel/server/sources/routes/config/config.service.js
@@ -39,7 +39,6 @@ const supersetSchema = require('../../../../../Common/config/schemas/config.sche
const {deriveSchemaForScope, X_SCOPE_KEYWORD} = require('./config.schema.utils');
// Constants
-const CRON6_REGEX = /^\s*\S+(?:\s+\S+){5}\s*$/;
const AJV_CONFIG = {allErrors: true, strict: false};
const AJV_FILTER_CONFIG = {allErrors: true, strict: false, removeAdditional: true};
@@ -49,7 +48,14 @@ const AJV_FILTER_CONFIG = {allErrors: true, strict: false, removeAdditional: tru
*/
function registerAjvExtras(instance) {
instance.addKeyword({keyword: X_SCOPE_KEYWORD, schemaType: ['string', 'array'], errors: false});
- instance.addFormat('cron6', CRON6_REGEX);
+
+ const formats = supersetSchema?.$defs?.formats;
+ if (formats && typeof formats === 'object') {
+ for (const [name, patternString] of Object.entries(formats)) {
+ const re = new RegExp(patternString);
+ instance.addFormat(name, re);
+ }
+ }
}
/**
@@ -114,13 +120,4 @@ function getScopedConfig(ctx) {
return configCopy;
}
-/**
- * Returns the derived per-scope schema for ctx (admin or tenant).
- * @param {operationContext} ctx
- * @returns {object}
- */
-function getScopedSchema(ctx) {
- return isAdminScope(ctx) ? adminSchema : tenantSchema;
-}
-
-module.exports = {validateScoped, getScopedConfig, getScopedSchema};
+module.exports = {validateScoped, getScopedConfig};
diff --git a/AdminPanel/server/sources/routes/config/router.js b/AdminPanel/server/sources/routes/config/router.js
index 22a3b9e7..3e83d80e 100644
--- a/AdminPanel/server/sources/routes/config/router.js
+++ b/AdminPanel/server/sources/routes/config/router.js
@@ -4,10 +4,11 @@ const express = require('express');
const bodyParser = require('body-parser');
const tenantManager = require('../../../../../Common/sources/tenantManager');
const runtimeConfigManager = require('../../../../../Common/sources/runtimeConfigManager');
-const {getScopedConfig, validateScoped, getScopedSchema} = require('./config.service');
+const {getScopedConfig, validateScoped} = require('./config.service');
const {validateJWT} = require('../../middleware/auth');
const cookieParser = require('cookie-parser');
const utils = require('../../../../../Common/sources/utils');
+const supersetSchema = require('../../../../../Common/config/schemas/config.schema.json');
const router = express.Router();
router.use(cookieParser());
@@ -35,18 +36,8 @@ router.get('/', validateJWT, async (req, res) => {
}
});
-router.get('/schema', validateJWT, async (req, res) => {
- const ctx = req.ctx;
- try {
- ctx.logger.info('config schema start');
- const schema = getScopedSchema(ctx);
- res.json(schema);
- } catch (error) {
- ctx.logger.error('Config schema error: %s', error.stack);
- res.status(500).json({error: 'Internal server error'});
- } finally {
- ctx.logger.info('config schema end');
- }
+router.get('/schema', async (_req, res) => {
+ res.json(supersetSchema);
});
router.patch('/', validateJWT, rawFileParser, async (req, res) => {
diff --git a/Common/config/schemas/config.schema.json b/Common/config/schemas/config.schema.json
index a132231c..37550d6d 100644
--- a/Common/config/schemas/config.schema.json
+++ b/Common/config/schemas/config.schema.json
@@ -5,6 +5,15 @@
"description": "Superset schema with x-scope markers. Use at runtime to derive per-scope schemas.",
"type": "object",
"additionalProperties": false,
+ "$defs": {
+ "formats": {
+ "cron6": "^\\s*\\S+(?:\\s+\\S+){5}\\s*$",
+ "passlength": "^.{8,128}$",
+ "passdigit": ".*\\d.*",
+ "passupper": ".*[A-Z].*",
+ "passspecial": ".*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?].*"
+ }
+ },
"properties": {
"aiSettings": {
"type": "object",
@@ -297,103 +306,141 @@
}
}
},
- "notification": {
- "type": "object",
- "additionalProperties": false,
- "x-scope": ["admin", "tenant"],
- "properties": {
- "rules": {
- "type": "object",
- "additionalProperties": false,
- "x-scope": ["admin", "tenant"],
- "properties": {
- "licenseExpirationWarning": {
- "type": "object",
- "additionalProperties": false,
- "x-scope": ["admin", "tenant"],
- "properties": {
- "enable": {"type": "boolean", "x-scope": ["admin", "tenant"]},
- "policies": {
- "type": "object",
- "additionalProperties": false,
- "x-scope": ["admin", "tenant"],
- "properties": {
- "repeatInterval": {
- "type": "string",
- "pattern": "^(\\d+[smhd]|\\d+\\s*(second|minute|hour|day)s?)$",
- "description": "Repeat interval in time format (e.g., '1d', '1h', '30m')",
- "x-scope": ["admin", "tenant"]
- }
- }
- }
- }
- },
- "licenseExpirationError": {
- "type": "object",
- "additionalProperties": false,
- "x-scope": ["admin", "tenant"],
- "properties": {
- "enable": {"type": "boolean", "x-scope": ["admin", "tenant"]},
- "policies": {
- "type": "object",
- "additionalProperties": false,
- "x-scope": ["admin", "tenant"],
- "properties": {
- "repeatInterval": {
- "type": "string",
- "pattern": "^(\\d+[smhd]|\\d+\\s*(second|minute|hour|day)s?)$",
- "description": "Repeat interval in time format (e.g., '1d', '1h', '30m')",
- "x-scope": ["admin", "tenant"]
- }
- }
- }
- }
- },
- "licenseLimitEdit": {
- "type": "object",
- "additionalProperties": false,
- "x-scope": ["admin", "tenant"],
- "properties": {
- "enable": {"type": "boolean", "x-scope": ["admin", "tenant"]},
- "policies": {
- "type": "object",
- "additionalProperties": false,
- "x-scope": ["admin", "tenant"],
- "properties": {
- "repeatInterval": {
- "type": "string",
- "pattern": "^(\\d+[smhd]|\\d+\\s*(second|minute|hour|day)s?)$",
- "description": "Repeat interval in time format (e.g., '1d', '1h', '30m')",
- "x-scope": ["admin", "tenant"]
- }
- }
- }
- }
- },
- "licenseLimitLiveViewer": {
- "type": "object",
- "additionalProperties": false,
- "x-scope": ["admin", "tenant"],
- "properties": {
- "enable": {"type": "boolean", "x-scope": ["admin", "tenant"]},
- "policies": {
- "type": "object",
- "additionalProperties": false,
- "x-scope": ["admin", "tenant"],
- "properties": {
- "repeatInterval": {
- "type": "string",
- "pattern": "^(\\d+[smhd]|\\d+\\s*(second|minute|hour|day)s?)$",
- "description": "Repeat interval in time format (e.g., '1d', '1h', '30m')",
- "x-scope": ["admin", "tenant"]
- }
- }
- }
- }
- }
- }
- }
- }
- }
+ "notification": {
+ "type": "object",
+ "additionalProperties": false,
+ "x-scope": ["admin", "tenant"],
+ "properties": {
+ "rules": {
+ "type": "object",
+ "additionalProperties": false,
+ "x-scope": ["admin", "tenant"],
+ "properties": {
+ "licenseExpirationWarning": {
+ "type": "object",
+ "additionalProperties": false,
+ "x-scope": ["admin", "tenant"],
+ "properties": {
+ "enable": {"type": "boolean", "x-scope": ["admin", "tenant"]},
+ "policies": {
+ "type": "object",
+ "additionalProperties": false,
+ "x-scope": ["admin", "tenant"],
+ "properties": {
+ "repeatInterval": {
+ "type": "string",
+ "pattern": "^(\\d+[smhd]|\\d+\\s*(second|minute|hour|day)s?)$",
+ "description": "Repeat interval in time format (e.g., '1d', '1h', '30m')",
+ "x-scope": ["admin", "tenant"]
+ }
+ }
+ }
+ }
+ },
+ "licenseExpirationError": {
+ "type": "object",
+ "additionalProperties": false,
+ "x-scope": ["admin", "tenant"],
+ "properties": {
+ "enable": {"type": "boolean", "x-scope": ["admin", "tenant"]},
+ "policies": {
+ "type": "object",
+ "additionalProperties": false,
+ "x-scope": ["admin", "tenant"],
+ "properties": {
+ "repeatInterval": {
+ "type": "string",
+ "pattern": "^(\\d+[smhd]|\\d+\\s*(second|minute|hour|day)s?)$",
+ "description": "Repeat interval in time format (e.g., '1d', '1h', '30m')",
+ "x-scope": ["admin", "tenant"]
+ }
+ }
+ }
+ }
+ },
+ "licenseLimitEdit": {
+ "type": "object",
+ "additionalProperties": false,
+ "x-scope": ["admin", "tenant"],
+ "properties": {
+ "enable": {"type": "boolean", "x-scope": ["admin", "tenant"]},
+ "policies": {
+ "type": "object",
+ "additionalProperties": false,
+ "x-scope": ["admin", "tenant"],
+ "properties": {
+ "repeatInterval": {
+ "type": "string",
+ "pattern": "^(\\d+[smhd]|\\d+\\s*(second|minute|hour|day)s?)$",
+ "description": "Repeat interval in time format (e.g., '1d', '1h', '30m')",
+ "x-scope": ["admin", "tenant"]
+ }
+ }
+ }
+ }
+ },
+ "licenseLimitLiveViewer": {
+ "type": "object",
+ "additionalProperties": false,
+ "x-scope": ["admin", "tenant"],
+ "properties": {
+ "enable": {"type": "boolean", "x-scope": ["admin", "tenant"]},
+ "policies": {
+ "type": "object",
+ "additionalProperties": false,
+ "x-scope": ["admin", "tenant"],
+ "properties": {
+ "repeatInterval": {
+ "type": "string",
+ "pattern": "^(\\d+[smhd]|\\d+\\s*(second|minute|hour|day)s?)$",
+ "description": "Repeat interval in time format (e.g., '1d', '1h', '30m')",
+ "x-scope": ["admin", "tenant"]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "adminPanel": {
+ "type": "object",
+ "additionalProperties": false,
+ "x-scope": ["admin", "tenant"],
+ "properties": {
+ "passwordValidation": {
+ "type": "object",
+ "x-scope": ["admin", "tenant"],
+ "description": "Password validation requirements using custom format types",
+ "properties": {
+ "minLength": {
+ "type": "string",
+ "format": "passlength",
+ "description": "be at least 8 characters long",
+ "x-scope": ["admin", "tenant"]
+ },
+ "hasDigit": {
+ "type": "string",
+ "format": "passdigit",
+ "description": "contain at least one digit",
+ "x-scope": ["admin", "tenant"]
+ },
+ "hasUppercase": {
+ "type": "string",
+ "format": "passupper",
+ "description": "contain at least one uppercase letter",
+ "x-scope": ["admin", "tenant"]
+ },
+ "hasSpecialChar": {
+ "type": "string",
+ "format": "passspecial",
+ "description": "contain at least one special character",
+ "x-scope": ["admin", "tenant"]
+ }
+ }
+ }
+ }
+ }
}
}