Compare commits

..

4 Commits

Author SHA1 Message Date
sanasol
7347910fe9 feat: identity protection UI, duplicate guards, name-lock enforcement (v2.4.6)
- Add password set/change/remove with loading states and double-click prevention
- Add protected identity deletion flow (server-side password removal first)
- Add restore flow for password-protected UUIDs (verify password before saving)
- Add UUID duplicate checks in setUuidForUser (prevent accidental overwrites)
- Add name-locked error handling in launch flow (server enforces registered name)
- Sync shield icon across all identity mutation paths
- Refresh identity dropdown after all password/identity operations
- Propagate force flag through IPC for legitimate overwrites

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 18:22:40 +01:00
sanasol
0b861904ba feat: add password protection UI and fix launch flow
- Password management UI in settings (set/change/remove password)
- Shield icon on play button for protected identities
- Interactive password popup on launch with inline error display
- Fix: re-throw password errors instead of falling to local tokens
- Fix: password popup properly cleans up on success/cancel
- Fix: expose updatePasswordShieldIcon for cross-module access

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 16:45:46 +01:00
sanasol
ee53911a06 Revert debug builds, update fastutil issue docs
Agent and -Xshare:off both ruled out as causes.
Restored normal agent injection. Updated docs with
complete findings — issue remains unsolved.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 23:15:01 +01:00
sanasol
43a2b6d004 debug: disable DualAuth agent to test fastutil classloader issue
Temporarily skip -javaagent injection to determine if agent's
appendToBootstrapClassLoaderSearch() causes the fastutil
ClassNotFoundException on affected Windows systems.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 22:47:46 +01:00
10 changed files with 1390 additions and 157 deletions

View File

@@ -77,6 +77,7 @@
<button class="identity-btn" onclick="toggleIdentityDropdown()">
<i class="fas fa-id-badge"></i>
<span id="currentIdentityName">Player</span>
<i id="passwordShieldIcon" class="fas fa-unlock password-shield unprotected" data-tooltip="Click to protect identity" onclick="event.stopPropagation(); openPasswordModal()"></i>
<i class="fas fa-chevron-down"></i>
</button>
<div class="identity-dropdown" id="identityDropdown">
@@ -830,6 +831,41 @@
</p>
</div>
</div>
<div class="uuid-advanced-section" style="margin-top: 12px;">
<button id="passwordSectionToggle" class="uuid-advanced-toggle">
<i class="fas fa-chevron-right uuid-advanced-chevron"></i>
<span>Password Protection</span>
</button>
<div id="passwordSectionContent" class="uuid-advanced-content" style="display: none;">
<h3 class="uuid-section-title">Protect Your Identity</h3>
<p class="uuid-custom-hint" style="margin-bottom: 12px;">
<i class="fas fa-shield-alt"></i>
<span>Set a password to prevent others from using your UUID</span>
</p>
<div id="passwordStatusMsg" class="uuid-custom-hint" style="margin-bottom: 8px; color: #93a3b8;"></div>
<div id="passwordSetForm">
<div class="uuid-custom-form" style="margin-bottom: 8px;">
<input type="password" id="currentPasswordInput" class="uuid-input"
placeholder="Current password" style="display: none;" />
</div>
<div class="uuid-custom-form" style="margin-bottom: 8px;">
<input type="password" id="newPasswordInput" class="uuid-input"
placeholder="New password (min 6 chars)" />
</div>
<div class="uuid-custom-form">
<button id="setPasswordBtn" class="uuid-set-btn" onclick="handleSetPassword()">
<i class="fas fa-lock"></i>
<span>Set Password</span>
</button>
<button id="removePasswordBtn" class="uuid-cancel-btn" onclick="handleRemovePassword()" style="display: none;">
<i class="fas fa-unlock"></i>
<span>Remove Password</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -861,6 +897,59 @@
</div>
</div>
<!-- Password Protection Modal -->
<div id="passwordModal" class="uuid-modal" style="display: none;">
<div class="uuid-modal-content" style="max-width: 420px;">
<div class="uuid-modal-header">
<h2 class="uuid-modal-title">
<i class="fas fa-shield-alt mr-2"></i>
<span>Identity Protection</span>
</h2>
<button class="modal-close-btn" onclick="closePasswordModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="uuid-modal-body" style="padding: 16px 20px;">
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 16px; padding: 12px; background: rgba(255,255,255,0.03); border-radius: 8px; border: 1px solid rgba(255,255,255,0.06);">
<i class="fas fa-user" style="color: #22c55e; font-size: 1.2em;"></i>
<div style="flex:1; min-width:0;">
<div id="pwModalName" style="font-weight: 600; font-size: 1em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">Player</div>
<div id="pwModalUuid" style="font-size: 0.7em; color: #6b7280; font-family: monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"></div>
</div>
<div id="pwModalStatusBadge" style="flex-shrink:0;"></div>
</div>
<div id="pwModalStatusText" style="margin-bottom: 14px; font-size: 0.85em; color: #93a3b8; text-align: center;"></div>
<div id="pwModalSetForm">
<div style="margin-bottom: 10px;">
<input type="password" id="pwModalCurrentPassword" class="uuid-input"
placeholder="Current password" style="display: none; width: 100%;" />
</div>
<div style="margin-bottom: 10px;">
<input type="password" id="pwModalNewPassword" class="uuid-input"
placeholder="New password (min 6 chars)" style="width: 100%;" />
</div>
<div style="display: flex; gap: 8px;">
<button id="pwModalSetBtn" class="uuid-set-btn" style="flex:1;" onclick="handlePasswordModalSet()">
<i class="fas fa-lock"></i>
<span>Set Password</span>
</button>
<button id="pwModalRemoveBtn" class="uuid-cancel-btn" style="display: none;" onclick="handlePasswordModalRemove()">
<i class="fas fa-unlock"></i>
<span>Remove</span>
</button>
</div>
</div>
<div id="pwModalUsernameInfo" style="margin-top: 14px; padding: 10px; background: rgba(34,197,94,0.06); border-radius: 6px; border: 1px solid rgba(34,197,94,0.15); font-size: 0.8em; color: #93a3b8; display: none;">
<i class="fas fa-info-circle" style="color: #22c55e;"></i>
<span>Setting a password also reserves your username — no one else can use it.</span>
</div>
</div>
</div>
</div>
<div class="version-display-bottom">
<i class="fas fa-code-branch"></i>
<span id="launcherVersion"></span>

View File

@@ -193,7 +193,8 @@ window.switchProfile = async (id) => {
};
export async function launch() {
if (isDownloading || (playBtn && playBtn.disabled)) return;
const btn = homePlayBtn || playBtn;
if (isDownloading || (btn && btn.disabled)) return;
// ==========================================================================
// STEP 1: Check launch readiness from backend (single source of truth)
@@ -271,11 +272,7 @@ export async function launch() {
// STEP 3: Start launch process
// ==========================================================================
if (window.LauncherUI) window.LauncherUI.showProgress();
isDownloading = true;
if (playBtn) {
playBtn.disabled = true;
playText.textContent = 'LAUNCHING...';
}
lockPlayButton('LAUNCHING...');
try {
const startingMsg = window.i18n ? window.i18n.t('progress.startingGame') : 'Starting game...';
@@ -285,20 +282,92 @@ 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();
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 {
showNotification('This username is reserved by another player. Please change your player name.', 'error');
}
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) {
// 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);
}
return;
}
resetPlayButton();
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
@@ -354,6 +423,187 @@ export async function launch() {
}
}
function promptForPasswordAndLaunch(playerName, javaPath, gpuPreference, uuid) {
return new Promise((resolve) => {
// Remove any existing password prompt
const existing = document.querySelector('.custom-confirm-modal');
if (existing) existing.remove();
const overlay = document.createElement('div');
overlay.className = 'custom-confirm-modal';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(4px);
z-index: 20000;
display: flex;
align-items: center;
justify-content: center;
`;
const dialog = document.createElement('div');
dialog.style.cssText = `
background: #1f2937;
border-radius: 12px;
padding: 0;
min-width: 380px;
max-width: 420px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
`;
dialog.innerHTML = `
<div style="padding: 20px 24px; border-bottom: 1px solid rgba(255,255,255,0.1);">
<div style="display: flex; align-items: center; gap: 10px; color: #f59e0b;">
<i class="fas fa-lock" style="font-size: 20px;"></i>
<h3 style="margin: 0; font-size: 1.1rem; font-weight: 600; color: #e5e7eb;">Password Required</h3>
</div>
</div>
<div style="padding: 20px 24px;">
<p style="margin: 0 0 12px 0; color: #9ca3af; font-size: 0.9rem; line-height: 1.5;">This identity is password-protected. Enter your password to continue.</p>
<div id="pwErrorMsg" style="display: none; margin-bottom: 12px; padding: 8px 12px; background: rgba(239,68,68,0.15); border: 1px solid rgba(239,68,68,0.3); border-radius: 6px; color: #f87171; font-size: 0.85rem;"></div>
<input type="password" id="launchPasswordInput" style="
width: 100%;
box-sizing: border-box;
padding: 10px 14px;
background: rgba(0,0,0,0.3);
border: 1px solid rgba(255,255,255,0.15);
border-radius: 8px;
color: #e5e7eb;
font-size: 0.95rem;
outline: none;
" placeholder="Password" autofocus />
<label style="display: flex; align-items: center; gap: 8px; margin-top: 12px; cursor: pointer; color: #9ca3af; font-size: 0.85rem; user-select: none;">
<input type="checkbox" id="pwRememberCheck" style="accent-color: #f59e0b; width: 16px; height: 16px; cursor: pointer;" />
Remember password
</label>
</div>
<div style="padding: 16px 24px; display: flex; gap: 10px; justify-content: flex-end; border-top: 1px solid rgba(255,255,255,0.1);">
<button id="pwCancelBtn" style="
background: transparent;
color: #9ca3af;
border: 1px solid rgba(156,163,175,0.3);
padding: 9px 18px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
">Cancel</button>
<button id="pwConfirmBtn" style="
background: #f59e0b;
color: #000;
border: none;
padding: 9px 18px;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 0.9rem;
">Login</button>
</div>
`;
overlay.appendChild(dialog);
document.body.appendChild(overlay);
const input = overlay.querySelector('#launchPasswordInput');
const confirmBtn = overlay.querySelector('#pwConfirmBtn');
const cancelBtn = overlay.querySelector('#pwCancelBtn');
const errorMsg = overlay.querySelector('#pwErrorMsg');
const rememberCheck = overlay.querySelector('#pwRememberCheck');
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();
lockPlayButton('LAUNCHING...');
const result = await window.electronAPI.launchGameWithPassword(playerName, javaPath, '', gpuPreference, password);
if (result.success) {
// Save password if "Remember" checked
if (rememberCheck.checked && uuid) {
try {
const cfg = await window.electronAPI.loadConfig();
const sp = cfg.savedPasswords || {};
sp[uuid] = password;
await window.electronAPI.saveConfig({ savedPasswords: sp });
} catch (e) { /* ignore */ }
}
overlay.remove();
isDownloading = false;
if (window.LauncherUI) window.LauncherUI.hideProgress();
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');
@@ -589,9 +839,21 @@ async function performRepair() {
function resetPlayButton() {
isDownloading = false;
if (playBtn) {
playBtn.disabled = false;
playText.textContent = window.i18n ? window.i18n.t('play.play') : 'PLAY';
const btn = homePlayBtn || playBtn;
if (btn) {
btn.disabled = false;
const textEl = btn.querySelector('span');
if (textEl) textEl.textContent = window.i18n ? window.i18n.t('play.playButton') : 'PLAY HYTALE';
}
}
function lockPlayButton(text) {
isDownloading = true;
const btn = homePlayBtn || playBtn;
if (btn) {
btn.disabled = true;
const textEl = btn.querySelector('span');
if (textEl) textEl.textContent = text || 'LAUNCHING...';
}
}
@@ -712,7 +974,7 @@ async function loadIdentities() {
}
}
function renderIdentityList(mappings, currentUsername) {
async function renderIdentityList(mappings, currentUsername) {
const list = document.getElementById('identityList');
if (!list) return;
@@ -721,13 +983,31 @@ function renderIdentityList(mappings, currentUsername) {
return;
}
list.innerHTML = mappings.map(m => {
// Check password status for all identities in parallel
const statusChecks = mappings.map(async m => {
try {
if (m.uuid && window.electronAPI?.checkPasswordStatus) {
const s = await window.electronAPI.checkPasswordStatus(m.uuid);
return s?.hasPassword || false;
}
} catch {}
return false;
});
const statuses = await Promise.all(statusChecks);
list.innerHTML = mappings.map((m, i) => {
const safe = escapeHtml(m.username);
const isActive = m.username === currentUsername;
const hasPassword = statuses[i];
const pwBadge = hasPassword
? '<span class="pw-badge locked"><i class="fas fa-lock"></i></span>'
: '<span class="pw-badge unlocked"><i class="fas fa-unlock"></i></span>';
return `
<div class="identity-item ${m.username === currentUsername ? 'active' : ''}"
<div class="identity-item ${isActive ? 'active' : ''}"
onclick="switchIdentity('${safe.replace(/'/g, "&#39;")}')">
<span>${safe}</span>
${m.username === currentUsername ? '<i class="fas fa-check ml-auto"></i>' : ''}
${pwBadge}
${isActive ? '<i class="fas fa-check" style="margin-left:4px;"></i>' : ''}
</div>
`;
}).join('');
@@ -774,6 +1054,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);
}

View File

@@ -515,8 +515,9 @@ async function savePlayerName() {
// Also refresh the UUID list to update which entry is marked as current
await loadAllUuids();
// Refresh header identity dropdown
// Refresh header identity dropdown + shield icon
if (window.loadIdentities) window.loadIdentities();
updatePasswordShieldIcon();
} catch (error) {
console.error('Error saving player name:', error);
@@ -746,6 +747,7 @@ async function performRegenerateUuid() {
await loadAllUuids();
}
if (window.loadIdentities) window.loadIdentities();
updatePasswordShieldIcon();
} else {
throw new Error(result.error || 'Failed to generate new UUID');
}
@@ -912,18 +914,61 @@ async function confirmAddIdentity() {
return;
}
if (window.electronAPI && window.electronAPI.setUuidForUser) {
const result = await window.electronAPI.setUuidForUser(username, uuid);
if (result.success) {
const msg = window.i18n ? window.i18n.t('notifications.identityAdded') : 'Identity added successfully!';
showNotification(msg, 'success');
hideAddIdentityForm();
await loadAllUuids();
if (window.loadIdentities) window.loadIdentities();
} else {
throw new Error(result.error || 'Failed to add identity');
// Check if name already exists locally
if (window.electronAPI && window.electronAPI.getAllUuidMappings) {
const mappings = await window.electronAPI.getAllUuidMappings();
const existing = mappings.find(m => m.username.toLowerCase() === username.toLowerCase());
if (existing) {
showNotification(`Identity "${existing.username}" already exists (UUID: ${existing.uuid.substring(0, 8)}...). Use the identity list to manage it.`, 'error');
return;
}
// Check if UUID already used by another identity
const uuidMatch = mappings.find(m => m.uuid.toLowerCase() === uuid.toLowerCase());
if (uuidMatch) {
showNotification(`This UUID is already used by identity "${uuidMatch.username}". Each identity must have a unique UUID.`, 'error');
return;
}
}
// Check username reservation on auth server
try {
const cfg = await window.electronAPI.loadConfig();
const authDomain = cfg.authDomain || 'auth.sanasol.ws';
const checkResp = await fetch(`https://${authDomain}/player/username/status/${encodeURIComponent(username)}`);
if (checkResp.ok) {
const status = await checkResp.json();
if (status.reserved) {
showNotification(`Username "${username}" is reserved by another player who set a password. Choose a different name.`, 'error');
return;
}
}
} catch (e) {
// Server check failed — allow creation (fail-open)
console.log('[Identity] Server username check skipped:', e.message);
}
// Check if UUID is password-protected on server (restore access flow)
let uuidIsProtected = false;
let registeredName = null;
try {
if (window.electronAPI.checkPasswordStatus) {
const pwStatus = await window.electronAPI.checkPasswordStatus(uuid);
if (pwStatus && pwStatus.hasPassword) {
uuidIsProtected = true;
registeredName = pwStatus.registeredName || null;
}
}
} catch (e) {
console.log('[Identity] UUID password check skipped:', e.message);
}
if (uuidIsProtected) {
// UUID is password-protected — need password to restore it
showRestoreProtectedIdentityDialog(username, uuid, registeredName);
return;
}
await saveNewIdentity(username, uuid);
} catch (error) {
console.error('Error adding identity:', error);
const msg = window.i18n ? window.i18n.t('notifications.identityAddFailed') : 'Failed to add identity';
@@ -931,6 +976,174 @@ async function confirmAddIdentity() {
}
}
async function saveNewIdentity(username, uuid) {
if (window.electronAPI && window.electronAPI.setUuidForUser) {
const result = await window.electronAPI.setUuidForUser(username, uuid);
if (result.success) {
showNotification('Identity added successfully!', 'success');
hideAddIdentityForm();
await loadAllUuids();
if (window.loadIdentities) window.loadIdentities();
updatePasswordShieldIcon();
} else if (result.error === 'duplicate') {
showNotification(`Identity "${username}" already exists (UUID: ${result.existingUuid.substring(0, 8)}...). Use the identity list to manage it.`, 'error');
} else if (result.error === 'uuid_in_use') {
showNotification(`This UUID is already used by identity "${result.existingUsername}". Each identity must have a unique UUID.`, 'error');
} else {
throw new Error(result.error || 'Failed to add identity');
}
}
}
function showRestoreProtectedIdentityDialog(username, uuid, registeredName) {
const existing = document.querySelector('.custom-confirm-modal');
if (existing) existing.remove();
const nameWarning = registeredName && registeredName.toLowerCase() !== username.toLowerCase()
? `<p style="color: #f59e0b; margin: 0 0 12px; font-size: 0.9rem;">
<i class="fas fa-exclamation-triangle"></i> This UUID is locked to name "<strong>${escapeHtml(registeredName)}</strong>".
Your entered name "${escapeHtml(username)}" will be replaced.
</p>`
: '';
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(147, 51, 234, 0.4);
transform: scale(0.9); transition: transform 0.3s ease;
`;
dialog.innerHTML = `
<div style="padding: 24px; border-bottom: 1px solid rgba(255,255,255,0.1);">
<div style="display: flex; align-items: center; gap: 12px; color: #9333ea;">
<i class="fas fa-shield-alt" style="font-size: 24px;"></i>
<h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">Restore Protected Identity</h3>
</div>
</div>
<div style="padding: 24px;">
<p style="color: #e5e7eb; margin: 0 0 16px; line-height: 1.6;">
This UUID is <strong style="color: #22c55e;">password-protected</strong>. Enter the password to restore access.
</p>
${nameWarning}
<div id="restoreError" style="display: none; color: #f87171; background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); border-radius: 8px; padding: 8px 12px; margin-bottom: 12px; font-size: 0.85rem;"></div>
<input type="password" id="restorePasswordInput" style="
width: 100%; box-sizing: border-box; padding: 10px 14px;
background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.2);
border-radius: 8px; color: #fff; font-size: 0.95rem; outline: none;
" placeholder="Password" autofocus />
</div>
<div style="padding: 16px 24px; display: flex; gap: 10px; justify-content: flex-end; border-top: 1px solid rgba(255,255,255,0.1);">
<button id="restoreCancelBtn" style="
padding: 8px 20px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.2);
background: transparent; color: #9ca3af; cursor: pointer; font-size: 0.9rem;
">Cancel</button>
<button id="restoreConfirmBtn" style="
padding: 8px 20px; border-radius: 8px; border: none;
background: linear-gradient(135deg, #9333ea, #3b82f6); color: white;
cursor: pointer; font-weight: 600; font-size: 0.9rem;
">Verify & Restore</button>
</div>
`;
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';
@@ -941,6 +1154,250 @@ 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 = '<i class="fas fa-lock" style="color:#22c55e"></i> 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 = '<i class="fas fa-unlock" style="color:#f59e0b"></i> 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');
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);
if (result.success) {
showNotification('Password set successfully', 'success');
newPw.value = '';
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);
if (result.success) {
showNotification('Password removed', 'success');
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'; }
}
};
// ─── 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 = '<span style="color:#22c55e;font-size:0.8em;padding:3px 8px;background:rgba(34,197,94,0.15);border-radius:6px;"><i class="fas fa-lock"></i> Protected</span>';
statusText.innerHTML = '<i class="fas fa-check-circle" style="color:#22c55e"></i> 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 = '<span style="color:#f59e0b;font-size:0.8em;padding:3px 8px;background:rgba(245,158,11,0.1);border-radius:6px;"><i class="fas fa-unlock"></i> Open</span>';
statusText.innerHTML = '<i class="fas fa-exclamation-triangle" style="color:#f59e0b"></i> 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');
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);
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();
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);
if (result.success) {
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'; }
}
};
// 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.';
@@ -1016,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;
@@ -1027,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');
}
@@ -1105,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)
@@ -1124,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 = `
<div style="padding: 24px; border-bottom: 1px solid rgba(255,255,255,0.1);">
<div style="display: flex; align-items: center; gap: 12px; color: #ef4444;">
<i class="fas fa-shield-alt" style="font-size: 24px;"></i>
<h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">Delete Protected Identity</h3>
</div>
</div>
<div style="padding: 24px;">
<p style="color: #e5e7eb; margin: 0 0 16px; line-height: 1.6;">
<strong>"${escapeHtml(username)}"</strong> is password-protected. Deleting it will:
</p>
<ul style="color: #f87171; margin: 0 0 16px; padding-left: 20px; line-height: 1.8;">
<li>Remove the password protection from this UUID</li>
<li>Release the reserved username "${escapeHtml(username)}"</li>
<li>Allow anyone to use this UUID and name</li>
</ul>
<p style="color: #9ca3af; margin: 0 0 16px; font-size: 0.9rem;">
Enter your current password to confirm deletion:
</p>
<div id="pwDeleteError" style="display: none; color: #f87171; background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); border-radius: 8px; padding: 8px 12px; margin-bottom: 12px; font-size: 0.85rem;"></div>
<input type="password" id="pwDeleteInput" style="
width: 100%; box-sizing: border-box; padding: 10px 14px;
background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.2);
border-radius: 8px; color: #fff; font-size: 0.95rem; outline: none;
" placeholder="Current password" autofocus />
</div>
<div style="padding: 16px 24px; display: flex; gap: 10px; justify-content: flex-end; border-top: 1px solid rgba(255,255,255,0.1);">
<button id="pwDeleteCancelBtn" style="
padding: 8px 20px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.2);
background: transparent; color: #9ca3af; cursor: pointer; font-size: 0.9rem;
">Cancel</button>
<button id="pwDeleteConfirmBtn" style="
padding: 8px 20px; border-radius: 8px; border: none;
background: #ef4444; color: white; cursor: pointer; font-weight: 600; font-size: 0.9rem;
">Delete & Remove Password</button>
</div>
`;
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');
}
}

View File

@@ -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%;

View File

@@ -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 });
}
// =============================================================================

View File

@@ -43,24 +43,51 @@ 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;
}
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}`);
}
@@ -77,10 +104,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 +128,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 || error.nameLocked) {
throw error;
}
console.error('Failed to fetch auth tokens:', error.message);
// Fallback to local generation if server unavailable
return generateLocalTokens(uuid, name);
@@ -147,7 +180,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 +289,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
@@ -488,8 +522,8 @@ async function launchGame(playerNameOverride = null, progressCallback, javaPathO
if (fs.existsSync(agentJar)) {
const agentFlag = `-javaagent:"${agentJar}"`;
env.JAVA_TOOL_OPTIONS = env.JAVA_TOOL_OPTIONS
? `${env.JAVA_TOOL_OPTIONS} ${agentFlag} -Xshare:off`
: `${agentFlag} -Xshare:off`;
? `${env.JAVA_TOOL_OPTIONS} ${agentFlag}`
: agentFlag;
console.log('DualAuth Agent: enabled via JAVA_TOOL_OPTIONS');
}
@@ -578,7 +612,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 +685,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 +699,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 || error.nameLocked) {
throw error;
}
// Always return an error response instead of throwing
return { success: false, error: error.message || 'Unknown launch error' };
}

View File

@@ -1,10 +1,10 @@
# Singleplayer Server Crash: fastutil ClassNotFoundException
## Status: Open (multiple users, Feb 24-27 2026)
## Status: Open — NO SOLUTION (Feb 24-27 2026)
## Symptom
Singleplayer server crashes immediately after DualAuth Agent installs successfully:
Singleplayer server crashes immediately on boot:
```
Exception in thread "main" java.lang.NoClassDefFoundError: it/unimi/dsi/fastutil/objects/ObjectArrayList
@@ -25,99 +25,55 @@ Server exits with code 1. Multiplayer works fine for the same user.
- Multiplayer works fine for both users
- macOS/Linux users are NOT affected
## What Works
## Ruled Out (confirmed via debug builds)
- Java wrapper correctly strips `-XX:+UseCompactObjectHeaders`
- Java wrapper correctly injects `--disable-sentry`
- DualAuth Agent v1.1.12 installs successfully (STATIC mode)
- Multiplayer connections work fine
- Repair and reinstall did NOT fix the issue
| Suspect | Tested | Result |
|---------|--------|--------|
| **DualAuth Agent** | Debug build with agent completely disabled (`debug-no-agent` tag) | **Same crash.** Agent is innocent. |
| **`-Xshare:off` (CDS)** | Added to `JAVA_TOOL_OPTIONS` in launcher code (`debug-xshare-off` tag) | **Did not help.** CDS is not the cause. |
| **`-XX:+UseCompactObjectHeaders`** | Stripped via wrapper | **Did not help.** Server has `-XX:+IgnoreUnrecognizedVMOptions` anyway. |
| **Corrupted game files** | User did repair + full reinstall | **Same crash.** |
| **Java wrapper** | Logs confirm wrapper works correctly | Not the cause. |
| **ARM64/Parallels** | User is on standard Windows x86_64 | Not applicable. |
| **AOT cache** | Asentrix has no AOT errors (JAYED did), both crash the same way | Not the root cause. |
## Root Cause Analysis
## What We Know
`fastutil` (`it.unimi.dsi.fastutil`) should be bundled inside `HytaleServer.jar` (fat JAR). The `ClassNotFoundException` means the JVM's app classloader cannot find it despite it being in the JAR.
- `fastutil` is bundled inside `HytaleServer.jar` (fat/shaded JAR) — same JAR for all users
- JVM's `BuiltinClassLoader` cannot find `it.unimi.dsi.fastutil.objects.ObjectArrayList` despite it being in the JAR
- Crash happens at `EarlyPluginLoader` static initializer (line 34) which imports `ObjectArrayList`
- The bundled JRE is identical for all users (downloaded by launcher)
- The issue is **not** caused by anything the F2P launcher adds — it's the vanilla server JVM failing to load its own classes
### Ruled Out
## Remaining Theories
- **Wrapper issue**: Wrapper is working correctly (confirmed in logs)
- **UseCompactObjectHeaders**: Server also has `-XX:+IgnoreUnrecognizedVMOptions`, so unrecognized flags don't crash it
- **DualAuth Agent**: Works for all other users; agent installs successfully before the crash
- **Corrupted game files**: Repair/reinstall didn't help
- **ARM64/Parallels**: User is on standard Windows, not ARM
1. **Antivirus/security software** — Windows Defender or third-party AV intercepting JAR file reads. Real-time scanning + fat JAR = known conflict. **Untested** — user should try disabling AV temporarily.
2. **Windows Insider build** — Asentrix is on NT 10.0.26200.0 (Windows 11 Dev/Insider). Bleeding-edge Windows may have JVM compatibility issues.
3. **File locking** — Stalled `java.exe` processes holding `HytaleServer.jar` open (Asentrix had stalled processes killed at every launch).
4. **Corrupted JRE on disk** — Despite being the same download, filesystem or AV may have corrupted specific JRE files on their system.
### Likely Cause
## Next Steps to Try
**CDS (Class Data Sharing) broken by bootstrap classloader modification.** DualAuth agent calls `appendToBootstrapClassLoaderSearch()` which triggers JVM warning: `"Sharing is only supported for boot loader classes because bootstrap classpath has been appended"`. This disables AppCDS for application classes. On some Windows systems, this breaks the classloader's ability to find classes (including fastutil) from the fat JAR.
This warning appears for ALL users, but only breaks classloading on some Windows systems — reason unknown.
### Other Possible Causes
1. **Antivirus interference** — AV blocking Java from reading classes out of JAR files
2. **File locking** — another process holding HytaleServer.jar open (Asentrix had stalled java.exe killed at launch)
## Potential Fix: `-Xshare:off` (testing Feb 27)
Disables CDS entirely, forcing standard classloading. User can add via launcher:
1. **Settings****Java Wrapper Configuration****Arguments to Inject**
2. Add `-Xshare:off` with **Server Only** condition
3. Retry singleplayer
Sent to affected users for testing — **awaiting results**.
If confirmed, should be added as default inject arg (server-only) in launcher config.
## Debugging Steps (for reference)
Most steps are impractical for F2P users:
- ~~Official Hytale singleplayer~~ — F2P users don't have official access
- ~~Try without DualAuth agent~~ — not possible, agent required for F2P token validation
- ~~Verify fastutil in JAR~~ — same JAR for all users, not a user-actionable step
- ~~Check JRE version~~ — bundled with launcher, same for all users
**Practical steps:**
1. **Add `-Xshare:off`** via wrapper inject args (server-only) — testing now
2. **Check antivirus** — add game directory to Windows Defender exclusions
3. **Check for stalled processes** — kill any leftover java.exe/HytaleServer before launch
1. **Disable Windows Defender** temporarily — the only quick test left
2. **Delete bundled JRE** and let launcher re-download — rules out local JRE corruption
3. **Ask if official Hytale singleplayer works** — if it also crashes, it's their system (but F2P users may not have access)
## Update History
### Feb 24: `-XX:+UseCompactObjectHeaders` stripping removed from defaults
Stripping this flag did NOT fix the issue. The server already has `-XX:+IgnoreUnrecognizedVMOptions` so unrecognized flags are harmless. The flag was removed from default `stripFlags` in `backend/core/config.js`.
### Feb 24: First report (JAYED)
User reported singleplayer crash. Initial investigation found AOT cache errors + fastutil ClassNotFoundException. Stripping `-XX:+UseCompactObjectHeaders` did not help.
### Feb 27: Second user (Asentrix) reported, `-Xshare:off` sent for testing
Asentrix hit the same crash on Launcher v2.4.4. Unlike JAYED, no AOT cache errors — just the CDS sharing warning followed by fastutil ClassNotFoundException. This confirms the issue is not AOT-specific but related to CDS/classloader interaction with the DualAuth agent's bootstrap CL modification. Sent `-Xshare:off` workaround to affected users — awaiting results.
## Using the Java Wrapper to Strip JVM Flags
If a user needs to strip a specific JVM flag (e.g., for debugging or compatibility), they can do it via the launcher UI:
1. Open **Settings** → scroll to **Java Wrapper Configuration**
2. Under **JVM Flags to Remove**, type the flag (e.g. `-XX:+UseCompactObjectHeaders`) and click **Add**
3. The flag will be stripped from all JVM invocations at launch time
4. To inject custom arguments, use the **Arguments to Inject** section (with optional "Server Only" condition)
5. **Restore Defaults** resets to empty strip flags + `--disable-sentry` (server only)
The wrapper generates platform-specific scripts at launch time:
- **Windows**: `java-wrapper.bat` in `jre/latest/bin/`
- **macOS/Linux**: `java-wrapper` shell script in the same directory
Config is stored in `config.json` under `javaWrapperConfig`:
```json
{
"javaWrapperConfig": {
"stripFlags": ["-XX:+SomeFlag"],
"injectArgs": [
{ "arg": "--some-arg", "condition": "server" },
{ "arg": "--other-arg", "condition": "always" }
]
}
}
```
### Feb 27: Second report (Asentrix), extensive debugging
- Asentrix hit same crash, no AOT errors — ruled out AOT as root cause
- Built `debug-xshare-off`: added `-Xshare:off` to `JAVA_TOOL_OPTIONS`**did not help**
- Built `debug-no-agent`: completely disabled DualAuth agent — **same crash**
- **Conclusion**: Neither the agent nor CDS is the cause. The JVM itself cannot load classes from the fat JAR on these specific Windows systems.
- Note: wrapper `injectArgs` append AFTER `-jar`, so they cannot inject JVM flags — only `JAVA_TOOL_OPTIONS` works for JVM flags
## Related
- Java wrapper config: `backend/core/config.js` (stripFlags / injectArgs)
- DualAuth Agent: v1.1.12, package `ws.sanasol.dualauth`
- Game version at time of reports: `2026.02.19-1a311a592`
- Log submission ID (Asentrix): `c88e7b71`
- Debug tags: `debug-xshare-off`, `debug-no-agent`
- Log submission IDs: `c88e7b71` (Asentrix initial), `0445e4dc` (xshare test), `748dceeb` (no-agent test)

168
main.js
View File

@@ -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 = {};
const { getAuthServerUrl, getUuidForUser } = require('./backend/core/config');
const launchUuid = getUuidForUser(playerName);
try {
const uuid = launchUuid;
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,68 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, g
}, 2000);
}
if (error.passwordRequired) {
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.' };
}
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 };
}
});
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() };
}
if (error.nameLocked) {
return { success: false, nameLocked: true, registeredName: error.registeredName, error: error.message };
}
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:`);
@@ -1352,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);
@@ -1391,6 +1471,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();

View File

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

View File

@@ -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'),
@@ -102,11 +103,20 @@ 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'),
// 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),