From 98b66d244ce7a122439e10c89fd70bdcc4c65f92 Mon Sep 17 00:00:00 2001 From: Eugene Toder Date: Sat, 10 Jan 2026 18:27:59 -0500 Subject: [PATCH] Improve rules for userGames Also, don't calculate unfinished games when starting a private game. --- database.rules.json | 4 +-- functions/src/index.ts | 80 ++++++++++++++++++++---------------------- src/pages/RoomPage.js | 3 +- 3 files changed, 42 insertions(+), 45 deletions(-) diff --git a/database.rules.json b/database.rules.json index 4c54261..a5c4a32 100644 --- a/database.rules.json +++ b/database.rules.json @@ -13,7 +13,7 @@ }, "users": { "$userId": { - ".write": "auth != null && auth.uid == $userId && data.parent().parent().child('status').val() == 'waiting' && !(root.hasChild('users/' + auth.uid + '/banned') && now < root.child('users/' + auth.uid + '/banned').val())", + ".write": "auth != null && auth.uid == $userId && data.parent().parent().child('status').val() == 'waiting' && newData.parent().parent().parent().parent().hasChild('userGames/' + $userId + '/' + $gameId) == newData.exists() && !(root.hasChild('users/' + auth.uid + '/banned') && now < root.child('users/' + auth.uid + '/banned').val())", ".validate": "newData.isNumber() && newData.val() == now" } }, @@ -117,7 +117,7 @@ ".read": "auth != null", ".indexOn": ".value", "$gameId": { - ".write": "auth != null && auth.uid == $userId", + ".write": "auth != null && auth.uid == $userId && newData.parent().parent().parent().hasChild('games/' + $gameId + '/users/' + $userId) == newData.exists()", ".validate": "newData.isNumber() && newData.val() == root.child('games/' + $gameId + '/createdAt').val()" } } diff --git a/functions/src/index.ts b/functions/src/index.ts index 59bae86..bda4e12 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -258,30 +258,38 @@ export const createGame = functions.https.onCall(async (data, context) => { "The function must be called while authenticated." ); } - const userId = context.auth.uid; - const oneHourAgo = Date.now() - 3600000; - const recentGameIds = await admin - .database() - .ref(`userGames/${userId}`) - .orderByValue() - .startAt(oneHourAgo) - .once("value"); - const recentGames = await Promise.all( - Object.keys(recentGameIds.val() || {}).map((recentGameId) => - admin.database().ref(`games/${recentGameId}`).once("value") - ) - ); + if (access === "public") { + const oneHourAgo = Date.now() - 3600000; + const recentGameIds = await admin + .database() + .ref(`userGames/${userId}`) + .orderByValue() + .startAt(oneHourAgo) + .once("value"); - let unfinishedGames = 0; - for (const snap of recentGames) { - if ( - snap.child("host").val() === userId && - snap.child("status").val() !== "done" && - snap.child("access").val() === "public" - ) { - ++unfinishedGames; + const recentGames = await Promise.all( + Object.keys(recentGameIds.val() || {}).map((recentGameId) => + admin.database().ref(`games/${recentGameId}`).once("value") + ) + ); + + let unfinishedGames = 0; + for (const snap of recentGames) { + if ( + snap.child("access").val() === "public" && + snap.child("status").val() !== "done" && + snap.child("host").val() === userId + ) { + ++unfinishedGames; + } + } + if (unfinishedGames >= MAX_UNFINISHED_GAMES_PER_HOUR) { + throw new functions.https.HttpsError( + "resource-exhausted", + "Too many unfinished public games were recently created." + ); } } @@ -289,27 +297,17 @@ export const createGame = functions.https.onCall(async (data, context) => { .database() .ref(`games/${gameId}`) .transaction((currentData) => { - if (currentData === null) { - if ( - unfinishedGames >= MAX_UNFINISHED_GAMES_PER_HOUR && - access === "public" - ) { - throw new functions.https.HttpsError( - "resource-exhausted", - "Too many unfinished public games were recently created." - ); - } - return { - host: userId, - createdAt: admin.database.ServerValue.TIMESTAMP, - status: "waiting", - access, - mode, - enableHint, - }; - } else { - return; + if (currentData !== null) { + return; // the game already exists, bail out } + return { + host: userId, + createdAt: admin.database.ServerValue.TIMESTAMP, + status: "waiting", + access, + mode, + enableHint, + }; }); if (!committed) { throw new functions.https.HttpsError( diff --git a/src/pages/RoomPage.js b/src/pages/RoomPage.js index 499c59a..f634c00 100644 --- a/src/pages/RoomPage.js +++ b/src/pages/RoomPage.js @@ -61,8 +61,7 @@ function RoomPage({ match, location }) { useEffect(() => { if ( !leaving && - game && - game.status === "waiting" && + game?.status === "waiting" && (!game.users || !(user.id in game.users)) ) { const updates = {