Add server-side validation support to password and PIN minigames

- Update password minigame to call server API when correctPassword is null
- Update PIN minigame to call server API when correctPin is null
- Pass lockable and type parameters to minigames for server validation
- Maintain backwards compatibility with client-side validation
- Handle network errors gracefully without counting failed attempts

This allows minigames to validate attempts server-side for security, preventing client-side answer spoofing.
This commit is contained in:
Z. Cliffe Schreuders
2025-11-22 00:46:55 +00:00
parent b27cde13d0
commit 02fc7f6876
3 changed files with 106 additions and 16 deletions

View File

@@ -373,23 +373,68 @@ export class PasswordMinigame extends MinigameScene {
}
}
submitPassword() {
async submitPassword() {
if (!this.gameState.isActive) return;
const enteredPassword = this.passwordField.value.trim();
if (!enteredPassword) {
this.showFailure("Please enter a password", false, 2000);
return;
}
this.gameData.attempts++;
this.attemptsDisplay.textContent = this.gameData.attempts;
if (enteredPassword === this.correctPassword) {
this.passwordCorrect();
// Check if we need server-side validation (correctPassword is null)
if (this.correctPassword === null && window.APIClient && window.gameId) {
await this.validatePasswordWithServer(enteredPassword);
} else {
this.passwordIncorrect();
// Client-side validation (backwards compatibility)
if (enteredPassword === this.correctPassword) {
this.passwordCorrect();
} else {
this.passwordIncorrect();
}
}
}
async validatePasswordWithServer(enteredPassword) {
try {
// Get lockable object and type from params
const lockable = this.params.lockable || this.params.sprite;
const targetType = this.params.type || 'object'; // 'door' or 'object'
// Get target ID from lockable
let targetId;
if (targetType === 'door') {
targetId = lockable.doorProperties?.connectedRoom || lockable.doorProperties?.roomId;
} else {
targetId = lockable.scenarioData?.id || lockable.scenarioData?.name || lockable.objectId;
}
if (!targetId) {
console.error('Could not determine targetId for unlock validation');
this.passwordIncorrect();
return;
}
console.log('Validating password with server:', { targetType, targetId, attempt: enteredPassword });
// Call server API for validation
const response = await window.APIClient.unlock(targetType, targetId, enteredPassword, 'password');
if (response.success) {
this.passwordCorrect();
} else {
this.passwordIncorrect();
}
} catch (error) {
console.error('Server validation error:', error);
this.showFailure("Network error. Please try again.", false, 3000);
// Decrease attempts counter since this wasn't a real attempt
this.gameData.attempts--;
this.attemptsDisplay.textContent = this.gameData.attempts;
}
}

View File

@@ -240,31 +240,73 @@ export class PinMinigame extends MinigameScene {
this.updateDisplay();
}
handleEnter() {
async handleEnter() {
if (this.isLocked || this.currentInput.length !== this.pinLength) {
return;
}
this.attemptCount++;
const isCorrect = this.currentInput === this.correctPin;
// Check if we need server-side validation (correctPin is null)
let isCorrect;
if (this.correctPin === null && window.APIClient && window.gameId) {
isCorrect = await this.validatePinWithServer(this.currentInput);
} else {
// Client-side validation (backwards compatibility)
isCorrect = this.currentInput === this.correctPin;
}
// Record attempt
const attempt = {
input: this.currentInput,
isCorrect: isCorrect,
timestamp: new Date(),
feedback: (this.infoLeakToggleElement?.checked || this.infoLeakMode) ? this.calculateFeedback(this.currentInput) : null
feedback: (this.infoLeakToggleElement?.checked || this.infoLeakMode) && this.correctPin ? this.calculateFeedback(this.currentInput) : null
};
this.attempts.push(attempt);
this.updateAttemptsDisplay();
if (isCorrect) {
this.handleSuccess();
} else {
this.handleFailure();
}
}
async validatePinWithServer(enteredPin) {
try {
// Get lockable object and type from params
const lockable = this.params.lockable || this.params.sprite;
const targetType = this.params.type || 'object'; // 'door' or 'object'
// Get target ID from lockable
let targetId;
if (targetType === 'door') {
targetId = lockable.doorProperties?.connectedRoom || lockable.doorProperties?.roomId;
} else {
targetId = lockable.scenarioData?.id || lockable.scenarioData?.name || lockable.objectId;
}
if (!targetId) {
console.error('Could not determine targetId for unlock validation');
return false;
}
console.log('Validating PIN with server:', { targetType, targetId, attempt: enteredPin });
// Call server API for validation
const response = await window.APIClient.unlock(targetType, targetId, enteredPin, 'pin');
return response.success;
} catch (error) {
console.error('Server validation error:', error);
this.showFailure("Network error. Please try again.", false, 3000);
// Decrease attempts counter since this wasn't a real attempt
this.attemptCount--;
return false;
}
}
calculateFeedback(input) {
// Mastermind-style feedback: right number in right place, right number in wrong place

View File

@@ -526,9 +526,11 @@ export function startPinMinigame(lockable, type, correctPin, callback) {
title: `Enter PIN for ${type}`,
correctPin: correctPin,
maxAttempts: 3,
pinLength: correctPin.length,
pinLength: correctPin ? correctPin.length : 4, // Default to 4 if null (server-side validation)
hasPinCracker: hasPinCracker,
allowBackspace: true,
lockable: lockable,
type: type, // Pass type for server validation
onComplete: (success, result) => {
if (success) {
console.log('PIN MINIGAME SUCCESS');
@@ -579,6 +581,7 @@ export function startPasswordMinigame(lockable, type, correctPassword, callback,
postitNote: options.postitNote || '',
showPostit: options.showPostit || false,
lockable: lockable,
type: type, // Pass type for server validation
requiresKeyboardInput: true, // Password minigame needs keyboard for text input
onComplete: (success, result) => {
if (success) {