optimizations

This commit is contained in:
proddy
2025-10-28 22:19:08 +01:00
parent 55b893362c
commit 3abfb7bb9c
93 changed files with 3953 additions and 3361 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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