diff --git a/fe-app-podkop/src/constants.ts b/fe-app-podkop/src/constants.ts index b0ccded..130089d 100644 --- a/fe-app-podkop/src/constants.ts +++ b/fe-app-podkop/src/constants.ts @@ -1,70 +1,76 @@ export const STATUS_COLORS = { - SUCCESS: '#4caf50', - ERROR: '#f44336', - WARNING: '#ff9800' + SUCCESS: '#4caf50', + ERROR: '#f44336', + WARNING: '#ff9800', }; export const FAKEIP_CHECK_DOMAIN = 'fakeip.podkop.fyi'; export const IP_CHECK_DOMAIN = 'ip.podkop.fyi'; -export const REGIONAL_OPTIONS = ['russia_inside', 'russia_outside', 'ukraine_inside']; +export const REGIONAL_OPTIONS = [ + 'russia_inside', + 'russia_outside', + 'ukraine_inside', +]; + export const ALLOWED_WITH_RUSSIA_INSIDE = [ - 'russia_inside', - 'meta', - 'twitter', - 'discord', - 'telegram', - 'cloudflare', - 'google_ai', - 'google_play', - 'hetzner', - 'ovh', - 'hodca', - 'digitalocean', - 'cloudfront' + 'russia_inside', + 'meta', + 'twitter', + 'discord', + 'telegram', + 'cloudflare', + 'google_ai', + 'google_play', + 'hetzner', + 'ovh', + 'hodca', + 'digitalocean', + 'cloudfront', ]; export const DOMAIN_LIST_OPTIONS = { - russia_inside: 'Russia inside', - russia_outside: 'Russia outside', - ukraine_inside: 'Ukraine', - geoblock: 'Geo Block', - block: 'Block', - porn: 'Porn', - news: 'News', - anime: 'Anime', - youtube: 'Youtube', - discord: 'Discord', - meta: 'Meta', - twitter: 'Twitter (X)', - hdrezka: 'HDRezka', - tiktok: 'Tik-Tok', - telegram: 'Telegram', - cloudflare: 'Cloudflare', - google_ai: 'Google AI', - google_play: 'Google Play', - hodca: 'H.O.D.C.A', - hetzner: 'Hetzner ASN', - ovh: 'OVH ASN', - digitalocean: 'Digital Ocean ASN', - cloudfront: 'CloudFront ASN' + russia_inside: 'Russia inside', + russia_outside: 'Russia outside', + ukraine_inside: 'Ukraine', + geoblock: 'Geo Block', + block: 'Block', + porn: 'Porn', + news: 'News', + anime: 'Anime', + youtube: 'Youtube', + discord: 'Discord', + meta: 'Meta', + twitter: 'Twitter (X)', + hdrezka: 'HDRezka', + tiktok: 'Tik-Tok', + telegram: 'Telegram', + cloudflare: 'Cloudflare', + google_ai: 'Google AI', + google_play: 'Google Play', + hodca: 'H.O.D.C.A', + hetzner: 'Hetzner ASN', + ovh: 'OVH ASN', + digitalocean: 'Digital Ocean ASN', + cloudfront: 'CloudFront ASN', }; export const UPDATE_INTERVAL_OPTIONS = { - '1h': 'Every hour', - '3h': 'Every 3 hours', - '12h': 'Every 12 hours', - '1d': 'Every day', - '3d': 'Every 3 days' + '1h': 'Every hour', + '3h': 'Every 3 hours', + '12h': 'Every 12 hours', + '1d': 'Every day', + '3d': 'Every 3 days', }; export const DNS_SERVER_OPTIONS = { - '1.1.1.1': '1.1.1.1 (Cloudflare)', - '8.8.8.8': '8.8.8.8 (Google)', - '9.9.9.9': '9.9.9.9 (Quad9)', - 'dns.adguard-dns.com': 'dns.adguard-dns.com (AdGuard Default)', - 'unfiltered.adguard-dns.com': 'unfiltered.adguard-dns.com (AdGuard Unfiltered)', - 'family.adguard-dns.com': 'family.adguard-dns.com (AdGuard Family)' + '1.1.1.1': '1.1.1.1 (Cloudflare)', + '8.8.8.8': '8.8.8.8 (Google)', + '9.9.9.9': '9.9.9.9 (Quad9)', + 'dns.adguard-dns.com': 'dns.adguard-dns.com (AdGuard Default)', + 'unfiltered.adguard-dns.com': + 'unfiltered.adguard-dns.com (AdGuard Unfiltered)', + 'family.adguard-dns.com': 'family.adguard-dns.com (AdGuard Family)', }; export const DIAGNOSTICS_UPDATE_INTERVAL = 10000; // 10 seconds @@ -77,15 +83,15 @@ export const DIAGNOSTICS_INITIAL_DELAY = 100; // 100 milliseconds // Command scheduling intervals in diagnostics (in milliseconds) export const COMMAND_SCHEDULING = { - P0_PRIORITY: 0, // Highest priority (no delay) - P1_PRIORITY: 100, // Very high priority - P2_PRIORITY: 300, // High priority - P3_PRIORITY: 500, // Above average - P4_PRIORITY: 700, // Standard priority - P5_PRIORITY: 900, // Below average - P6_PRIORITY: 1100, // Low priority - P7_PRIORITY: 1300, // Very low priority - P8_PRIORITY: 1500, // Background execution - P9_PRIORITY: 1700, // Idle mode execution - P10_PRIORITY: 1900 // Lowest priority + P0_PRIORITY: 0, // Highest priority (no delay) + P1_PRIORITY: 100, // Very high priority + P2_PRIORITY: 300, // High priority + P3_PRIORITY: 500, // Above average + P4_PRIORITY: 700, // Standard priority + P5_PRIORITY: 900, // Below average + P6_PRIORITY: 1100, // Low priority + P7_PRIORITY: 1300, // Very low priority + P8_PRIORITY: 1500, // Background execution + P9_PRIORITY: 1700, // Idle mode execution + P10_PRIORITY: 1900, // Lowest priority }; diff --git a/fe-app-podkop/src/main.ts b/fe-app-podkop/src/main.ts index bc2665e..fad5419 100644 --- a/fe-app-podkop/src/main.ts +++ b/fe-app-podkop/src/main.ts @@ -2,4 +2,4 @@ 'require baseclass'; export * from './validators'; -export * from './constants' +export * from './constants'; diff --git a/fe-app-podkop/src/validators/index.ts b/fe-app-podkop/src/validators/index.ts index 2adb0bd..5d0851d 100644 --- a/fe-app-podkop/src/validators/index.ts +++ b/fe-app-podkop/src/validators/index.ts @@ -2,3 +2,4 @@ export * from './validateIp'; export * from './validateDomain'; export * from './validateDns'; export * from './validateUrl'; +export * from './validatePath'; diff --git a/fe-app-podkop/src/validators/tests/validatePath.test.js b/fe-app-podkop/src/validators/tests/validatePath.test.js new file mode 100644 index 0000000..5f43be4 --- /dev/null +++ b/fe-app-podkop/src/validators/tests/validatePath.test.js @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest'; +import { validatePath } from '../validatePath'; + +export const validPaths = [ + ['Single level', '/etc'], + ['Nested path', '/usr/local/bin'], + ['With dash', '/var/log/nginx-access'], + ['With underscore', '/opt/my_app/config'], + ['With numbers', '/data123/files'], + ['With dots', '/home/user/.config'], + ['Deep nested', '/a/b/c/d/e/f/g'], +]; + +export const invalidPaths = [ + ['Empty string', ''], + ['Missing starting slash', 'usr/local'], + ['Only dot', '.'], + ['Space inside', '/path with space'], + ['Illegal char', '/path$'], + ['Backslash not allowed', '\\windows\\path'], + ['Relative path ./', './relative'], + ['Relative path ../', '../parent'], +]; + +describe('validatePath', () => { + describe.each(validPaths)('Valid path: %s', (_desc, path) => { + it(`returns valid=true for "${path}"`, () => { + const res = validatePath(path); + expect(res.valid).toBe(true); + }); + }); + + describe.each(invalidPaths)('Invalid path: %s', (_desc, path) => { + it(`returns valid=false for "${path}"`, () => { + const res = validatePath(path); + expect(res.valid).toBe(false); + }); + }); +}); diff --git a/fe-app-podkop/src/validators/validatePath.ts b/fe-app-podkop/src/validators/validatePath.ts new file mode 100644 index 0000000..9da07ba --- /dev/null +++ b/fe-app-podkop/src/validators/validatePath.ts @@ -0,0 +1,25 @@ +import { ValidationResult } from './types'; + +export function validatePath(value: string): ValidationResult { + if (!value) { + return { + valid: false, + message: 'Path cannot be empty', + }; + } + + const pathRegex = /^\/[a-zA-Z0-9_\-/.]+$/; + + if (pathRegex.test(value)) { + return { + valid: true, + message: 'Valid', + }; + } + + return { + valid: false, + message: + 'Invalid path format. Path must start with "/" and contain valid characters', + }; +} diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js index cdca7be..3fe88c7 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js @@ -409,12 +409,18 @@ function createConfigSection(section, map, network) { o.rmempty = false; o.ucisection = s.section; o.validate = function (section_id, value) { - if (!value || value.length === 0) return true; - const pathRegex = /^\/[a-zA-Z0-9_\-\/\.]+$/; - if (!pathRegex.test(value)) { - return _('Invalid path format. Path must start with "/" and contain valid characters'); + // Optional + if (!value || value.length === 0) { + return true } - return true; + + const validation = main.validatePath(value); + + if (validation.valid) { + return true; + } + + return _(validation.message) }; o = s.taboption('basic', form.Flag, 'remote_domain_lists_enabled', _('Remote Domain Lists'), _('Download and use domain lists from remote URLs')); @@ -453,12 +459,18 @@ function createConfigSection(section, map, network) { o.rmempty = false; o.ucisection = s.section; o.validate = function (section_id, value) { - if (!value || value.length === 0) return true; - const pathRegex = /^\/[a-zA-Z0-9_\-\/\.]+$/; - if (!pathRegex.test(value)) { - return _('Invalid path format. Path must start with "/" and contain valid characters'); + // Optional + if (!value || value.length === 0) { + return true } - return true; + + const validation = main.validatePath(value); + + if (validation.valid) { + return true; + } + + return _(validation.message) }; o = s.taboption('basic', form.ListValue, 'user_subnet_list_type', _('User Subnet List Type'), _('Select how to add your custom subnets')); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js index 6b4953a..36bfda0 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js @@ -59,6 +59,27 @@ function validateUrl(url, protocols = ["http:", "https:"]) { } } +// src/validators/validatePath.ts +function validatePath(value) { + if (!value) { + return { + valid: false, + message: "Path cannot be empty" + }; + } + const pathRegex = /^\/[a-zA-Z0-9_\-/.]+$/; + if (pathRegex.test(value)) { + return { + valid: true, + message: "Valid" + }; + } + return { + valid: false, + message: 'Invalid path format. Path must start with "/" and contain valid characters' + }; +} + // src/constants.ts var STATUS_COLORS = { SUCCESS: "#4caf50", @@ -67,7 +88,11 @@ var STATUS_COLORS = { }; var FAKEIP_CHECK_DOMAIN = "fakeip.podkop.fyi"; var IP_CHECK_DOMAIN = "ip.podkop.fyi"; -var REGIONAL_OPTIONS = ["russia_inside", "russia_outside", "ukraine_inside"]; +var REGIONAL_OPTIONS = [ + "russia_inside", + "russia_outside", + "ukraine_inside" +]; var ALLOWED_WITH_RUSSIA_INSIDE = [ "russia_inside", "meta", @@ -174,5 +199,6 @@ return baseclass.extend({ validateDNS, validateDomain, validateIPV4, + validatePath, validateUrl });