d07005bde3
Addresses two Major findings from PR review: - Auth lockout no longer griefable: the throttle key is namespaced per path and includes the client IP (web:<session>:<ip> vs desktop:<session>:<ip>), so a flood of bad web-client auths can neither lock other clients of the same session nor block the desktop from (re)authenticating. Per-socket failure close and brute-force lockout are preserved. - Relay web client XSS eliminated: app.html tab/macro rendering rebuilt with createElement/textContent (no macro/tab value reaches innerHTML); inline scripts/handlers externalized to /static/app.js and /static/login.js so the helmet CSP now uses script-src 'self' (dropped 'unsafe-inline' for scripts). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
92 lines
3.0 KiB
JavaScript
92 lines
3.0 KiB
JavaScript
// MacroPad login page logic (externalized so the CSP can drop 'unsafe-inline'
|
|
// for scripts).
|
|
const sessionId = window.location.pathname.split('/')[1];
|
|
const form = document.getElementById('loginForm');
|
|
const passwordInput = document.getElementById('password');
|
|
const rememberCheckbox = document.getElementById('remember');
|
|
const submitBtn = document.getElementById('submitBtn');
|
|
const errorDiv = document.getElementById('error');
|
|
const statusDiv = document.getElementById('status');
|
|
|
|
let desktopConnected = false;
|
|
|
|
// Check for saved password (session first, then "remembered" store)
|
|
const savedPassword = sessionStorage.getItem(`macropad_${sessionId}`)
|
|
|| localStorage.getItem(`macropad_${sessionId}`);
|
|
if (savedPassword) {
|
|
passwordInput.value = savedPassword;
|
|
}
|
|
|
|
// Connect to WebSocket to check desktop status
|
|
function checkStatus() {
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const ws = new WebSocket(`${protocol}//${window.location.host}/${sessionId}/ws`);
|
|
|
|
ws.onmessage = (event) => {
|
|
const data = JSON.parse(event.data);
|
|
if (data.type === 'desktop_status') {
|
|
desktopConnected = data.status === 'connected';
|
|
updateStatus();
|
|
}
|
|
};
|
|
|
|
ws.onerror = () => {
|
|
statusDiv.textContent = 'Connection error';
|
|
statusDiv.className = 'status disconnected';
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
setTimeout(checkStatus, 5000);
|
|
};
|
|
}
|
|
|
|
function updateStatus() {
|
|
if (desktopConnected) {
|
|
statusDiv.textContent = 'Desktop connected';
|
|
statusDiv.className = 'status connected';
|
|
submitBtn.disabled = false;
|
|
} else {
|
|
statusDiv.textContent = 'Desktop not connected';
|
|
statusDiv.className = 'status disconnected';
|
|
submitBtn.disabled = true;
|
|
}
|
|
}
|
|
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
errorDiv.style.display = 'none';
|
|
|
|
const password = passwordInput.value;
|
|
|
|
try {
|
|
// Test password with a simple API call
|
|
const response = await fetch(`/${sessionId}/api/tabs`, {
|
|
headers: {
|
|
'X-MacroPad-Password': password
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Hand the credential to the app via storage (never the URL).
|
|
sessionStorage.setItem(`macropad_${sessionId}`, password);
|
|
if (rememberCheckbox.checked) {
|
|
localStorage.setItem(`macropad_${sessionId}`, password);
|
|
} else {
|
|
localStorage.removeItem(`macropad_${sessionId}`);
|
|
}
|
|
|
|
// Redirect to the PWA (credential is read from storage).
|
|
window.location.href = `/${sessionId}/app`;
|
|
} else {
|
|
const data = await response.json();
|
|
errorDiv.textContent = data.error || 'Invalid password';
|
|
errorDiv.style.display = 'block';
|
|
}
|
|
} catch (error) {
|
|
errorDiv.textContent = 'Connection failed';
|
|
errorDiv.style.display = 'block';
|
|
}
|
|
});
|
|
|
|
checkStatus();
|