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 @@
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),