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;