Feat: add IP-based rate limiting and house balance thresholds

This commit is contained in:
jwinterm
2026-02-01 17:38:30 -05:00
parent 9a53df4a35
commit d27df3b6cf
5 changed files with 254 additions and 23 deletions
+26 -1
View File
@@ -47,6 +47,19 @@ class PaymentHandlers {
this.io.to(socket.id).emit('payment_error', { error: 'Payment system not available' });
return;
}
// Check if house balance is critically low before proceeding with payment
if (this.gameModeManager.alertService) {
const balanceCheck = await this.gameModeManager.alertService.checkBalanceForGameStart();
if (balanceCheck.halted) {
this.io.to(socket.id).emit('balance_critical', {
reason: 'low_balance',
message: balanceCheck.reason || 'Sorry, the house balance is too low to initiate new games. Please try again later.'
});
return;
}
}
try {
const { type, packageId } = data;
const paymentType = type || data.gameMode || 'single_game';
@@ -104,7 +117,19 @@ class PaymentHandlers {
async createAndShowPaymentRequest(socket, options = {}) {
if (!this.gameModeManager) return;
// Check if house balance is critically low before proceeding with payment
if (this.gameModeManager.alertService) {
const balanceCheck = await this.gameModeManager.alertService.checkBalanceForGameStart();
if (balanceCheck.halted) {
this.io.to(socket.id).emit('balance_critical', {
reason: 'low_balance',
message: balanceCheck.reason || 'Sorry, the house balance is too low to initiate new games. Please try again later.'
});
return;
}
}
// If both direct and credits modes are enabled, let the user choose
const bothModesEnabled = this.gameModeManager.directModeEnabled && this.gameModeManager.creditsModeEnabled;
const forceShowOptions = options.showOptions === true;
+26 -8
View File
@@ -43,13 +43,14 @@ class RateLimiter {
const now = Date.now();
const key = `${userId}:${action}`;
// Check user-based limits
const userResult = this._checkSingleLimit(key, limit, now);
// Check IP-based limits if IP provided and it's a connection-type action
// Check IP-based limits if IP provided
// Apply to: connection actions, payment actions, and game actions
let ipResult = { allowed: true, remaining: Infinity, retryAfter: 0 };
if (ip && action.startsWith('connection:')) {
if (ip && this._shouldApplyIpLimit(action)) {
const ipKey = `${ip}:${action}`;
ipResult = this._checkSingleLimit(ipKey, limit, now, this.ipStorage);
}
@@ -68,6 +69,23 @@ class RateLimiter {
return result;
}
/**
* Determine if an action should have IP-based rate limiting applied
* @param {string} action - Action type
* @returns {boolean}
*/
_shouldApplyIpLimit(action) {
// Actions that should be rate limited by IP (in addition to socket ID)
// This prevents abuse when users reconnect to bypass socket-based limits
const ipLimitedPrefixes = [
'connection:', // Connection attempts
'payment:', // Payment creation/operations
'game:', // Game start/queue operations
'address:' // Address changes
];
return ipLimitedPrefixes.some(prefix => action.startsWith(prefix));
}
/**
* Record an action attempt (increment counter)
* @param {string} userId - User identifier
@@ -77,12 +95,12 @@ class RateLimiter {
async recordAttempt(userId, action, ip = null) {
const now = Date.now();
const key = `${userId}:${action}`;
// Record user attempt
this._recordSingleAttempt(key, now);
// Record IP attempt for connection actions
if (ip && action.startsWith('connection:')) {
// Record IP attempt for actions that should have IP-based limiting
if (ip && this._shouldApplyIpLimit(action)) {
const ipKey = `${ip}:${action}`;
this._recordSingleAttempt(ipKey, now, this.ipStorage);
}