mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-28 19:41:46 -03:00
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>
This commit is contained in:
@@ -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' };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user