diff --git a/GUI/index.html b/GUI/index.html index 65314c9..d4bcbaa 100644 --- a/GUI/index.html +++ b/GUI/index.html @@ -77,6 +77,7 @@
@@ -830,6 +831,41 @@

+ +
+ + +
@@ -861,6 +897,59 @@ + + +
diff --git a/GUI/js/launcher.js b/GUI/js/launcher.js index ea89530..d11f48a 100644 --- a/GUI/js/launcher.js +++ b/GUI/js/launcher.js @@ -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 = ` +
+
+ +

Password Required

+
+
+
+

This identity is password-protected. Enter your password to continue.

+ + +
+
+ + +
+ `; + + 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 + ? '' + : ''; return ` -
${safe} - ${m.username === currentUsername ? '' : ''} + ${pwBadge} + ${isActive ? '' : ''}
`; }).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); } diff --git a/GUI/js/settings.js b/GUI/js/settings.js index 811a917..be008da 100644 --- a/GUI/js/settings.js +++ b/GUI/js/settings.js @@ -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 = ' 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 = ' 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 = ' Protected'; + statusText.innerHTML = ' 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 = ' Open'; + statusText.innerHTML = ' 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.'; diff --git a/GUI/style.css b/GUI/style.css index 5e7e569..fc5f18f 100644 --- a/GUI/style.css +++ b/GUI/style.css @@ -6368,6 +6368,79 @@ input[type="text"].uuid-input, color: #22c55e; } +.identity-btn .password-shield { + position: relative; + font-size: 0.8rem; + padding: 4px 6px; + border-radius: 6px; + transition: all 0.2s ease; + cursor: pointer; + z-index: 10; +} + +.identity-btn .password-shield.unprotected { + color: #f59e0b; + background: rgba(245, 158, 11, 0.15); + border: 1px solid rgba(245, 158, 11, 0.3); + animation: shieldPulse 2s ease-in-out infinite; +} + +.identity-btn .password-shield.protected { + color: #22c55e; + background: rgba(34, 197, 94, 0.1); + border: 1px solid rgba(34, 197, 94, 0.2); + animation: none; +} + +.identity-btn .password-shield:hover { + transform: scale(1.15); + filter: brightness(1.3); +} + +.identity-btn .password-shield::after { + content: attr(data-tooltip); + position: absolute; + bottom: calc(100% + 8px); + left: 50%; + transform: translateX(-50%); + background: rgba(0, 0, 0, 0.9); + color: #fff; + padding: 6px 10px; + border-radius: 6px; + font-size: 0.7rem; + font-weight: 400; + white-space: nowrap; + pointer-events: none; + opacity: 0; + transition: opacity 0.15s ease; + border: 1px solid rgba(255,255,255,0.1); +} + +.identity-btn .password-shield:hover::after { + opacity: 1; +} + +@keyframes shieldPulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +/* Password status in identity dropdown */ +.identity-item .pw-badge { + font-size: 0.65rem; + margin-left: auto; + padding: 1px 5px; + border-radius: 4px; +} +.identity-item .pw-badge.locked { + color: #22c55e; + background: rgba(34, 197, 94, 0.15); +} +.identity-item .pw-badge.unlocked { + color: #f59e0b; + background: rgba(245, 158, 11, 0.1); +} + .identity-dropdown { position: absolute; top: 100%; diff --git a/backend/managers/gameLauncher.js b/backend/managers/gameLauncher.js index 39c5e32..1618253 100644 --- a/backend/managers/gameLauncher.js +++ b/backend/managers/gameLauncher.js @@ -43,24 +43,45 @@ try { const execAsync = promisify(exec); // Fetch tokens from the auth server (properly signed with server's Ed25519 key) -async function fetchAuthTokens(uuid, name) { +async function fetchAuthTokens(uuid, name, password) { const authServerUrl = getAuthServerUrl(); try { console.log(`Fetching auth tokens from ${authServerUrl}/game-session/child`); + const bodyData = { + uuid: uuid, + name: name, + scopes: ['hytale:server', 'hytale:client'] + }; + if (password) bodyData.password = password; + const response = await fetch(`${authServerUrl}/game-session/child`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - uuid: uuid, - name: name, - scopes: ['hytale:server', 'hytale:client'] - }) + body: JSON.stringify(bodyData) }); if (!response.ok) { + const errBody = await response.json().catch(() => ({})); + if (response.status === 401 && errBody.password_required) { + const err = new Error('Password required'); + err.passwordRequired = true; + err.attemptsRemaining = errBody.attemptsRemaining; + throw err; + } + if (response.status === 429) { + const err = new Error('Too many failed attempts. Try again later.'); + err.lockedOut = true; + err.lockoutSeconds = errBody.lockoutSeconds; + throw err; + } + if (response.status === 403 && errBody.username_taken) { + const err = new Error('This username is reserved by another player who has set a password. Please use a different name.'); + err.usernameTaken = true; + throw err; + } throw new Error(`Auth server returned ${response.status}`); } @@ -77,10 +98,12 @@ async function fetchAuthTokens(uuid, name) { if (payload.username && payload.username !== name && name !== 'Player') { console.warn(`[Auth] Token username mismatch: token has "${payload.username}", expected "${name}". Retrying...`); // Retry once with explicit name + const retryBody = { uuid: uuid, name: name, scopes: ['hytale:server', 'hytale:client'] }; + if (password) retryBody.password = password; const retryResponse = await fetch(`${authServerUrl}/game-session/child`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ uuid: uuid, name: name, scopes: ['hytale:server', 'hytale:client'] }) + body: JSON.stringify(retryBody) }); if (retryResponse.ok) { const retryData = await retryResponse.json(); @@ -99,6 +122,10 @@ async function fetchAuthTokens(uuid, name) { console.log('Auth tokens received from server'); return { identityToken, sessionToken }; } catch (error) { + // Re-throw authentication errors — must not fall back to local tokens + if (error.passwordRequired || error.lockedOut || error.usernameTaken) { + throw error; + } console.error('Failed to fetch auth tokens:', error.message); // Fallback to local generation if server unavailable return generateLocalTokens(uuid, name); @@ -147,7 +174,7 @@ function generateLocalTokens(uuid, name) { }; } -async function launchGame(playerNameOverride = null, progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) { +async function launchGame(playerNameOverride = null, progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null, options = {}) { // ========================================================================== // CACHE INVALIDATION: Clear proxyClient module cache to force fresh .env load // This prevents stale cached values from affecting multiple launch attempts @@ -256,11 +283,12 @@ async function launchGame(playerNameOverride = null, progressCallback, javaPathO const uuid = getUuidForUser(playerName); console.log(`[Launcher] UUID for "${playerName}": ${uuid} (verify this stays constant across launches)`); - // Fetch tokens from auth server + // Fetch tokens from auth server (with password if provided) if (progressCallback) { progressCallback('Fetching authentication tokens...', null, null, null, null); } - const { identityToken, sessionToken } = await fetchAuthTokens(uuid, playerName); + const launchPassword = options?.password || null; + const { identityToken, sessionToken } = await fetchAuthTokens(uuid, playerName, launchPassword); // Patch client and server binaries to use custom auth server (BEFORE signing on macOS) // FORCE patch on every launch to ensure consistency @@ -578,7 +606,7 @@ async function launchGame(playerNameOverride = null, progressCallback, javaPathO } } -async function launchGameWithVersionCheck(playerNameOverride = null, progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) { +async function launchGameWithVersionCheck(playerNameOverride = null, progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null, options = {}) { try { // ========================================================================== // PRE-LAUNCH VALIDATION: Check username is configured @@ -651,7 +679,7 @@ async function launchGameWithVersionCheck(playerNameOverride = null, progressCal progressCallback('Launching game...', 80, null, null, null); } - const launchResult = await launchGame(playerNameOverride, progressCallback, javaPathOverride, installPathOverride, gpuPreference, branch); + const launchResult = await launchGame(playerNameOverride, progressCallback, javaPathOverride, installPathOverride, gpuPreference, branch, options); // Ensure we always return a result if (!launchResult) { @@ -665,6 +693,10 @@ async function launchGameWithVersionCheck(playerNameOverride = null, progressCal if (progressCallback) { 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) { + throw error; + } // Always return an error response instead of throwing return { success: false, error: error.message || 'Unknown launch error' }; } diff --git a/main.js b/main.js index 6b258b0..77d53ff 100644 --- a/main.js +++ b/main.js @@ -530,7 +530,26 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, g } }; - const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference); + // Check if UUID has password before launching + let launchOptions = {}; + try { + const { getAuthServerUrl } = require('./backend/core/config'); + const { getUuidForUser } = require('./backend/core/config'); + const uuid = getUuidForUser(playerName); + const authServerUrl = getAuthServerUrl(); + const statusResp = await fetch(`${authServerUrl}/player/password/status/${uuid}`); + if (statusResp.ok) { + const status = await statusResp.json(); + if (status.hasPassword) { + // Return to renderer to prompt for password + return { success: false, passwordRequired: true, uuid }; + } + } + } catch (pwErr) { + console.log('[Launch] Password check skipped:', pwErr.message); + } + + const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference, null, launchOptions); if (result.success && result.launched) { const closeOnStart = loadCloseLauncherOnStart(); @@ -554,10 +573,62 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, g }, 2000); } + if (error.passwordRequired) { + return { success: false, passwordRequired: true, error: 'Password required' }; + } + if (error.lockedOut) { + return { success: false, error: 'Too many failed attempts. Try again in ' + Math.ceil((error.lockoutSeconds || 900) / 60) + ' minutes.' }; + } + if (error.usernameTaken) { + return { success: false, usernameTaken: true, error: errorMessage }; + } + return { success: false, error: errorMessage }; } }); +ipcMain.handle('launch-game-with-password', async (event, playerName, javaPath, installPath, gpuPreference, password) => { + try { + const progressCallback = (message, percent, speed, downloaded, total, retryState) => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('progress-update', { + message: message || null, + percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null, + speed: speed !== null && speed !== undefined ? speed : null, + downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null, + total: total !== null && total !== undefined ? total : null, + retryState: retryState || null + }); + } + }; + + const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference, null, { password }); + + if (result.success && result.launched) { + const closeOnStart = loadCloseLauncherOnStart(); + if (closeOnStart) { + setTimeout(() => { app.quit(); }, 1000); + } + } + return result; + } catch (error) { + console.error('Launch with password error:', error); + if (mainWindow && !mainWindow.isDestroyed()) { + setTimeout(() => { mainWindow.webContents.send('progress-complete'); }, 2000); + } + if (error.passwordRequired) { + return { success: false, passwordRequired: true, error: 'Incorrect password. ' + (error.attemptsRemaining != null ? error.attemptsRemaining + ' attempts remaining.' : '') }; + } + if (error.lockedOut) { + return { success: false, error: 'Too many failed attempts. Try again in ' + Math.ceil((error.lockoutSeconds || 900) / 60) + ' minutes.' }; + } + if (error.usernameTaken) { + return { success: false, usernameTaken: true, error: error.message || error.toString() }; + } + return { success: false, error: error.message || error.toString() }; + } +}); + ipcMain.handle('install-game', async (event, playerName, javaPath, installPath, branch) => { try { console.log(`[IPC] install-game called with parameters:`); @@ -1391,6 +1462,88 @@ ipcMain.handle('reset-current-user-uuid', async () => { } }); +// Password Management IPC handlers +ipcMain.handle('check-password-status', async (event, uuid) => { + try { + const { getAuthServerUrl } = require('./backend/core/config'); + const authServerUrl = getAuthServerUrl(); + const response = await fetch(`${authServerUrl}/player/password/status/${uuid}`); + if (!response.ok) return { hasPassword: false }; + return await response.json(); + } catch (error) { + console.error('Error checking password status:', error); + return { hasPassword: false, error: error.message }; + } +}); + +ipcMain.handle('set-player-password', async (event, uuid, password, currentPassword) => { + try { + const { getAuthServerUrl } = require('./backend/core/config'); + const { getUuidForUser, loadUsername } = require('./backend/core/config'); + const authServerUrl = getAuthServerUrl(); + // First get a bearer token for auth + const name = loadUsername() || 'Player'; + const tokenResp = await fetch(`${authServerUrl}/game-session/child`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ uuid, name, password: currentPassword || undefined }) + }); + if (!tokenResp.ok) { + const err = await tokenResp.json().catch(() => ({})); + return { success: false, error: err.error || 'Failed to authenticate' }; + } + const tokenData = await tokenResp.json(); + const bearerToken = tokenData.identityToken || tokenData.IdentityToken; + + const response = await fetch(`${authServerUrl}/player/password/set`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${bearerToken}` + }, + body: JSON.stringify({ uuid, password, currentPassword: currentPassword || undefined }) + }); + return await response.json(); + } catch (error) { + console.error('Error setting password:', error); + return { success: false, error: error.message }; + } +}); + +ipcMain.handle('remove-player-password', async (event, uuid, currentPassword) => { + try { + const { getAuthServerUrl } = require('./backend/core/config'); + const { loadUsername } = require('./backend/core/config'); + const authServerUrl = getAuthServerUrl(); + const name = loadUsername() || 'Player'; + // Get bearer token with current password + const tokenResp = await fetch(`${authServerUrl}/game-session/child`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ uuid, name, password: currentPassword }) + }); + if (!tokenResp.ok) { + const err = await tokenResp.json().catch(() => ({})); + return { success: false, error: err.error || 'Failed to authenticate' }; + } + const tokenData = await tokenResp.json(); + const bearerToken = tokenData.identityToken || tokenData.IdentityToken; + + const response = await fetch(`${authServerUrl}/player/password/remove`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${bearerToken}` + }, + body: JSON.stringify({ uuid, currentPassword }) + }); + return await response.json(); + } catch (error) { + console.error('Error removing password:', error); + return { success: false, error: error.message }; + } +}); + ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => { try { const logDir = logger.getLogDirectory(); diff --git a/preload.js b/preload.js index d8d2e18..dbd7eb8 100644 --- a/preload.js +++ b/preload.js @@ -2,6 +2,7 @@ const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('electronAPI', { launchGame: (playerName, javaPath, installPath, gpuPreference) => ipcRenderer.invoke('launch-game', playerName, javaPath, installPath, gpuPreference), + launchGameWithPassword: (playerName, javaPath, installPath, gpuPreference, password) => ipcRenderer.invoke('launch-game-with-password', playerName, javaPath, installPath, gpuPreference, password), installGame: (playerName, javaPath, installPath, branch) => ipcRenderer.invoke('install-game', playerName, javaPath, installPath, branch), closeWindow: () => ipcRenderer.invoke('window-close'), minimizeWindow: () => ipcRenderer.invoke('window-minimize'), @@ -107,6 +108,15 @@ contextBridge.exposeInMainWorld('electronAPI', { deleteUuidForUser: (username) => ipcRenderer.invoke('delete-uuid-for-user', username), resetCurrentUserUuid: () => ipcRenderer.invoke('reset-current-user-uuid'), + // Password Management methods + checkPasswordStatus: (uuid) => ipcRenderer.invoke('check-password-status', uuid), + setPlayerPassword: (uuid, password, currentPassword) => ipcRenderer.invoke('set-player-password', uuid, password, currentPassword), + removePlayerPassword: (uuid, currentPassword) => ipcRenderer.invoke('remove-player-password', uuid, currentPassword), + promptPassword: () => ipcRenderer.invoke('prompt-password'), + onPasswordPrompt: (callback) => { + ipcRenderer.on('show-password-prompt', (event, data) => callback(data)); + }, + // Java Wrapper Config API loadWrapperConfig: () => ipcRenderer.invoke('load-wrapper-config'), saveWrapperConfig: (config) => ipcRenderer.invoke('save-wrapper-config', config),