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

@@ -292,6 +292,25 @@ export async function launch() {
}
resetPlayButton();
if (result.usernameTaken) {
// Username reserved by another player
if (window.LauncherUI && window.LauncherUI.showError) {
window.LauncherUI.showError('This username is reserved by another player. Please change your player name in Identity settings.');
} else {
showNotification('This username is reserved by another player. Please change your player name.', 'error');
}
return;
}
if (result.passwordRequired) {
// UUID has a password — show interactive password dialog
const launchResult = await promptForPasswordAndLaunch(playerName, javaPath, gpuPreference);
if (launchResult && launchResult.success) {
if (window.electronAPI.minimizeWindow) setTimeout(() => { window.electronAPI.minimizeWindow(); }, 500);
}
return;
}
if (result.success) {
if (window.electronAPI.minimizeWindow) {
setTimeout(() => {
@@ -354,6 +373,177 @@ export async function launch() {
}
}
function promptForPasswordAndLaunch(playerName, javaPath, gpuPreference) {
return new Promise((resolve) => {
// Remove any existing password prompt
const existing = document.querySelector('.custom-confirm-modal');
if (existing) existing.remove();
const overlay = document.createElement('div');
overlay.className = 'custom-confirm-modal';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(4px);
z-index: 20000;
display: flex;
align-items: center;
justify-content: center;
`;
const dialog = document.createElement('div');
dialog.style.cssText = `
background: #1f2937;
border-radius: 12px;
padding: 0;
min-width: 380px;
max-width: 420px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
`;
dialog.innerHTML = `
<div style="padding: 20px 24px; border-bottom: 1px solid rgba(255,255,255,0.1);">
<div style="display: flex; align-items: center; gap: 10px; color: #f59e0b;">
<i class="fas fa-lock" style="font-size: 20px;"></i>
<h3 style="margin: 0; font-size: 1.1rem; font-weight: 600; color: #e5e7eb;">Password Required</h3>
</div>
</div>
<div style="padding: 20px 24px;">
<p style="margin: 0 0 12px 0; color: #9ca3af; font-size: 0.9rem; line-height: 1.5;">This identity is password-protected. Enter your password to continue.</p>
<div id="pwErrorMsg" style="display: none; margin-bottom: 12px; padding: 8px 12px; background: rgba(239,68,68,0.15); border: 1px solid rgba(239,68,68,0.3); border-radius: 6px; color: #f87171; font-size: 0.85rem;"></div>
<input type="password" id="launchPasswordInput" style="
width: 100%;
box-sizing: border-box;
padding: 10px 14px;
background: rgba(0,0,0,0.3);
border: 1px solid rgba(255,255,255,0.15);
border-radius: 8px;
color: #e5e7eb;
font-size: 0.95rem;
outline: none;
" placeholder="Password" autofocus />
</div>
<div style="padding: 16px 24px; display: flex; gap: 10px; justify-content: flex-end; border-top: 1px solid rgba(255,255,255,0.1);">
<button id="pwCancelBtn" style="
background: transparent;
color: #9ca3af;
border: 1px solid rgba(156,163,175,0.3);
padding: 9px 18px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
">Cancel</button>
<button id="pwConfirmBtn" style="
background: #f59e0b;
color: #000;
border: none;
padding: 9px 18px;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 0.9rem;
">Login</button>
</div>
`;
overlay.appendChild(dialog);
document.body.appendChild(overlay);
const input = overlay.querySelector('#launchPasswordInput');
const confirmBtn = overlay.querySelector('#pwConfirmBtn');
const cancelBtn = overlay.querySelector('#pwCancelBtn');
const errorMsg = overlay.querySelector('#pwErrorMsg');
let busy = false;
const close = (result) => {
overlay.remove();
isDownloading = false;
if (window.LauncherUI) window.LauncherUI.hideProgress();
resetPlayButton();
resolve(result);
};
const showError = (msg) => {
errorMsg.textContent = msg;
errorMsg.style.display = 'block';
input.style.borderColor = 'rgba(239,68,68,0.5)';
input.value = '';
input.focus();
};
const tryLogin = async () => {
const password = input.value;
if (!password) {
showError('Please enter your password.');
return;
}
if (busy) return;
busy = true;
// Show loading state
confirmBtn.disabled = true;
confirmBtn.textContent = 'Logging in...';
errorMsg.style.display = 'none';
input.style.borderColor = 'rgba(255,255,255,0.15)';
try {
if (window.LauncherUI) window.LauncherUI.showProgress();
isDownloading = true;
const playBtn = document.getElementById('play-btn');
const playText = playBtn?.querySelector('.play-text');
if (playBtn) { playBtn.disabled = true; }
if (playText) { playText.textContent = 'LAUNCHING...'; }
const result = await window.electronAPI.launchGameWithPassword(playerName, javaPath, '', gpuPreference, password);
if (result.success) {
overlay.remove();
isDownloading = false;
if (window.LauncherUI) window.LauncherUI.hideProgress();
resetPlayButton();
resolve(result);
return;
}
// Wrong password
if (result.passwordRequired) {
showError(result.error || 'Incorrect password. Please try again.');
} else {
showError(result.error || 'Launch failed.');
}
isDownloading = false;
if (window.LauncherUI) window.LauncherUI.hideProgress();
resetPlayButton();
} catch (err) {
showError(err.message || 'An error occurred.');
isDownloading = false;
if (window.LauncherUI) window.LauncherUI.hideProgress();
resetPlayButton();
} finally {
busy = false;
confirmBtn.disabled = false;
confirmBtn.textContent = 'Login';
}
};
confirmBtn.addEventListener('click', tryLogin);
cancelBtn.addEventListener('click', () => close(null));
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') tryLogin();
if (e.key === 'Escape') close(null);
});
setTimeout(() => input.focus(), 100);
});
}
function showCustomConfirm(message, title, onConfirm, onCancel = null, confirmText, cancelText) {
// Apply defaults with i18n support
title = title || (window.i18n ? window.i18n.t('confirm.defaultTitle') : 'Confirm Action');
@@ -712,7 +902,7 @@ async function loadIdentities() {
}
}
function renderIdentityList(mappings, currentUsername) {
async function renderIdentityList(mappings, currentUsername) {
const list = document.getElementById('identityList');
if (!list) return;
@@ -721,13 +911,31 @@ function renderIdentityList(mappings, currentUsername) {
return;
}
list.innerHTML = mappings.map(m => {
// Check password status for all identities in parallel
const statusChecks = mappings.map(async m => {
try {
if (m.uuid && window.electronAPI?.checkPasswordStatus) {
const s = await window.electronAPI.checkPasswordStatus(m.uuid);
return s?.hasPassword || false;
}
} catch {}
return false;
});
const statuses = await Promise.all(statusChecks);
list.innerHTML = mappings.map((m, i) => {
const safe = escapeHtml(m.username);
const isActive = m.username === currentUsername;
const hasPassword = statuses[i];
const pwBadge = hasPassword
? '<span class="pw-badge locked"><i class="fas fa-lock"></i></span>'
: '<span class="pw-badge unlocked"><i class="fas fa-unlock"></i></span>';
return `
<div class="identity-item ${m.username === currentUsername ? 'active' : ''}"
<div class="identity-item ${isActive ? 'active' : ''}"
onclick="switchIdentity('${safe.replace(/'/g, "&#39;")}')">
<span>${safe}</span>
${m.username === currentUsername ? '<i class="fas fa-check ml-auto"></i>' : ''}
${pwBadge}
${isActive ? '<i class="fas fa-check" style="margin-left:4px;"></i>' : ''}
</div>
`;
}).join('');
@@ -774,6 +982,9 @@ window.switchIdentity = async (username) => {
if (settingsInput) settingsInput.value = username;
if (window.loadCurrentUuid) window.loadCurrentUuid();
// Update password shield icon for new identity
if (window.updatePasswordShieldIcon) window.updatePasswordShieldIcon();
} catch (error) {
console.error('Failed to switch identity:', error);
}