|  | 
| 275 | 275 |           $$('input[type=range]').forEach(el => {rangeSlider(el, false, el.getAttribute('value'));});  // initialise range sliders | 
| 276 | 276 |         } | 
| 277 | 277 | 
 | 
| 278 |  | -        async function sendUpdates(doAction) {     | 
|  | 278 | +        async function sendUpdates(doAction) { | 
|  | 279 | +          // Validate AP_Pass before sending | 
|  | 280 | +          const apPassInput = $('#AP_Pass'); | 
|  | 281 | +          if (apPassInput) { | 
|  | 282 | +            const errorMessage = validateApPassword(apPassInput.value.trim()); | 
|  | 283 | +            if (errorMessage) { | 
|  | 284 | +              showInputError(apPassInput, errorMessage); | 
|  | 285 | +              showAlert('Cannot save settings: Invalid AP password.'); | 
|  | 286 | +              return; | 
|  | 287 | +            } | 
|  | 288 | +          }  | 
| 279 | 289 |           // send bulk updates to app as json  | 
| 280 | 290 |           statusData['action'] = doAction; | 
| 281 | 291 |           const response = await fetch(webServer + '/update', { | 
|  | 
| 288 | 298 | 
 | 
| 289 | 299 |         /*********** utility functions ***********/ | 
| 290 | 300 | 
 | 
|  | 301 | +        // Function to validate AP password | 
|  | 302 | +        function validateApPassword(password) { | 
|  | 303 | +          const minLength = 8; | 
|  | 304 | +          const maxLength = 63; | 
|  | 305 | +          const validAscii = /^[\x20-\x7E]*$/; // Printable ASCII characters only | 
|  | 306 | +          const strongPassword = /^(?=.*[A-Za-z])(?=.*\d).+$/; // At least one letter and one number | 
|  | 307 | + | 
|  | 308 | +          if (password.length < minLength) { | 
|  | 309 | +            return 'Password must be at least 8 characters long.'; | 
|  | 310 | +          } | 
|  | 311 | +          if (password.length > maxLength) { | 
|  | 312 | +            return 'Password cannot exceed 63 characters.'; | 
|  | 313 | +          } | 
|  | 314 | +          if (!validAscii.test(password)) { | 
|  | 315 | +            return 'Password must contain only printable ASCII characters.'; | 
|  | 316 | +          } | 
|  | 317 | +          if (!strongPassword.test(password)) { | 
|  | 318 | +            return 'Password must include at least one letter and one number.'; | 
|  | 319 | +          } | 
|  | 320 | +          return ''; // Valid password | 
|  | 321 | +        } | 
|  | 322 | + | 
|  | 323 | +        // Display error messages below input fields | 
|  | 324 | +        function showInputError(inputEl, errorMessage) { | 
|  | 325 | +          let errorEl = inputEl.nextElementSibling; | 
|  | 326 | +          if (!errorEl || !errorEl.classList.contains('error-message')) { | 
|  | 327 | +            errorEl = document.createElement('div'); | 
|  | 328 | +            errorEl.classList.add('error-message'); | 
|  | 329 | +            errorEl.style.color = 'red'; | 
|  | 330 | +            errorEl.style.fontSize = '0.8em'; | 
|  | 331 | +            inputEl.parentElement.appendChild(errorEl); | 
|  | 332 | +          } | 
|  | 333 | +          errorEl.textContent = errorMessage; | 
|  | 334 | +        } | 
|  | 335 | + | 
| 291 | 336 |         function debounce(func, timeout = 500){ | 
| 292 | 337 |           // debounce rapid clicks to prevent unnecessary fetches | 
| 293 | 338 |           let timer; | 
|  | 
| 372 | 417 |         } | 
| 373 | 418 | 
 | 
| 374 | 419 |         async function saveChanges() { | 
|  | 420 | +          // Validate AP_Pass before saving | 
|  | 421 | +          const apPassInput = $('#AP_Pass'); | 
|  | 422 | +          if (apPassInput) { | 
|  | 423 | +            const errorMessage = validateApPassword(apPassInput.value.trim()); | 
|  | 424 | +            if (errorMessage) { | 
|  | 425 | +              showInputError(apPassInput, errorMessage); | 
|  | 426 | +              showAlert('Cannot save settings: Invalid AP password.'); | 
|  | 427 | +              return; | 
|  | 428 | +            } | 
|  | 429 | +          } | 
| 375 | 430 |           // save change and reboot | 
| 376 | 431 |           await sleep(100); | 
| 377 | 432 |           sendControl('save', 1); | 
|  | 
| 468 | 523 |             const et = event.target.type; | 
| 469 | 524 |             // input fields of given class  | 
| 470 | 525 |             if (e.nodeName == 'INPUT') { | 
| 471 |  | -              if (e.type === 'checkbox') processStatus(ID, e.id, e.checked ? 1 : 0); | 
|  | 526 | +              if (e.id === 'AP_Pass') { | 
|  | 527 | +                // Validate AP password | 
|  | 528 | +                const errorMessage = validateApPassword(value); | 
|  | 529 | +                showInputError(e, errorMessage); | 
|  | 530 | +                // Only send valid passwords | 
|  | 531 | +                if (!errorMessage) { | 
|  | 532 | +                  processStatus(ID, e.id, value); | 
|  | 533 | +                } | 
|  | 534 | +              } | 
|  | 535 | +              else if (e.type === 'checkbox') processStatus(ID, e.id, e.checked ? 1 : 0); | 
| 472 | 536 |               else if (et === 'button' || et === 'file') processStatus(ID, e.id, 1); | 
| 473 | 537 |               else if (et === 'radio') { if (e.checked) processStatus(ID, e.name, value); }  | 
| 474 | 538 |               else if (et === 'range') processStatus(ID, e.id, e.parentElement.children.rangeVal.innerHTML);  | 
|  | 
| 782 | 846 |         } | 
| 783 | 847 |       } | 
| 784 | 848 | 
 | 
| 785 |  | -      function refreshAllContainers() { | 
|  | 849 | +      function refreshAllContainers(uniq = false) { | 
| 786 | 850 |         const containers = document.querySelectorAll('.ipContainer'); | 
| 787 | 851 |         containers.forEach((container) => { | 
| 788 | 852 |           const ip = container.querySelector('.ipUrl').textContent; | 
| 789 | 853 |           const hubImg = container.querySelector('img'); | 
| 790 | 854 |           hubImg.src = `http://${ip}`; | 
|  | 855 | +          if (uniq) hubImg.src += Date.now(); | 
| 791 | 856 |         }); | 
| 792 | 857 |       } | 
| 793 | 858 | 
 | 
|  | 
| 796 | 861 |         const ipInput = document.getElementById('ipInput'); | 
| 797 | 862 |         const ipAddresses = localStorage.getItem('enteredIPs') ? JSON.parse(localStorage.getItem('enteredIPs')) : []; | 
| 798 | 863 | 
 | 
| 799 |  | -        // Add the entered IP to the array | 
|  | 864 | +        // Add the entered IP to the array if not already present | 
| 800 | 865 |         let newIP = ipInput.value.trim(); | 
| 801 |  | -        if (newIP !== '' && !ipAddresses.includes(newIP)) { | 
|  | 866 | +        if (newIP !== '' && !ipAddresses.some(item => item.includes(newIP))) { | 
| 802 | 867 |           // if only ip address, add app specific URI | 
| 803 | 868 |           // for any other app, enter full URL | 
| 804 | 869 |           if (newIP.indexOf('/') == -1) newIP += appHub; | 
|  | 
0 commit comments