feat: identity protection UI, duplicate guards, name-lock enforcement (v2.4.6)

- Add password set/change/remove with loading states and double-click prevention
- Add protected identity deletion flow (server-side password removal first)
- Add restore flow for password-protected UUIDs (verify password before saving)
- Add UUID duplicate checks in setUuidForUser (prevent accidental overwrites)
- Add name-locked error handling in launch flow (server enforces registered name)
- Sync shield icon across all identity mutation paths
- Refresh identity dropdown after all password/identity operations
- Propagate force flag through IPC for legitimate overwrites

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
sanasol
2026-02-28 18:22:40 +01:00
parent 0b861904ba
commit 7347910fe9
7 changed files with 558 additions and 73 deletions

View File

@@ -193,7 +193,8 @@ window.switchProfile = async (id) => {
};
export async function launch() {
if (isDownloading || (playBtn && playBtn.disabled)) return;
const btn = homePlayBtn || playBtn;
if (isDownloading || (btn && btn.disabled)) return;
// ==========================================================================
// STEP 1: Check launch readiness from backend (single source of truth)
@@ -271,11 +272,7 @@ export async function launch() {
// STEP 3: Start launch process
// ==========================================================================
if (window.LauncherUI) window.LauncherUI.showProgress();
isDownloading = true;
if (playBtn) {
playBtn.disabled = true;
playText.textContent = 'LAUNCHING...';
}
lockPlayButton('LAUNCHING...');
try {
const startingMsg = window.i18n ? window.i18n.t('progress.startingGame') : 'Starting game...';
@@ -285,15 +282,11 @@ export async function launch() {
// Pass playerName from config - backend will validate again
const result = await window.electronAPI.launchGame(playerName, javaPath, '', gpuPreference);
isDownloading = false;
if (window.LauncherUI) {
window.LauncherUI.hideProgress();
}
resetPlayButton();
if (result.usernameTaken) {
// Username reserved by another player
isDownloading = false;
if (window.LauncherUI) window.LauncherUI.hideProgress();
resetPlayButton();
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 {
@@ -302,9 +295,57 @@ export async function launch() {
return;
}
if (result.nameLocked) {
// UUID is password-protected and locked to a specific name
isDownloading = false;
if (window.LauncherUI) window.LauncherUI.hideProgress();
resetPlayButton();
const msg = result.registeredName
? `This UUID is locked to username "${result.registeredName}". Change your identity name to "${result.registeredName}" in Settings.`
: 'This UUID is locked to a different username. Check your identity settings.';
if (window.LauncherUI && window.LauncherUI.showError) {
window.LauncherUI.showError(msg);
} else {
showNotification(msg, 'error');
}
return;
}
if (result.passwordRequired) {
// UUID has a password — show interactive password dialog
const launchResult = await promptForPasswordAndLaunch(playerName, javaPath, gpuPreference);
// Check for saved password first
let savedPw = null;
try {
const cfg = await window.electronAPI.loadConfig();
const uuid = result.uuid || '';
savedPw = cfg && cfg.savedPasswords && cfg.savedPasswords[uuid] ? cfg.savedPasswords[uuid] : null;
} catch (e) { /* ignore */ }
if (savedPw) {
// Try saved password silently
if (window.LauncherUI) window.LauncherUI.showProgress();
lockPlayButton('LAUNCHING...');
const autoResult = await window.electronAPI.launchGameWithPassword(playerName, javaPath, '', gpuPreference, savedPw);
if (autoResult.success) {
isDownloading = false;
if (window.LauncherUI) window.LauncherUI.hideProgress();
resetPlayButton();
if (window.electronAPI.minimizeWindow) setTimeout(() => { window.electronAPI.minimizeWindow(); }, 500);
return;
}
// Saved password failed — clear it and show popup
isDownloading = false;
if (window.LauncherUI) window.LauncherUI.hideProgress();
resetPlayButton();
try {
const cfg2 = await window.electronAPI.loadConfig();
const sp = cfg2.savedPasswords || {};
delete sp[result.uuid || ''];
await window.electronAPI.saveConfig({ savedPasswords: sp });
} catch (e) { /* ignore */ }
}
// Show interactive password dialog
const launchResult = await promptForPasswordAndLaunch(playerName, javaPath, gpuPreference, result.uuid);
if (launchResult && launchResult.success) {
if (window.electronAPI.minimizeWindow) setTimeout(() => { window.electronAPI.minimizeWindow(); }, 500);
}
@@ -312,12 +353,21 @@ export async function launch() {
}
if (result.success) {
// Keep button locked so user can't double-launch
if (window.LauncherUI) window.LauncherUI.hideProgress();
lockPlayButton('GAME RUNNING');
setTimeout(() => {
resetPlayButton();
}, 10000); // Reset after 10s (game should be visible by then)
if (window.electronAPI.minimizeWindow) {
setTimeout(() => {
window.electronAPI.minimizeWindow();
}, 500);
}
} else {
isDownloading = false;
if (window.LauncherUI) window.LauncherUI.hideProgress();
resetPlayButton();
console.error('[Launcher] Launch failed:', result.error);
// Handle specific error cases
@@ -373,7 +423,7 @@ export async function launch() {
}
}
function promptForPasswordAndLaunch(playerName, javaPath, gpuPreference) {
function promptForPasswordAndLaunch(playerName, javaPath, gpuPreference, uuid) {
return new Promise((resolve) => {
// Remove any existing password prompt
const existing = document.querySelector('.custom-confirm-modal');
@@ -427,6 +477,10 @@ function promptForPasswordAndLaunch(playerName, javaPath, gpuPreference) {
font-size: 0.95rem;
outline: none;
" placeholder="Password" autofocus />
<label style="display: flex; align-items: center; gap: 8px; margin-top: 12px; cursor: pointer; color: #9ca3af; font-size: 0.85rem; user-select: none;">
<input type="checkbox" id="pwRememberCheck" style="accent-color: #f59e0b; width: 16px; height: 16px; cursor: pointer;" />
Remember password
</label>
</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="
@@ -458,6 +512,7 @@ function promptForPasswordAndLaunch(playerName, javaPath, gpuPreference) {
const confirmBtn = overlay.querySelector('#pwConfirmBtn');
const cancelBtn = overlay.querySelector('#pwCancelBtn');
const errorMsg = overlay.querySelector('#pwErrorMsg');
const rememberCheck = overlay.querySelector('#pwRememberCheck');
let busy = false;
@@ -494,15 +549,20 @@ function promptForPasswordAndLaunch(playerName, javaPath, gpuPreference) {
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...'; }
lockPlayButton('LAUNCHING...');
const result = await window.electronAPI.launchGameWithPassword(playerName, javaPath, '', gpuPreference, password);
if (result.success) {
// Save password if "Remember" checked
if (rememberCheck.checked && uuid) {
try {
const cfg = await window.electronAPI.loadConfig();
const sp = cfg.savedPasswords || {};
sp[uuid] = password;
await window.electronAPI.saveConfig({ savedPasswords: sp });
} catch (e) { /* ignore */ }
}
overlay.remove();
isDownloading = false;
if (window.LauncherUI) window.LauncherUI.hideProgress();
@@ -779,9 +839,21 @@ async function performRepair() {
function resetPlayButton() {
isDownloading = false;
if (playBtn) {
playBtn.disabled = false;
playText.textContent = window.i18n ? window.i18n.t('play.play') : 'PLAY';
const btn = homePlayBtn || playBtn;
if (btn) {
btn.disabled = false;
const textEl = btn.querySelector('span');
if (textEl) textEl.textContent = window.i18n ? window.i18n.t('play.playButton') : 'PLAY HYTALE';
}
}
function lockPlayButton(text) {
isDownloading = true;
const btn = homePlayBtn || playBtn;
if (btn) {
btn.disabled = true;
const textEl = btn.querySelector('span');
if (textEl) textEl.textContent = text || 'LAUNCHING...';
}
}