From d194a8f0dfc10de426b1664f73c2c421b8877427 Mon Sep 17 00:00:00 2001 From: Eliezer Croitoru Date: Fri, 2 Oct 2026 04:38:35 +0300 Subject: [PATCH] 1 --- app.js | 207 +++++++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 152 +++++++++++++++++++++++++++++++++++++++ style.css | 31 ++++++++ 3 files changed, 390 insertions(+) create mode 100644 app.js create mode 100644 index.html create mode 100644 style.css diff --git a/app.js b/app.js new file mode 100644 index 0000000..d27070f --- /dev/null +++ b/app.js @@ -0,0 +1,207 @@ +document.addEventListener('DOMContentLoaded', () => { + // DOM Elements + const form = document.getElementById('urlForm'); + const hostnameInput = document.getElementById('hostname'); + const domainSelect = document.getElementById('domain'); + const portInput = document.getElementById('port'); + const usernameInput = document.getElementById('username'); + const passwordInput = document.getElementById('password'); + const includeAuthCheck = document.getElementById('includeAuthCheck'); + const authStatusText = document.getElementById('authStatusText'); + const credentialsFields = document.getElementById('credentialsFields'); + const preventSaveCheck = document.getElementById('preventPasswordSave'); + const togglePasswordBtn = document.getElementById('togglePasswordVisibility'); + const hostnameFeedback = document.getElementById('hostnameFeedback'); + const urlDisplay = document.getElementById('urlDisplay'); + const launchBtn = document.getElementById('launchBtn'); + const copyBtn = document.getElementById('copyBtn'); + const copyBtnText = document.getElementById('copyBtnText'); + const autoOpenNotice = document.getElementById('autoOpenNotice'); + + // Validation patterns: IPv4, Router Serial (alphanumeric 10-14 chars), and FQDN + const serialRegex = /^[a-zA-Z0-9]{10,14}$/; + const ipv4Regex = /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/; + const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/; + + let currentWinboxUri = ''; + + // Validate and categorize user input + function validateHostname(raw) { + let clean = raw.trim(); + let port = null; + + // Extract port if typed directly into host field (e.g., 10.0.0.1:8292) + const portMatch = clean.match(/:(\d{1,5})$/); + if (portMatch) { + port = portMatch[1]; + clean = clean.replace(/:\d{1,5}$/, ''); + } + + if (!clean) { + hostnameFeedback.innerHTML = ''; + return { type: 'empty', value: '', port }; + } + + if (ipv4Regex.test(clean)) { + hostnameFeedback.innerHTML = 'IPv4 Address'; + return { type: 'ipv4', value: clean, port }; + } + + if (serialRegex.test(clean)) { + hostnameFeedback.innerHTML = 'Router Serial'; + return { type: 'serial', value: clean, port }; + } + + if (domainRegex.test(clean)) { + hostnameFeedback.innerHTML = 'Domain / FQDN'; + return { type: 'domain', value: clean, port }; + } + + hostnameFeedback.innerHTML = 'Unrecognized Format'; + return { type: 'raw', value: clean, port }; + } + + // Convert router serial into MikroTik cloud DDNS or VPN hostname + function resolveHost(value, type) { + if (type === 'serial') { + return domainSelect.value === 'vpn' + ? `${value.toLowerCase()}.mt.vpn.sn` + : `${value.toLowerCase()}.sn.mynetname.net`; + } + return value; + } + + // Build the winbox:// URI and update the UI + function updateUi() { + const val = validateHostname(hostnameInput.value); + if (val.port) { + portInput.value = val.port; + } + + const host = resolveHost(val.value, val.type); + const port = portInput.value.trim(); + const includeAuth = includeAuthCheck.checked; + const username = includeAuth ? usernameInput.value.trim() : ''; + const password = includeAuth ? passwordInput.value : ''; + + if (!host) { + currentWinboxUri = ''; + urlDisplay.textContent = 'winbox://'; + launchBtn.disabled = true; + launchBtn.classList.add('opacity-50', 'cursor-not-allowed'); + return; + } + + launchBtn.disabled = false; + launchBtn.classList.remove('opacity-50', 'cursor-not-allowed'); + + let uri = 'winbox://'; + let displayUri = 'winbox://'; + + if (username && password) { + uri += `${encodeURIComponent(username)}:${encodeURIComponent(password)}@`; + displayUri += `${encodeURIComponent(username)}:••••••••@`; + } else if (username) { + uri += `${encodeURIComponent(username)}@`; + displayUri += `${encodeURIComponent(username)}@`; + } + + uri += host; + displayUri += host; + + if (port && port !== '8291') { + uri += `:${port}`; + displayUri += `:${port}`; + } + + currentWinboxUri = uri; + urlDisplay.textContent = displayUri; + } + + // Launch action + function openWinbox() { + if (!currentWinboxUri) return; + window.location.href = currentWinboxUri; + } + + // Form submission + form.addEventListener('submit', (e) => { + e.preventDefault(); + openWinbox(); + }); + + // Copy to clipboard + copyBtn.addEventListener('click', async () => { + if (!currentWinboxUri) return; + try { + await navigator.clipboard.writeText(currentWinboxUri); + copyBtnText.textContent = 'Copied!'; + setTimeout(() => { copyBtnText.textContent = 'Copy URI'; }, 1800); + } catch { + prompt('Copy WinBox URI:', currentWinboxUri); + } + }); + + // Password visibility toggle + togglePasswordBtn.addEventListener('click', () => { + const isPassword = passwordInput.type === 'password'; + passwordInput.type = isPassword ? 'text' : 'password'; + togglePasswordBtn.innerHTML = isPassword + ? `` + : ``; + }); + + // Toggle credentials block on/off + includeAuthCheck.addEventListener('change', () => { + const enabled = includeAuthCheck.checked; + authStatusText.textContent = enabled ? 'Include' : 'Omit'; + credentialsFields.classList.toggle('disabled-state', !enabled); + updateUi(); + }); + + // Disallow browser password save prompt + preventSaveCheck.addEventListener('change', () => { + if (preventSaveCheck.checked) { + passwordInput.setAttribute('autocomplete', 'new-password'); + usernameInput.setAttribute('autocomplete', 'off'); + } else { + passwordInput.setAttribute('autocomplete', 'current-password'); + usernameInput.setAttribute('autocomplete', 'username'); + } + }); + + // Handle incoming URL parameters (e.g., ?hostname=192.168.88.1&user=admin&autoclose=1) + function handleQueryParams() { + const params = new URLSearchParams(window.location.search); + const hostParam = params.get('hostname') || params.get('host'); + if (!hostParam) return; + + hostnameInput.value = hostParam; + if (params.get('port')) portInput.value = params.get('port'); + if (params.get('username') || params.get('user')) usernameInput.value = params.get('username') || params.get('user'); + if (params.get('password') || params.get('pass')) passwordInput.value = params.get('password') || params.get('pass'); + if (params.get('domain')) domainSelect.value = params.get('domain'); + + updateUi(); + + if (currentWinboxUri) { + openWinbox(); + autoOpenNotice.classList.remove('hidden'); + + if (params.get('autoclose') === '1') { + setTimeout(() => { + window.close(); + }, 500); + } + } + } + + // Input listeners + [hostnameInput, domainSelect, portInput, usernameInput, passwordInput].forEach(el => { + el.addEventListener('input', updateUi); + }); + + // Initialization + updateUi(); + handleQueryParams(); +}); diff --git a/index.html b/index.html new file mode 100644 index 0000000..9dd5af9 --- /dev/null +++ b/index.html @@ -0,0 +1,152 @@ + + + + + + MikroTik WinBox URL Launcher + + + + + + + +
+ +
+
+
+ M +
+
+

MikroTik URL Generator

+

Launch WinBox sessions directly from URI

+
+
+ + + + + Installer + +
+ +
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+
+ Authentication + +
+ +
+
+ + +
+ +
+ +
+ + +
+
+
+ +
+ + +
+
+ + +
+
+ Generated URI + +
+
+ winbox:// +
+
+ + +
+ +
+
+ + + +
+ + + + + diff --git a/style.css b/style.css new file mode 100644 index 0000000..92dbc2e --- /dev/null +++ b/style.css @@ -0,0 +1,31 @@ +/* Additional custom styles & overrides */ +:root { + color-scheme: light; +} + +/* Ensure monospace fonts render consistently across OS platforms */ +#urlDisplay { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + letter-spacing: -0.01em; +} + +/* Smooth fade and disabled states for credentials card */ +#credentialsFields.disabled-state { + opacity: 0.35; + pointer-events: none; + user-select: none; +} + +/* Focus outline refinements for form elements */ +input:focus, select:focus { + outline: none; +} + +/* Eye button toggle animation */ +#togglePasswordVisibility svg { + transition: transform 0.15s ease-in-out; +} + +#togglePasswordVisibility:active svg { + transform: scale(0.9); +}