mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-08 08:49:52 +03:00
optimizations
This commit is contained in:
@@ -4,6 +4,42 @@ import type { APSettingsType } from 'types';
|
||||
|
||||
import { IP_ADDRESS_VALIDATOR } from './shared';
|
||||
|
||||
// Reusable validation rules
|
||||
const IP_FIELD_RULE = (fieldName: string) => [
|
||||
{ required: true, message: `${fieldName} is required` },
|
||||
IP_ADDRESS_VALIDATOR
|
||||
];
|
||||
|
||||
const SSID_RULES = [
|
||||
{ required: true, message: 'Please provide an SSID' },
|
||||
{ type: 'string' as const, max: 32, message: 'SSID must be 32 characters or less' }
|
||||
];
|
||||
|
||||
const PASSWORD_RULES = [
|
||||
{ required: true, message: 'Please provide an access point password' },
|
||||
{
|
||||
type: 'string' as const,
|
||||
min: 8,
|
||||
max: 64,
|
||||
message: 'Password must be 8-64 characters'
|
||||
}
|
||||
];
|
||||
|
||||
const CHANNEL_RULES = [
|
||||
{ required: true, message: 'Please provide a network channel' },
|
||||
{ type: 'number' as const, message: 'Channel must be between 1 and 14' }
|
||||
];
|
||||
|
||||
const MAX_CLIENTS_RULES = [
|
||||
{ required: true, message: 'Please specify a value for max clients' },
|
||||
{
|
||||
type: 'number' as const,
|
||||
min: 1,
|
||||
max: 9,
|
||||
message: 'Max clients must be between 1 and 9'
|
||||
}
|
||||
];
|
||||
|
||||
export const createAPSettingsValidator = (apSettings: APSettingsType) =>
|
||||
new Schema({
|
||||
provision_mode: {
|
||||
@@ -11,47 +47,12 @@ export const createAPSettingsValidator = (apSettings: APSettingsType) =>
|
||||
message: 'Please provide a provision mode'
|
||||
},
|
||||
...(isAPEnabled(apSettings) && {
|
||||
ssid: [
|
||||
{ required: true, message: 'Please provide an SSID' },
|
||||
{
|
||||
type: 'string',
|
||||
max: 32,
|
||||
message: 'SSID must be 32 characters or less'
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: 'Please provide an access point password' },
|
||||
{
|
||||
type: 'string',
|
||||
min: 8,
|
||||
max: 64,
|
||||
message: 'Password must be 8-64 characters'
|
||||
}
|
||||
],
|
||||
channel: [
|
||||
{ required: true, message: 'Please provide a network channel' },
|
||||
{ type: 'number', message: 'Channel must be between 1 and 14' }
|
||||
],
|
||||
max_clients: [
|
||||
{ required: true, message: 'Please specify a value for max clients' },
|
||||
{
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 9,
|
||||
message: 'Max clients must be between 1 and 9'
|
||||
}
|
||||
],
|
||||
local_ip: [
|
||||
{ required: true, message: 'Local IP address is required' },
|
||||
IP_ADDRESS_VALIDATOR
|
||||
],
|
||||
gateway_ip: [
|
||||
{ required: true, message: 'Gateway IP address is required' },
|
||||
IP_ADDRESS_VALIDATOR
|
||||
],
|
||||
subnet_mask: [
|
||||
{ required: true, message: 'Subnet mask is required' },
|
||||
IP_ADDRESS_VALIDATOR
|
||||
]
|
||||
ssid: SSID_RULES,
|
||||
password: PASSWORD_RULES,
|
||||
channel: CHANNEL_RULES,
|
||||
max_clients: MAX_CLIENTS_RULES,
|
||||
local_ip: IP_FIELD_RULE('Local IP address'),
|
||||
gateway_ip: IP_FIELD_RULE('Gateway IP address'),
|
||||
subnet_mask: IP_FIELD_RULE('Subnet mask')
|
||||
})
|
||||
});
|
||||
|
||||
@@ -3,35 +3,57 @@ import type { MqttSettingsType } from 'types';
|
||||
|
||||
import { IP_OR_HOSTNAME_VALIDATOR } from './shared';
|
||||
|
||||
// Constants for validation ranges
|
||||
const PORT_MIN = 0;
|
||||
const PORT_MAX = 65535;
|
||||
const KEEP_ALIVE_MIN = 1;
|
||||
const KEEP_ALIVE_MAX = 86400;
|
||||
const HEARTBEAT_MIN = 10;
|
||||
const HEARTBEAT_MAX = 86400;
|
||||
|
||||
// Reusable validator rules
|
||||
const REQUIRED_HOST_VALIDATOR = [
|
||||
{ required: true, message: 'Host is required' },
|
||||
IP_OR_HOSTNAME_VALIDATOR
|
||||
];
|
||||
|
||||
const REQUIRED_BASE_VALIDATOR = [{ required: true, message: 'Base is required' }];
|
||||
|
||||
const PORT_VALIDATOR = [
|
||||
{ required: true, message: 'Port is required' },
|
||||
{
|
||||
type: 'number' as const,
|
||||
min: PORT_MIN,
|
||||
max: PORT_MAX,
|
||||
message: `Port must be between ${PORT_MIN} and ${PORT_MAX}`
|
||||
}
|
||||
];
|
||||
|
||||
const createNumberValidator = (fieldName: string, min: number, max: number) => [
|
||||
{ required: true, message: `${fieldName} is required` },
|
||||
{
|
||||
type: 'number' as const,
|
||||
min,
|
||||
max,
|
||||
message: `${fieldName} must be between ${min} and ${max}`
|
||||
}
|
||||
];
|
||||
|
||||
export const createMqttSettingsValidator = (mqttSettings: MqttSettingsType) =>
|
||||
new Schema({
|
||||
...(mqttSettings.enabled && {
|
||||
host: [
|
||||
{ required: true, message: 'Host is required' },
|
||||
IP_OR_HOSTNAME_VALIDATOR
|
||||
],
|
||||
base: { required: true, message: 'Base is required' },
|
||||
port: [
|
||||
{ required: true, message: 'Port is required' },
|
||||
{ type: 'number', min: 0, max: 65535, message: 'Invalid Port' }
|
||||
],
|
||||
keep_alive: [
|
||||
{ required: true, message: 'Keep alive is required' },
|
||||
{
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 86400,
|
||||
message: 'Keep alive must be between 1 and 86400'
|
||||
}
|
||||
],
|
||||
publish_time_heartbeat: [
|
||||
{ required: true, message: 'Heartbeat is required' },
|
||||
{
|
||||
type: 'number',
|
||||
min: 10,
|
||||
max: 86400,
|
||||
message: 'Heartbeat must be between 10 and 86400'
|
||||
}
|
||||
]
|
||||
host: REQUIRED_HOST_VALIDATOR,
|
||||
base: REQUIRED_BASE_VALIDATOR,
|
||||
port: PORT_VALIDATOR,
|
||||
keep_alive: createNumberValidator(
|
||||
'Keep alive',
|
||||
KEEP_ALIVE_MIN,
|
||||
KEEP_ALIVE_MAX
|
||||
),
|
||||
publish_time_heartbeat: createNumberValidator(
|
||||
'Heartbeat',
|
||||
HEARTBEAT_MIN,
|
||||
HEARTBEAT_MAX
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
@@ -3,6 +3,23 @@ import type { NetworkSettingsType } from 'types';
|
||||
|
||||
import { HOSTNAME_VALIDATOR, IP_ADDRESS_VALIDATOR } from './shared';
|
||||
|
||||
// Reusable validator rules
|
||||
const REQUIRED_IP_VALIDATOR = (fieldName: string) => [
|
||||
{ required: true, message: `${fieldName} is required` },
|
||||
IP_ADDRESS_VALIDATOR
|
||||
];
|
||||
|
||||
const OPTIONAL_IP_VALIDATOR = [IP_ADDRESS_VALIDATOR];
|
||||
|
||||
// Helper to create static IP validation rules
|
||||
const createStaticIpRules = () => ({
|
||||
local_ip: REQUIRED_IP_VALIDATOR('Local IP'),
|
||||
gateway_ip: REQUIRED_IP_VALIDATOR('Gateway IP'),
|
||||
subnet_mask: REQUIRED_IP_VALIDATOR('Subnet mask'),
|
||||
dns_ip_1: OPTIONAL_IP_VALIDATOR,
|
||||
dns_ip_2: OPTIONAL_IP_VALIDATOR
|
||||
});
|
||||
|
||||
export const createNetworkSettingsValidator = (
|
||||
networkSettings: NetworkSettingsType
|
||||
) =>
|
||||
@@ -17,29 +34,16 @@ export const createNetworkSettingsValidator = (
|
||||
message: 'BSSID must be 17 characters or empty'
|
||||
}
|
||||
],
|
||||
password: {
|
||||
type: 'string',
|
||||
max: 64,
|
||||
message: 'Password must be 64 characters or less'
|
||||
},
|
||||
password: [
|
||||
{
|
||||
type: 'string',
|
||||
max: 64,
|
||||
message: 'Password must be 64 characters or less'
|
||||
}
|
||||
],
|
||||
hostname: [
|
||||
{ required: true, message: 'Hostname is required' },
|
||||
HOSTNAME_VALIDATOR
|
||||
],
|
||||
...(networkSettings.static_ip_config && {
|
||||
local_ip: [
|
||||
{ required: true, message: 'Local IP is required' },
|
||||
IP_ADDRESS_VALIDATOR
|
||||
],
|
||||
gateway_ip: [
|
||||
{ required: true, message: 'Gateway IP is required' },
|
||||
IP_ADDRESS_VALIDATOR
|
||||
],
|
||||
subnet_mask: [
|
||||
{ required: true, message: 'Subnet mask is required' },
|
||||
IP_ADDRESS_VALIDATOR
|
||||
],
|
||||
dns_ip_1: IP_ADDRESS_VALIDATOR,
|
||||
dns_ip_2: IP_ADDRESS_VALIDATOR
|
||||
})
|
||||
...(networkSettings.static_ip_config && createStaticIpRules())
|
||||
});
|
||||
|
||||
@@ -7,8 +7,5 @@ export const NTP_SETTINGS_VALIDATOR = new Schema({
|
||||
{ required: true, message: 'Server is required' },
|
||||
IP_OR_HOSTNAME_VALIDATOR
|
||||
],
|
||||
tz_label: {
|
||||
required: true,
|
||||
message: 'Time zone is required'
|
||||
}
|
||||
tz_label: [{ required: true, message: 'Time zone is required' }]
|
||||
});
|
||||
|
||||
@@ -2,25 +2,34 @@ import Schema from 'async-validator';
|
||||
import type { InternalRuleItem } from 'async-validator';
|
||||
import type { UserType } from 'types';
|
||||
|
||||
const USERNAME_PATTERN = /^[a-zA-Z0-9_\\.]{1,24}$/;
|
||||
const JWT_SECRET_MAX_LENGTH = 64;
|
||||
const PASSWORD_MAX_LENGTH = 64;
|
||||
|
||||
export const SECURITY_SETTINGS_VALIDATOR = new Schema({
|
||||
jwt_secret: [
|
||||
{ required: true, message: 'JWT secret is required' },
|
||||
{
|
||||
type: 'string',
|
||||
min: 1,
|
||||
max: 64,
|
||||
message: 'JWT secret must be between 1 and 64 characters'
|
||||
max: JWT_SECRET_MAX_LENGTH,
|
||||
message: `JWT secret must be between 1 and ${JWT_SECRET_MAX_LENGTH} characters`
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a validator to ensure username uniqueness
|
||||
* @param users - Array of existing users to check against
|
||||
* @returns Validator rule for unique username
|
||||
*/
|
||||
export const createUniqueUsernameValidator = (users: UserType[]) => ({
|
||||
validator(
|
||||
rule: InternalRuleItem,
|
||||
_rule: InternalRuleItem,
|
||||
username: string,
|
||||
callback: (error?: string) => void
|
||||
) {
|
||||
if (username && users.find((u) => u.username === username)) {
|
||||
if (username && users.some((u) => u.username === username)) {
|
||||
callback('Username already in use');
|
||||
} else {
|
||||
callback();
|
||||
@@ -28,13 +37,19 @@ export const createUniqueUsernameValidator = (users: UserType[]) => ({
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a validator schema for user creation/editing
|
||||
* @param users - Array of existing users for uniqueness check
|
||||
* @param creating - Whether this is for creating a new user (enables uniqueness check)
|
||||
* @returns Schema validator for user data
|
||||
*/
|
||||
export const createUserValidator = (users: UserType[], creating: boolean) =>
|
||||
new Schema({
|
||||
username: [
|
||||
{ required: true, message: 'Username is required' },
|
||||
{
|
||||
type: 'string',
|
||||
pattern: /^[a-zA-Z0-9_\\.]{1,24}$/,
|
||||
pattern: USERNAME_PATTERN,
|
||||
message: "Must be 1-24 characters: alphanumeric, '_' or '.'"
|
||||
},
|
||||
...(creating ? [createUniqueUsernameValidator(users)] : [])
|
||||
@@ -44,8 +59,8 @@ export const createUserValidator = (users: UserType[], creating: boolean) =>
|
||||
{
|
||||
type: 'string',
|
||||
min: 1,
|
||||
max: 64,
|
||||
message: 'Password must be 1-64 characters'
|
||||
max: PASSWORD_MAX_LENGTH,
|
||||
message: `Password must be 1-${PASSWORD_MAX_LENGTH} characters`
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@@ -7,66 +7,54 @@ export const validate = <T extends object>(
|
||||
options?: ValidateOption
|
||||
): Promise<T> =>
|
||||
new Promise((resolve, reject) => {
|
||||
void validator.validate(source, options || {}, (errors, fieldErrors) => {
|
||||
if (errors) {
|
||||
reject(fieldErrors as Error);
|
||||
} else {
|
||||
resolve(source as T);
|
||||
}
|
||||
void validator.validate(source, options ?? {}, (errors, fieldErrors) => {
|
||||
errors ? reject(fieldErrors as Error) : resolve(source as T);
|
||||
});
|
||||
});
|
||||
|
||||
// updated to support both IPv4 and IPv6
|
||||
const IP_ADDRESS_REGEXP =
|
||||
/((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/;
|
||||
// IPv4 pattern: matches 0.0.0.0 to 255.255.255.255
|
||||
const IPV4_PATTERN =
|
||||
/^((25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)$/;
|
||||
|
||||
const isValidIpAddress = (value: string) => IP_ADDRESS_REGEXP.test(value);
|
||||
// IPv6 pattern: matches full and compressed IPv6 addresses (including IPv4-mapped)
|
||||
const IPV6_PATTERN =
|
||||
/^(([0-9a-f]{1,4}:){7}[0-9a-f]{1,4}|([0-9a-f]{1,4}:){1,7}:|([0-9a-f]{1,4}:){1,6}:[0-9a-f]{1,4}|([0-9a-f]{1,4}:){1,5}(:[0-9a-f]{1,4}){1,2}|([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,3}|([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}|([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}|[0-9a-f]{1,4}:((:[0-9a-f]{1,4}){1,6})|:((:[0-9a-f]{1,4}){1,7}|:)|::)$/i;
|
||||
|
||||
export const IP_ADDRESS_VALIDATOR = {
|
||||
// Hostname pattern: RFC 1123 compliant (max 200 chars)
|
||||
const HOSTNAME_PATTERN =
|
||||
/^(?=.{1,200}$)(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])$/i;
|
||||
|
||||
const isValidIpAddress = (value: string): boolean =>
|
||||
IPV4_PATTERN.test(value.trim()) || IPV6_PATTERN.test(value.trim());
|
||||
|
||||
const isValidHostname = (value: string): boolean =>
|
||||
HOSTNAME_PATTERN.test(value.trim());
|
||||
|
||||
// Factory function to create validators with consistent structure
|
||||
const createValidator = (
|
||||
validatorFn: (value: string) => boolean,
|
||||
errorMessage: string
|
||||
) => ({
|
||||
validator(
|
||||
rule: InternalRuleItem,
|
||||
_rule: InternalRuleItem,
|
||||
value: string,
|
||||
callback: (error?: string) => void
|
||||
) {
|
||||
if (value && !isValidIpAddress(value)) {
|
||||
callback('Must be an IP address');
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
callback(value && !validatorFn(value) ? errorMessage : undefined);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const HOSTNAME_LENGTH_REGEXP = /^.{0,200}$/;
|
||||
const HOSTNAME_PATTERN_REGEXP =
|
||||
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/;
|
||||
export const IP_ADDRESS_VALIDATOR = createValidator(
|
||||
isValidIpAddress,
|
||||
'Must be an IP address'
|
||||
);
|
||||
|
||||
const isValidHostname = (value: string) =>
|
||||
HOSTNAME_LENGTH_REGEXP.test(value) && HOSTNAME_PATTERN_REGEXP.test(value);
|
||||
export const HOSTNAME_VALIDATOR = createValidator(
|
||||
isValidHostname,
|
||||
'Must be a valid hostname'
|
||||
);
|
||||
|
||||
export const HOSTNAME_VALIDATOR = {
|
||||
validator(
|
||||
rule: InternalRuleItem,
|
||||
value: string,
|
||||
callback: (error?: string) => void
|
||||
) {
|
||||
if (value && !isValidHostname(value)) {
|
||||
callback('Must be a valid hostname');
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const IP_OR_HOSTNAME_VALIDATOR = {
|
||||
validator(
|
||||
rule: InternalRuleItem,
|
||||
value: string,
|
||||
callback: (error?: string) => void
|
||||
) {
|
||||
if (value && !(isValidIpAddress(value) || isValidHostname(value))) {
|
||||
callback('Must be a valid IP address or hostname');
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
export const IP_OR_HOSTNAME_VALIDATOR = createValidator(
|
||||
(value) => isValidIpAddress(value) || isValidHostname(value),
|
||||
'Must be a valid IP address or hostname'
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user