feat: add password protection UI and fix launch flow

- Password management UI in settings (set/change/remove password)
- Shield icon on play button for protected identities
- Interactive password popup on launch with inline error display
- Fix: re-throw password errors instead of falling to local tokens
- Fix: password popup properly cleans up on success/cancel
- Fix: expose updatePasswordShieldIcon for cross-module access

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
sanasol
2026-02-28 16:45:46 +01:00
parent ee53911a06
commit 0b861904ba
7 changed files with 809 additions and 17 deletions

View File

@@ -941,6 +941,230 @@ function toggleAdvancedSection() {
}
}
// Password section toggle
function togglePasswordSection() {
const content = document.getElementById('passwordSectionContent');
const toggle = document.getElementById('passwordSectionToggle');
if (!content || !toggle) return;
const isOpen = content.style.display !== 'none';
content.style.display = isOpen ? 'none' : 'block';
const chevron = toggle.querySelector('.uuid-advanced-chevron');
if (chevron) chevron.classList.toggle('open', !isOpen);
if (!isOpen) refreshPasswordStatus();
}
async function refreshPasswordStatus() {
const statusMsg = document.getElementById('passwordStatusMsg');
const currentPwInput = document.getElementById('currentPasswordInput');
const removeBtn = document.getElementById('removePasswordBtn');
const setBtn = document.getElementById('setPasswordBtn');
try {
const uuid = await window.electronAPI?.getCurrentUuid();
if (!uuid) { if (statusMsg) statusMsg.textContent = 'No UUID available'; return; }
const result = await window.electronAPI.checkPasswordStatus(uuid);
if (result && result.hasPassword) {
if (statusMsg) statusMsg.innerHTML = '<i class="fas fa-lock" style="color:#22c55e"></i> Password is set for this UUID';
if (currentPwInput) currentPwInput.style.display = '';
if (removeBtn) removeBtn.style.display = '';
if (setBtn) { const span = setBtn.querySelector('span'); if (span) span.textContent = 'Change Password'; }
} else {
if (statusMsg) statusMsg.innerHTML = '<i class="fas fa-unlock" style="color:#f59e0b"></i> No password set — anyone can use this UUID';
if (currentPwInput) currentPwInput.style.display = 'none';
if (removeBtn) removeBtn.style.display = 'none';
if (setBtn) { const span = setBtn.querySelector('span'); if (span) span.textContent = 'Set Password'; }
}
} catch (e) {
if (statusMsg) statusMsg.textContent = 'Could not check password status';
}
}
window.handleSetPassword = async function () {
const newPw = document.getElementById('newPasswordInput');
const currentPw = document.getElementById('currentPasswordInput');
if (!newPw || !newPw.value || newPw.value.length < 6) {
showNotification('Password must be at least 6 characters', 'error');
return;
}
try {
const uuid = await window.electronAPI.getCurrentUuid();
const result = await window.electronAPI.setPlayerPassword(uuid, newPw.value, currentPw?.value || null);
if (result.success) {
showNotification('Password set successfully', 'success');
newPw.value = '';
if (currentPw) currentPw.value = '';
refreshPasswordStatus();
updatePasswordShieldIcon();
} else {
showNotification(result.error || 'Failed to set password', 'error');
}
} catch (e) {
showNotification('Error: ' + e.message, 'error');
}
};
window.handleRemovePassword = async function () {
const currentPw = document.getElementById('currentPasswordInput');
if (!currentPw || !currentPw.value) {
showNotification('Enter your current password to remove it', 'error');
return;
}
try {
const uuid = await window.electronAPI.getCurrentUuid();
const result = await window.electronAPI.removePlayerPassword(uuid, currentPw.value);
if (result.success) {
showNotification('Password removed', 'success');
currentPw.value = '';
refreshPasswordStatus();
updatePasswordShieldIcon();
} else {
showNotification(result.error || 'Failed to remove password', 'error');
}
} catch (e) {
showNotification('Error: ' + e.message, 'error');
}
};
// ─── Password Shield Icon ───
window.updatePasswordShieldIcon = updatePasswordShieldIcon;
async function updatePasswordShieldIcon() {
const icon = document.getElementById('passwordShieldIcon');
if (!icon) return;
try {
const uuid = await window.electronAPI?.getCurrentUuid();
if (!uuid) {
icon.className = 'fas fa-unlock password-shield unprotected';
icon.setAttribute('data-tooltip', 'No identity loaded');
return;
}
const result = await window.electronAPI.checkPasswordStatus(uuid);
if (result && result.hasPassword) {
icon.className = 'fas fa-lock password-shield protected';
icon.setAttribute('data-tooltip', 'Protected — click to manage');
} else {
icon.className = 'fas fa-unlock password-shield unprotected';
icon.setAttribute('data-tooltip', 'Click to protect identity');
}
} catch (e) {
icon.className = 'fas fa-unlock password-shield unprotected';
icon.setAttribute('data-tooltip', 'Click to protect identity');
}
}
// ─── Password Modal ───
window.openPasswordModal = async function () {
const modal = document.getElementById('passwordModal');
if (!modal) return;
modal.style.display = 'flex';
// Clear inputs
const newPw = document.getElementById('pwModalNewPassword');
const curPw = document.getElementById('pwModalCurrentPassword');
if (newPw) newPw.value = '';
if (curPw) curPw.value = '';
// Load current identity info
try {
const username = await window.electronAPI?.loadUsername() || 'Player';
const uuid = await window.electronAPI?.getCurrentUuid() || '';
document.getElementById('pwModalName').textContent = username;
document.getElementById('pwModalUuid').textContent = uuid || 'No UUID';
// Check password status
const result = uuid ? await window.electronAPI.checkPasswordStatus(uuid) : null;
const badge = document.getElementById('pwModalStatusBadge');
const statusText = document.getElementById('pwModalStatusText');
const curPwInput = document.getElementById('pwModalCurrentPassword');
const removeBtn = document.getElementById('pwModalRemoveBtn');
const setBtn = document.getElementById('pwModalSetBtn');
const usernameInfo = document.getElementById('pwModalUsernameInfo');
if (result && result.hasPassword) {
badge.innerHTML = '<span style="color:#22c55e;font-size:0.8em;padding:3px 8px;background:rgba(34,197,94,0.15);border-radius:6px;"><i class="fas fa-lock"></i> Protected</span>';
statusText.innerHTML = '<i class="fas fa-check-circle" style="color:#22c55e"></i> Password set — your UUID and username are protected';
if (curPwInput) curPwInput.style.display = '';
if (removeBtn) removeBtn.style.display = '';
if (setBtn) { const s = setBtn.querySelector('span'); if (s) s.textContent = 'Change Password'; }
if (usernameInfo) usernameInfo.style.display = 'none';
} else {
badge.innerHTML = '<span style="color:#f59e0b;font-size:0.8em;padding:3px 8px;background:rgba(245,158,11,0.1);border-radius:6px;"><i class="fas fa-unlock"></i> Open</span>';
statusText.innerHTML = '<i class="fas fa-exclamation-triangle" style="color:#f59e0b"></i> Anyone can use this UUID and username';
if (curPwInput) curPwInput.style.display = 'none';
if (removeBtn) removeBtn.style.display = 'none';
if (setBtn) { const s = setBtn.querySelector('span'); if (s) s.textContent = 'Set Password'; }
if (usernameInfo) usernameInfo.style.display = '';
}
} catch (e) {
document.getElementById('pwModalStatusText').textContent = 'Could not check password status';
}
};
window.closePasswordModal = function () {
const modal = document.getElementById('passwordModal');
if (modal) modal.style.display = 'none';
};
window.handlePasswordModalSet = async function () {
const newPw = document.getElementById('pwModalNewPassword');
const curPw = document.getElementById('pwModalCurrentPassword');
if (!newPw || !newPw.value || newPw.value.length < 6) {
showNotification('Password must be at least 6 characters', 'error');
return;
}
try {
const uuid = await window.electronAPI.getCurrentUuid();
const result = await window.electronAPI.setPlayerPassword(uuid, newPw.value, curPw?.value || null);
if (result.success) {
const msg = result.username_reserved ? 'Password set! Username "' + (result.reserved_username || '') + '" reserved.' : 'Password set!';
showNotification(msg, 'success');
newPw.value = '';
if (curPw) curPw.value = '';
updatePasswordShieldIcon();
openPasswordModal(); // refresh modal state
} else {
showNotification(result.error || 'Failed to set password', 'error');
}
} catch (e) {
showNotification('Error: ' + e.message, 'error');
}
};
window.handlePasswordModalRemove = async function () {
const curPw = document.getElementById('pwModalCurrentPassword');
if (!curPw || !curPw.value) {
showNotification('Enter your current password to remove it', 'error');
return;
}
try {
const uuid = await window.electronAPI.getCurrentUuid();
const result = await window.electronAPI.removePlayerPassword(uuid, curPw.value);
if (result.success) {
showNotification('Password removed', 'success');
curPw.value = '';
updatePasswordShieldIcon();
openPasswordModal(); // refresh modal state
} else {
showNotification(result.error || 'Failed to remove password', 'error');
}
} catch (e) {
showNotification('Error: ' + e.message, 'error');
}
};
// Close modal on backdrop click
document.addEventListener('click', (e) => {
const modal = document.getElementById('passwordModal');
if (modal && e.target === modal) closePasswordModal();
});
// Bind password section toggle (for legacy UUID modal section)
document.addEventListener('DOMContentLoaded', () => {
const pwToggle = document.getElementById('passwordSectionToggle');
if (pwToggle) pwToggle.addEventListener('click', togglePasswordSection);
updatePasswordShieldIcon();
});
window.regenerateUuidForUser = async function (username) {
try {
const message = window.i18n ? window.i18n.t('confirm.regenerateUuidMessage') : 'Are you sure you want to generate a new UUID? This will change your player identity.';