From 7347910fe973527a56178d95a26af229c8af733c Mon Sep 17 00:00:00 2001 From: sanasol Date: Sat, 28 Feb 2026 18:22:40 +0100 Subject: [PATCH] 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 --- GUI/js/launcher.js | 120 ++++++-- GUI/js/settings.js | 452 ++++++++++++++++++++++++++++--- backend/core/config.js | 24 +- backend/managers/gameLauncher.js | 10 +- main.js | 21 +- package.json | 2 +- preload.js | 2 +- 7 files changed, 558 insertions(+), 73 deletions(-) diff --git a/GUI/js/launcher.js b/GUI/js/launcher.js index d11f48a..b629a2f 100644 --- a/GUI/js/launcher.js +++ b/GUI/js/launcher.js @@ -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 /> +
+ +
+ `; + + overlay.appendChild(dialog); + document.body.appendChild(overlay); + requestAnimationFrame(() => { + overlay.style.opacity = '1'; + dialog.style.transform = 'scale(1)'; + }); + + const input = overlay.querySelector('#restorePasswordInput'); + const errorMsg = overlay.querySelector('#restoreError'); + const confirmBtn = overlay.querySelector('#restoreConfirmBtn'); + const cancelBtn = overlay.querySelector('#restoreCancelBtn'); + let busy = false; + + const close = () => { overlay.remove(); }; + cancelBtn.onclick = close; + overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); + + const doRestore = async () => { + if (busy) return; + const password = input.value.trim(); + if (!password) { + errorMsg.textContent = 'Password is required'; + errorMsg.style.display = 'block'; + input.focus(); + return; + } + + busy = true; + confirmBtn.disabled = true; + confirmBtn.textContent = 'Verifying...'; + errorMsg.style.display = 'none'; + + try { + // Use the registered name if UUID is name-locked + const finalName = registeredName || username; + + // Verify password by attempting to get tokens + const cfg = await window.electronAPI.loadConfig(); + const authDomain = cfg.authDomain || 'auth.sanasol.ws'; + const resp = await fetch(`https://${authDomain}/game-session/new`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ uuid, name: finalName, password, scopes: 'hytale:server hytale:client' }) + }); + + if (resp.status === 401 || resp.status === 429) { + const err = await resp.json(); + errorMsg.textContent = err.error || 'Incorrect password'; + errorMsg.style.display = 'block'; + input.value = ''; + input.focus(); + busy = false; + confirmBtn.disabled = false; + confirmBtn.textContent = 'Verify & Restore'; + return; + } + + if (!resp.ok) { + throw new Error(`Server returned ${resp.status}`); + } + + // Password verified — save identity locally (force to allow the name) + close(); + if (window.electronAPI && window.electronAPI.setUuidForUser) { + const result = await window.electronAPI.setUuidForUser(finalName, uuid, true); + if (result.success || (result && result.uuid)) { + showNotification(`Identity "${finalName}" restored successfully!`, 'success'); + hideAddIdentityForm(); + await loadAllUuids(); + if (window.loadIdentities) window.loadIdentities(); + updatePasswordShieldIcon(); + } else { + showNotification(result.error || 'Failed to save identity', 'error'); + } + } + } catch (e) { + errorMsg.textContent = 'Error: ' + e.message; + errorMsg.style.display = 'block'; + busy = false; + confirmBtn.disabled = false; + confirmBtn.textContent = 'Verify & Restore'; + } + }; + + confirmBtn.onclick = doRestore; + input.addEventListener('keydown', (e) => { if (e.key === 'Enter') doRestore(); }); +} + function toggleAdvancedSection() { if (!uuidAdvancedContent || !uuidAdvancedToggle) return; const isOpen = uuidAdvancedContent.style.display !== 'none'; @@ -981,10 +1194,12 @@ async function refreshPasswordStatus() { window.handleSetPassword = async function () { const newPw = document.getElementById('newPasswordInput'); const currentPw = document.getElementById('currentPasswordInput'); + const setBtn = document.getElementById('setPasswordBtn'); if (!newPw || !newPw.value || newPw.value.length < 6) { showNotification('Password must be at least 6 characters', 'error'); return; } + if (setBtn) { setBtn.disabled = true; setBtn.textContent = 'Setting...'; } try { const uuid = await window.electronAPI.getCurrentUuid(); const result = await window.electronAPI.setPlayerPassword(uuid, newPw.value, currentPw?.value || null); @@ -994,20 +1209,25 @@ window.handleSetPassword = async function () { if (currentPw) currentPw.value = ''; refreshPasswordStatus(); updatePasswordShieldIcon(); + if (window.loadIdentities) window.loadIdentities(); } else { showNotification(result.error || 'Failed to set password', 'error'); } } catch (e) { showNotification('Error: ' + e.message, 'error'); + } finally { + if (setBtn) { setBtn.disabled = false; setBtn.textContent = 'Set Password'; } } }; window.handleRemovePassword = async function () { const currentPw = document.getElementById('currentPasswordInput'); + const removeBtn = document.getElementById('removePasswordBtn'); if (!currentPw || !currentPw.value) { showNotification('Enter your current password to remove it', 'error'); return; } + if (removeBtn) { removeBtn.disabled = true; removeBtn.textContent = 'Removing...'; } try { const uuid = await window.electronAPI.getCurrentUuid(); const result = await window.electronAPI.removePlayerPassword(uuid, currentPw.value); @@ -1016,11 +1236,14 @@ window.handleRemovePassword = async function () { currentPw.value = ''; refreshPasswordStatus(); updatePasswordShieldIcon(); + if (window.loadIdentities) window.loadIdentities(); } else { showNotification(result.error || 'Failed to remove password', 'error'); } } catch (e) { showNotification('Error: ' + e.message, 'error'); + } finally { + if (removeBtn) { removeBtn.disabled = false; removeBtn.textContent = 'Remove Password'; } } }; @@ -1108,10 +1331,12 @@ window.closePasswordModal = function () { window.handlePasswordModalSet = async function () { const newPw = document.getElementById('pwModalNewPassword'); const curPw = document.getElementById('pwModalCurrentPassword'); + const setBtn = document.getElementById('pwModalSetBtn'); if (!newPw || !newPw.value || newPw.value.length < 6) { showNotification('Password must be at least 6 characters', 'error'); return; } + if (setBtn) { setBtn.disabled = true; const s = setBtn.querySelector('span'); if (s) s.textContent = 'Saving...'; } try { const uuid = await window.electronAPI.getCurrentUuid(); const result = await window.electronAPI.setPlayerPassword(uuid, newPw.value, curPw?.value || null); @@ -1121,21 +1346,26 @@ window.handlePasswordModalSet = async function () { newPw.value = ''; if (curPw) curPw.value = ''; updatePasswordShieldIcon(); + if (window.loadIdentities) window.loadIdentities(); openPasswordModal(); // refresh modal state } else { showNotification(result.error || 'Failed to set password', 'error'); } } catch (e) { showNotification('Error: ' + e.message, 'error'); + } finally { + if (setBtn) { setBtn.disabled = false; const s = setBtn.querySelector('span'); if (s) s.textContent = 'Set Password'; } } }; window.handlePasswordModalRemove = async function () { const curPw = document.getElementById('pwModalCurrentPassword'); + const removeBtn = document.getElementById('pwModalRemoveBtn'); if (!curPw || !curPw.value) { showNotification('Enter your current password to remove it', 'error'); return; } + if (removeBtn) { removeBtn.disabled = true; removeBtn.textContent = 'Removing...'; } try { const uuid = await window.electronAPI.getCurrentUuid(); const result = await window.electronAPI.removePlayerPassword(uuid, curPw.value); @@ -1143,12 +1373,15 @@ window.handlePasswordModalRemove = async function () { showNotification('Password removed', 'success'); curPw.value = ''; updatePasswordShieldIcon(); + if (window.loadIdentities) window.loadIdentities(); openPasswordModal(); // refresh modal state } else { showNotification(result.error || 'Failed to remove password', 'error'); } } catch (e) { showNotification('Error: ' + e.message, 'error'); + } finally { + if (removeBtn) { removeBtn.disabled = false; removeBtn.textContent = 'Remove Password'; } } }; @@ -1240,7 +1473,7 @@ async function performSetCustomUuid(uuid) { showNotification(msg, 'error'); return; } - const result = await window.electronAPI.setUuidForUser(username, uuid); + const result = await window.electronAPI.setUuidForUser(username, uuid, true); // force: true — explicit UUID change if (result.success) { if (currentUuidDisplay) currentUuidDisplay.value = uuid; @@ -1251,6 +1484,7 @@ async function performSetCustomUuid(uuid) { await loadAllUuids(); if (window.loadIdentities) window.loadIdentities(); + updatePasswordShieldIcon(); } else { throw new Error(result.error || 'Failed to set custom UUID'); } @@ -1329,8 +1563,9 @@ async function performSwitchToUsername(username) { // Refresh the UUID list to show new "Current" badge await loadAllUuids(); - // Refresh header identity dropdown + // Refresh header identity dropdown + shield icon if (window.loadIdentities) window.loadIdentities(); + updatePasswordShieldIcon(); const msg = window.i18n ? window.i18n.t('notifications.switchUsernameSuccess').replace('{username}', username) @@ -1348,46 +1583,195 @@ async function performSwitchToUsername(username) { window.deleteUuid = async function (username) { try { - const message = window.i18n ? window.i18n.t('confirm.deleteUuidMessage').replace('{username}', username) : `Are you sure you want to delete the UUID for "${username}"? This action cannot be undone.`; - const title = window.i18n ? window.i18n.t('confirm.deleteUuidTitle') : 'Delete UUID'; - const confirmBtn = window.i18n ? window.i18n.t('confirm.deleteUuidButton') : 'Delete'; - const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel'; + // Look up UUID for this username + let uuid = null; + if (window.electronAPI && window.electronAPI.getAllUuidMappings) { + const mappings = await window.electronAPI.getAllUuidMappings(); + const entry = mappings.find(m => m.username.toLowerCase() === username.toLowerCase()); + if (entry) uuid = entry.uuid; + } - showCustomConfirm( - message, - title, - async () => { - await performDeleteUuid(username); - }, - null, - confirmBtn, - cancelBtn - ); + // Check if password-protected + let isProtected = false; + if (uuid && window.electronAPI && window.electronAPI.checkPasswordStatus) { + try { + const pwStatus = await window.electronAPI.checkPasswordStatus(uuid); + isProtected = pwStatus && pwStatus.hasPassword; + } catch (e) { + console.log('[Identity] Password status check failed:', e.message); + } + } + + if (isProtected) { + // Password-protected identity — show warning with password input + showPasswordProtectedDeleteDialog(username, uuid); + } else { + // Normal identity — simple confirm + const message = `Are you sure you want to delete the identity "${username}"? This action cannot be undone.`; + showCustomConfirm( + message, + 'Delete Identity', + async () => { await performDeleteUuid(username); }, + null, + 'Delete', + 'Cancel' + ); + } } catch (error) { console.error('Error in deleteUuid:', error); - const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteFailed') : 'Failed to delete UUID'; - showNotification(msg, 'error'); + showNotification('Failed to delete identity', 'error'); } }; +function showPasswordProtectedDeleteDialog(username, uuid) { + 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; + opacity: 0; transition: opacity 0.3s ease; + `; + + const dialog = document.createElement('div'); + dialog.style.cssText = ` + background: #1f2937; border-radius: 12px; padding: 0; + min-width: 420px; max-width: 520px; + box-shadow: 0 20px 40px rgba(0,0,0,0.6); + border: 1px solid rgba(239, 68, 68, 0.4); + transform: scale(0.9); transition: transform 0.3s ease; + `; + + dialog.innerHTML = ` +
+
+ +

Delete Protected Identity

+
+
+
+

+ "${escapeHtml(username)}" is password-protected. Deleting it will: +

+
    +
  • Remove the password protection from this UUID
  • +
  • Release the reserved username "${escapeHtml(username)}"
  • +
  • Allow anyone to use this UUID and name
  • +
+

+ Enter your current password to confirm deletion: +

+ + +
+
+ + +
+ `; + + overlay.appendChild(dialog); + document.body.appendChild(overlay); + requestAnimationFrame(() => { + overlay.style.opacity = '1'; + dialog.style.transform = 'scale(1)'; + }); + + const input = overlay.querySelector('#pwDeleteInput'); + const errorMsg = overlay.querySelector('#pwDeleteError'); + const confirmBtn = overlay.querySelector('#pwDeleteConfirmBtn'); + const cancelBtn = overlay.querySelector('#pwDeleteCancelBtn'); + let busy = false; + + const close = () => { overlay.remove(); }; + + cancelBtn.onclick = close; + overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); + + const doDelete = async () => { + if (busy) return; + const password = input.value.trim(); + if (!password) { + errorMsg.textContent = 'Password is required'; + errorMsg.style.display = 'block'; + input.focus(); + return; + } + + busy = true; + confirmBtn.disabled = true; + confirmBtn.textContent = 'Removing...'; + errorMsg.style.display = 'none'; + + try { + // Step 1: Remove password on server (validates current password) + const removeResult = await window.electronAPI.removePlayerPassword(uuid, password); + if (!removeResult.success) { + errorMsg.textContent = removeResult.error || 'Incorrect password'; + errorMsg.style.display = 'block'; + input.value = ''; + input.focus(); + busy = false; + confirmBtn.disabled = false; + confirmBtn.textContent = 'Delete & Remove Password'; + return; + } + + // Step 2: Also clear saved password if any + try { + const cfg = await window.electronAPI.loadConfig(); + if (cfg.savedPasswords && cfg.savedPasswords[uuid]) { + delete cfg.savedPasswords[uuid]; + await window.electronAPI.saveConfig({ savedPasswords: cfg.savedPasswords }); + } + } catch (e) { /* ignore */ } + + // Step 3: Delete identity locally + close(); + await performDeleteUuid(username); + } catch (e) { + errorMsg.textContent = 'Error: ' + e.message; + errorMsg.style.display = 'block'; + busy = false; + confirmBtn.disabled = false; + confirmBtn.textContent = 'Delete & Remove Password'; + } + }; + + confirmBtn.onclick = doDelete; + input.addEventListener('keydown', (e) => { if (e.key === 'Enter') doDelete(); }); +} + async function performDeleteUuid(username) { try { if (window.electronAPI && window.electronAPI.deleteUuidForUser) { const result = await window.electronAPI.deleteUuidForUser(username); if (result.success) { - const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteSuccess') : 'UUID deleted successfully!'; - showNotification(msg, 'success'); + showNotification('Identity deleted successfully!', 'success'); await loadAllUuids(); if (window.loadIdentities) window.loadIdentities(); + updatePasswordShieldIcon(); } else { - throw new Error(result.error || 'Failed to delete UUID'); + throw new Error(result.error || 'Failed to delete identity'); } } } catch (error) { console.error('Error deleting UUID:', error); - const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteFailed').replace('{error}', error.message) : `Failed to delete UUID: ${error.message}`; - showNotification(msg, 'error'); + showNotification(`Failed to delete identity: ${error.message}`, 'error'); } } diff --git a/backend/core/config.js b/backend/core/config.js index b89ca8d..45ecc33 100644 --- a/backend/core/config.js +++ b/backend/core/config.js @@ -529,7 +529,7 @@ function getAllUuidMappingsArray() { * Validates UUID format before saving * Preserves original case of username */ -function setUuidForUser(username, uuid) { +function setUuidForUser(username, uuid, { force = false } = {}) { const { validate: validateUuid } = require('uuid'); if (!username || typeof username !== 'string' || !username.trim()) { @@ -543,15 +543,29 @@ function setUuidForUser(username, uuid) { const displayName = username.trim(); const normalizedLookup = displayName.toLowerCase(); - // 1. Update UUID store (source of truth) + // 1. Check for existing entries — reject overwrite unless forced migrateUuidStoreIfNeeded(); const uuidStore = loadUuidStore(); const storeKey = Object.keys(uuidStore).find(k => k.toLowerCase() === normalizedLookup); + if (storeKey && uuidStore[storeKey] !== uuid && !force) { + console.log(`[Config] Rejected UUID overwrite for "${displayName}": existing ${uuidStore[storeKey]}, attempted ${uuid}`); + return { success: false, error: 'duplicate', existingUuid: uuidStore[storeKey] }; + } + // Check if UUID already used by a different name + if (!force) { + const existingByUuid = Object.entries(uuidStore).find(([k, v]) => v.toLowerCase() === uuid.toLowerCase() && k.toLowerCase() !== normalizedLookup); + if (existingByUuid) { + console.log(`[Config] Rejected duplicate UUID for "${displayName}": UUID ${uuid} already used by "${existingByUuid[0]}"`); + return { success: false, error: 'uuid_in_use', existingUsername: existingByUuid[0] }; + } + } + + // 2. Update UUID store (source of truth) if (storeKey) delete uuidStore[storeKey]; uuidStore[displayName] = uuid; saveUuidStore(uuidStore); - // 2. Update config.json (backward compat) + // 3. Update config.json (backward compat) const config = loadConfig(); const userUuids = config.userUuids || {}; const existingKey = Object.keys(userUuids).find(k => k.toLowerCase() === normalizedLookup); @@ -560,7 +574,7 @@ function setUuidForUser(username, uuid) { saveConfig({ userUuids }); console.log(`[Config] UUID set for "${displayName}": ${uuid}`); - return uuid; + return { success: true, uuid }; } /** @@ -619,7 +633,7 @@ function resetCurrentUserUuid() { const { v4: uuidv4 } = require('uuid'); const newUuid = uuidv4(); - return setUuidForUser(username, newUuid); + return setUuidForUser(username, newUuid, { force: true }); } // ============================================================================= diff --git a/backend/managers/gameLauncher.js b/backend/managers/gameLauncher.js index 1618253..a4a4308 100644 --- a/backend/managers/gameLauncher.js +++ b/backend/managers/gameLauncher.js @@ -82,6 +82,12 @@ async function fetchAuthTokens(uuid, name, password) { err.usernameTaken = true; throw err; } + if (response.status === 403 && errBody.name_locked) { + const err = new Error(`This UUID is locked to username "${errBody.registeredName}". Change your identity name to match.`); + err.nameLocked = true; + err.registeredName = errBody.registeredName; + throw err; + } throw new Error(`Auth server returned ${response.status}`); } @@ -123,7 +129,7 @@ async function fetchAuthTokens(uuid, name, password) { return { identityToken, sessionToken }; } catch (error) { // Re-throw authentication errors — must not fall back to local tokens - if (error.passwordRequired || error.lockedOut || error.usernameTaken) { + if (error.passwordRequired || error.lockedOut || error.usernameTaken || error.nameLocked) { throw error; } console.error('Failed to fetch auth tokens:', error.message); @@ -694,7 +700,7 @@ async function launchGameWithVersionCheck(playerNameOverride = null, progressCal progressCallback(`Error: ${error.message}`, -1, null, null, null); } // Re-throw authentication errors so IPC handler can return proper flags - if (error.passwordRequired || error.lockedOut || error.usernameTaken) { + if (error.passwordRequired || error.lockedOut || error.usernameTaken || error.nameLocked) { throw error; } // Always return an error response instead of throwing diff --git a/main.js b/main.js index 77d53ff..efc7cfa 100644 --- a/main.js +++ b/main.js @@ -532,10 +532,10 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, g // Check if UUID has password before launching let launchOptions = {}; + const { getAuthServerUrl, getUuidForUser } = require('./backend/core/config'); + const launchUuid = getUuidForUser(playerName); try { - const { getAuthServerUrl } = require('./backend/core/config'); - const { getUuidForUser } = require('./backend/core/config'); - const uuid = getUuidForUser(playerName); + const uuid = launchUuid; const authServerUrl = getAuthServerUrl(); const statusResp = await fetch(`${authServerUrl}/player/password/status/${uuid}`); if (statusResp.ok) { @@ -574,7 +574,7 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, g } if (error.passwordRequired) { - return { success: false, passwordRequired: true, error: 'Password required' }; + return { success: false, passwordRequired: true, uuid: launchUuid, error: 'Password required' }; } if (error.lockedOut) { return { success: false, error: 'Too many failed attempts. Try again in ' + Math.ceil((error.lockoutSeconds || 900) / 60) + ' minutes.' }; @@ -582,6 +582,9 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, g if (error.usernameTaken) { return { success: false, usernameTaken: true, error: errorMessage }; } + if (error.nameLocked) { + return { success: false, nameLocked: true, registeredName: error.registeredName, error: error.message }; + } return { success: false, error: errorMessage }; } @@ -625,6 +628,9 @@ ipcMain.handle('launch-game-with-password', async (event, playerName, javaPath, if (error.usernameTaken) { return { success: false, usernameTaken: true, error: error.message || error.toString() }; } + if (error.nameLocked) { + return { success: false, nameLocked: true, registeredName: error.registeredName, error: error.message }; + } return { success: false, error: error.message || error.toString() }; } }); @@ -1423,9 +1429,12 @@ ipcMain.handle('get-all-uuid-mappings', async () => { } }); -ipcMain.handle('set-uuid-for-user', async (event, username, uuid) => { +ipcMain.handle('set-uuid-for-user', async (event, username, uuid, force) => { try { - await setUuidForUser(username, uuid); + const result = setUuidForUser(username, uuid, { force: !!force }); + if (result && result.success === false) { + return result; // { success: false, error: 'duplicate', existingUuid } + } return { success: true }; } catch (error) { console.error('Error setting UUID for user:', error); diff --git a/package.json b/package.json index b7bd436..4307d70 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hytale-f2p-launcher", - "version": "2.4.5", + "version": "2.4.6", "description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support", "homepage": "https://git.sanhost.net/sanasol/hytale-f2p", "main": "main.js", diff --git a/preload.js b/preload.js index dbd7eb8..211dd48 100644 --- a/preload.js +++ b/preload.js @@ -103,7 +103,7 @@ contextBridge.exposeInMainWorld('electronAPI', { // UUID Management methods getCurrentUuid: () => ipcRenderer.invoke('get-current-uuid'), getAllUuidMappings: () => ipcRenderer.invoke('get-all-uuid-mappings'), - setUuidForUser: (username, uuid) => ipcRenderer.invoke('set-uuid-for-user', username, uuid), + setUuidForUser: (username, uuid, force) => ipcRenderer.invoke('set-uuid-for-user', username, uuid, force), generateNewUuid: () => ipcRenderer.invoke('generate-new-uuid'), deleteUuidForUser: (username) => ipcRenderer.invoke('delete-uuid-for-user', username), resetCurrentUserUuid: () => ipcRenderer.invoke('reset-current-user-uuid'),