208 lines
8.7 KiB
JavaScript
208 lines
8.7 KiB
JavaScript
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 = '<span class="text-emerald-600 bg-emerald-50 px-2 py-0.5 rounded border border-emerald-200">IPv4 Address</span>';
|
|
return { type: 'ipv4', value: clean, port };
|
|
}
|
|
|
|
if (serialRegex.test(clean)) {
|
|
hostnameFeedback.innerHTML = '<span class="text-blue-600 bg-blue-50 px-2 py-0.5 rounded border border-blue-200">Router Serial</span>';
|
|
return { type: 'serial', value: clean, port };
|
|
}
|
|
|
|
if (domainRegex.test(clean)) {
|
|
hostnameFeedback.innerHTML = '<span class="text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded border border-indigo-200">Domain / FQDN</span>';
|
|
return { type: 'domain', value: clean, port };
|
|
}
|
|
|
|
hostnameFeedback.innerHTML = '<span class="text-amber-600 bg-amber-50 px-2 py-0.5 rounded border border-amber-200">Unrecognized Format</span>';
|
|
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
|
|
? `<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l18 18"/></svg>`
|
|
: `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>`;
|
|
});
|
|
|
|
// 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();
|
|
});
|