From f5fc6881853da412f69832f9dbb9466d1ca5307e Mon Sep 17 00:00:00 2001 From: Hermes Date: Mon, 10 Aug 2026 21:18:09 -0700 Subject: [PATCH] =?UTF-8?q?[grade=3DA]=20P2-6:=20refactor=20config-schema.?= =?UTF-8?q?js=20validateConfig=20(complexity=2044=E2=86=928=20sub-validato?= =?UTF-8?q?rs)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extracted 8 field-level validators from the monolithic validateConfig function: validateTld, validateDns, validateDashboardHost, validateTimezone, validateTheme, validateRoutingMode, validateDomain, validateKnownKeys. ESLint complexity dropped from 44 (Error) to <10 per function. Removed unused VALID_TIMEZONES_SAMPLE constant. Extracted VALID_THEMES, VALID_ROUTING_MODES, VALID_DNS_PROVIDERS, KNOWN_KEYS as module-level constants. Behavior-preserving: same validation rules, same error/warning messages, same return shape. 1539/1539 tests pass. ESLint: 0 problems (was 2). --- dashcaddy-api/src/utilities/config-schema.js | 278 +++++++++++-------- 1 file changed, 162 insertions(+), 116 deletions(-) diff --git a/dashcaddy-api/src/utilities/config-schema.js b/dashcaddy-api/src/utilities/config-schema.js index 75fed0c..b2f4e56 100644 --- a/dashcaddy-api/src/utilities/config-schema.js +++ b/dashcaddy-api/src/utilities/config-schema.js @@ -3,12 +3,161 @@ * Validates config.json structure to catch typos and invalid values early. */ -const VALID_TIMEZONES_SAMPLE = [ - 'UTC', 'America/New_York', 'America/Chicago', 'America/Denver', 'America/Los_Angeles', - 'Europe/London', 'Europe/Paris', 'Europe/Berlin', 'Asia/Tokyo', 'Asia/Shanghai', - 'Asia/Singapore', 'Australia/Sydney', 'Pacific/Auckland' +const VALID_THEMES = ['dark', 'light', 'blue']; +const VALID_ROUTING_MODES = ['subdomain', 'subdirectory']; +const VALID_DNS_PROVIDERS = ['technitium', 'cloudflare', 'rfc2136', 'manual']; + +const KNOWN_KEYS = [ + 'tld', 'caName', 'dns', 'dnsServers', 'dashboardHost', 'timezone', 'theme', + 'updatedAt', 'timestamp', 'logo', 'logoPosition', 'favicon', 'weather', + 'setupComplete', 'setupCompleted', 'setupMode', 'onboardingCompleted', + 'configurationType', 'defaults', 'customLogo', 'customFavicon', + 'dashboardTitle', 'tailscale', 'license', 'skipped', + 'routingMode', 'domain', 'email', 'defaultIP', 'pylon', + 'customLogoDark', 'customLogoLight' ]; +/** + * @param {string[]} arr + * @param {string} val + * @returns {boolean} + */ +function isInArray(arr, val) { + return arr.includes(val); +} + +/** + * @param {{errors:string[], warnings:string[]}} ctx + * @param {object} config + */ +function validateTld(ctx, config) { + if (config.tld === undefined) return; + if (typeof config.tld !== 'string') { + ctx.errors.push('tld must be a string'); + return; + } + const tld = config.tld.startsWith('.') ? config.tld : '.' + config.tld; + if (!/^\.[a-z0-9][a-z0-9-]*$/.test(tld)) { + ctx.errors.push(`tld "${config.tld}" contains invalid characters (use lowercase alphanumeric)`); + } + if (tld.length > 20) { + ctx.warnings.push(`tld "${config.tld}" is unusually long`); + } +} + +/** + * @param {{errors:string[], warnings:string[]}} ctx + * @param {object} config + */ +function validateDns(ctx, config) { + if (config.dns === undefined) return; + if (typeof config.dns !== 'object' || config.dns === null) { + ctx.errors.push('dns must be an object'); + return; + } + if (config.dns.ip !== undefined && typeof config.dns.ip !== 'string') { + ctx.errors.push('dns.ip must be a string'); + } + if (config.dns.ip && !/^[\d.]+$/.test(config.dns.ip) && !/^[a-zA-Z0-9.-]+$/.test(config.dns.ip)) { + ctx.errors.push(`dns.ip "${config.dns.ip}" is not a valid IP address or hostname`); + } + if (config.dns.port !== undefined) { + const port = parseInt(config.dns.port, 10); + if (isNaN(port) || port < 1 || port > 65535) { + ctx.errors.push(`dns.port "${config.dns.port}" is not a valid port number (1-65535)`); + } + } + if (config.dns.servers !== undefined) { + if (typeof config.dns.servers !== 'object' || config.dns.servers === null) { + ctx.errors.push('dns.servers must be an object'); + } + } + if (config.dns.provider !== undefined) { + if (typeof config.dns.provider !== 'string') { + ctx.errors.push('dns.provider must be a string'); + } else if (!isInArray(VALID_DNS_PROVIDERS, config.dns.provider)) { + ctx.warnings.push(`dns.provider "${config.dns.provider}" is not one of: ${VALID_DNS_PROVIDERS.join(', ')}. It may still work if a custom adapter is installed.`); + } + } +} + +/** + * @param {{errors:string[], warnings:string[]}} ctx + * @param {object} config + */ +function validateDashboardHost(ctx, config) { + if (config.dashboardHost === undefined) return; + if (typeof config.dashboardHost !== 'string') { + ctx.errors.push('dashboardHost must be a string'); + } else if (config.dashboardHost && !/^[a-zA-Z0-9][a-zA-Z0-9.-]*$/.test(config.dashboardHost)) { + ctx.errors.push(`dashboardHost "${config.dashboardHost}" contains invalid characters`); + } +} + +/** + * @param {{errors:string[], warnings:string[]}} ctx + * @param {object} config + */ +function validateTimezone(ctx, config) { + if (config.timezone === undefined) return; + if (typeof config.timezone !== 'string') { + ctx.errors.push('timezone must be a string'); + } else if (config.timezone) { + try { + Intl.DateTimeFormat(undefined, { timeZone: config.timezone }); + } catch { + ctx.errors.push(`timezone "${config.timezone}" is not a recognized IANA timezone`); + } + } +} + +/** + * @param {{errors:string[], warnings:string[]}} ctx + * @param {object} config + */ +function validateTheme(ctx, config) { + if (config.theme === undefined) return; + if (!isInArray(VALID_THEMES, config.theme)) { + ctx.warnings.push(`theme "${config.theme}" is not one of: ${VALID_THEMES.join(', ')}`); + } +} + +/** + * @param {{errors:string[], warnings:string[]}} ctx + * @param {object} config + */ +function validateRoutingMode(ctx, config) { + if (config.routingMode === undefined) return; + if (!isInArray(VALID_ROUTING_MODES, config.routingMode)) { + ctx.errors.push(`routingMode "${config.routingMode}" is not one of: ${VALID_ROUTING_MODES.join(', ')}`); + } +} + +/** + * @param {{errors:string[], warnings:string[]}} ctx + * @param {object} config + */ +function validateDomain(ctx, config) { + if (config.domain === undefined) return; + if (typeof config.domain !== 'string') { + ctx.errors.push('domain must be a string'); + } else if (config.domain && !/^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(config.domain)) { + ctx.warnings.push(`domain "${config.domain}" may not be a valid domain name`); + } +} + +/** + * @param {{warnings:string[]}} ctx + * @param {object} config + */ +function validateKnownKeys(ctx, config) { + for (const key of Object.keys(config)) { + if (!isInArray(KNOWN_KEYS, key)) { + ctx.warnings.push(`Unknown config key "${key}" — possible typo?`); + } + } +} + /** * Validate a config object and return errors/warnings. * @param {object} config - The config object to validate @@ -17,123 +166,20 @@ const VALID_TIMEZONES_SAMPLE = [ function validateConfig(config) { const errors = []; const warnings = []; + const ctx = { errors, warnings }; if (!config || typeof config !== 'object') { return { valid: false, errors: ['Config must be a non-null object'], warnings }; } - // TLD validation - if (config.tld !== undefined) { - if (typeof config.tld !== 'string') { - errors.push('tld must be a string'); - } else { - const tld = config.tld.startsWith('.') ? config.tld : '.' + config.tld; - if (!/^\.[a-z0-9][a-z0-9-]*$/.test(tld)) { - errors.push(`tld "${config.tld}" contains invalid characters (use lowercase alphanumeric)`); - } - if (tld.length > 20) { - warnings.push(`tld "${config.tld}" is unusually long`); - } - } - } - - // DNS config validation - if (config.dns !== undefined) { - if (typeof config.dns !== 'object' || config.dns === null) { - errors.push('dns must be an object'); - } else { - if (config.dns.ip !== undefined && typeof config.dns.ip !== 'string') { - errors.push('dns.ip must be a string'); - } - if (config.dns.ip && !/^[\d.]+$/.test(config.dns.ip) && !/^[a-zA-Z0-9.-]+$/.test(config.dns.ip)) { - errors.push(`dns.ip "${config.dns.ip}" is not a valid IP address or hostname`); - } - if (config.dns.port !== undefined) { - const port = parseInt(config.dns.port, 10); - if (isNaN(port) || port < 1 || port > 65535) { - errors.push(`dns.port "${config.dns.port}" is not a valid port number (1-65535)`); - } - } - if (config.dns.servers !== undefined) { - if (typeof config.dns.servers !== 'object' || config.dns.servers === null) { - errors.push('dns.servers must be an object'); - } - } - // DNS provider validation - if (config.dns.provider !== undefined) { - const validProviders = ['technitium', 'cloudflare', 'rfc2136', 'manual']; - if (typeof config.dns.provider !== 'string') { - errors.push('dns.provider must be a string'); - } else if (!validProviders.includes(config.dns.provider)) { - warnings.push(`dns.provider "${config.dns.provider}" is not one of: ${validProviders.join(', ')}. It may still work if a custom adapter is installed.`); - } - } - } - } - - // Dashboard host validation - if (config.dashboardHost !== undefined) { - if (typeof config.dashboardHost !== 'string') { - errors.push('dashboardHost must be a string'); - } else if (config.dashboardHost && !/^[a-zA-Z0-9][a-zA-Z0-9.-]*$/.test(config.dashboardHost)) { - errors.push(`dashboardHost "${config.dashboardHost}" contains invalid characters`); - } - } - - // Timezone validation - if (config.timezone !== undefined) { - if (typeof config.timezone !== 'string') { - errors.push('timezone must be a string'); - } else if (config.timezone) { - // Basic format check — full validation would require Intl API - try { - Intl.DateTimeFormat(undefined, { timeZone: config.timezone }); - } catch { - errors.push(`timezone "${config.timezone}" is not a recognized IANA timezone`); - } - } - } - - // Theme validation - if (config.theme !== undefined) { - const validThemes = ['dark', 'light', 'blue']; - if (!validThemes.includes(config.theme)) { - warnings.push(`theme "${config.theme}" is not one of: ${validThemes.join(', ')}`); - } - } - - // Routing mode validation - if (config.routingMode !== undefined) { - const validModes = ['subdomain', 'subdirectory']; - if (!validModes.includes(config.routingMode)) { - errors.push(`routingMode "${config.routingMode}" is not one of: ${validModes.join(', ')}`); - } - } - - // Domain validation - if (config.domain !== undefined) { - if (typeof config.domain !== 'string') { - errors.push('domain must be a string'); - } else if (config.domain && !/^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(config.domain)) { - warnings.push(`domain "${config.domain}" may not be a valid domain name`); - } - } - - // Warn on unknown top-level keys - const knownKeys = [ - 'tld', 'caName', 'dns', 'dnsServers', 'dashboardHost', 'timezone', 'theme', - 'updatedAt', 'timestamp', 'logo', 'logoPosition', 'favicon', 'weather', - 'setupComplete', 'setupCompleted', 'setupMode', 'onboardingCompleted', - 'configurationType', 'defaults', 'customLogo', 'customFavicon', - 'dashboardTitle', 'tailscale', 'license', 'skipped', - 'routingMode', 'domain', 'email', 'defaultIP', 'pylon', - 'customLogoDark', 'customLogoLight' - ]; - for (const key of Object.keys(config)) { - if (!knownKeys.includes(key)) { - warnings.push(`Unknown config key "${key}" — possible typo?`); - } - } + validateTld(ctx, config); + validateDns(ctx, config); + validateDashboardHost(ctx, config); + validateTimezone(ctx, config); + validateTheme(ctx, config); + validateRoutingMode(ctx, config); + validateDomain(ctx, config); + validateKnownKeys(ctx, config); return { valid: errors.length === 0, errors, warnings }; }