From c1ef8d663095439a435202d1066ff2e9787de2fa Mon Sep 17 00:00:00 2001 From: null Date: Thu, 25 Jun 2026 09:37:36 -0500 Subject: [PATCH] fix(rules): allow completedByUsers on session update so finished games close (B-001 P1) The sessions allow-update rule required affectedKeys().hasOnly(['status','completedAt']), but the async-game completion path (markUserComplete) always writes completedByUsers, so every 'I reached results' write was denied and the session stayed active forever -> the couple was locked out of starting any new game (only the destructive 'End their game' worked, since abandonSession only diffs status/completedAt). Rule now permits ['status','completedAt','completedByUsers'], lets any couple member record completion progress, keeps startedByUserId immutable and status monotonic (active->completed). Deployed + verified live: both finish a game -> session auto-completes (completedByUsers =[both]) -> next game starts immediately (no 'Waiting for partner' block). Co-Authored-By: Claude Opus 4.8 --- firestore.rules | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/firestore.rules b/firestore.rules index 2634a784..74c4d74a 100644 --- a/firestore.rules +++ b/firestore.rules @@ -309,17 +309,20 @@ service cloud.firestore { allow create: if isCouplesMember(coupleId) && request.resource.data.startedByUserId == request.auth.uid; - // Update: only the user who started the session can update it, OR valid status transitions. - // startedByUserId is immutable for direct client writes. + // Update: any couple member may record session progress/completion. + // (Async two-device games mark each player done via `completedByUsers`; the + // session flips active→completed once both are in. The previous rule only + // allowed `status`/`completedAt`, so every `completedByUsers` write was denied + // and finished games never closed — locking the couple out of new games. B-001.) allow update: if isCouplesMember(coupleId) - // Either the original starter can update - && (resource.data.startedByUserId == request.auth.uid - // Or status transition is valid: active → completed - || (resource.data.status == 'active' && request.resource.data.status == 'completed')) - // startedByUserId cannot be changed by clients + // startedByUserId is immutable for direct client writes. && request.resource.data.startedByUserId == resource.data.startedByUserId - // Only a fixed set of fields may change - && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['status', 'completedAt']); + // Only session progress/completion fields may change. + && request.resource.data.diff(resource.data).affectedKeys() + .hasOnly(['status', 'completedAt', 'completedByUsers']) + // status is monotonic: stay the same, or transition active → completed (never revert). + && (request.resource.data.status == resource.data.status + || (resource.data.status == 'active' && request.resource.data.status == 'completed')); // Delete: server-only (admin SDK). Admin SDK bypasses rules. allow delete: if false;