fix: real uid in bucket list, Firestore rules hardening for date plans & bucket list

This commit is contained in:
null 2026-06-17 19:41:27 -05:00
parent ec315c63e0
commit 19c6b4a6cb
2 changed files with 51 additions and 6 deletions

View File

@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.closer.domain.model.BucketListItem
import app.closer.domain.repository.BucketListRepository
import com.google.firebase.auth.FirebaseAuth
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
@ -48,7 +49,7 @@ class BucketListViewModel @Inject constructor(
title = title.take(MAX_TITLE_LENGTH),
description = description.take(MAX_DESCRIPTION_LENGTH),
category = category,
addedBy = "currentUser",
addedBy = FirebaseAuth.getInstance().currentUser?.uid ?: "",
addedAt = System.currentTimeMillis()
)
@ -71,7 +72,7 @@ class BucketListViewModel @Inject constructor(
)
}
} else {
repository.completeItem(itemId, "currentUser")
repository.completeItem(itemId, FirebaseAuth.getInstance().currentUser?.uid ?: "")
_uiState.update {
it.copy(
items = it.items.map {

View File

@ -47,6 +47,16 @@ service cloud.firestore {
return action == 'love' || action == 'maybe' || action == 'skip';
}
function isValidDatePlanStatus(status) {
return status == 'draft' || status == 'planned' || status == 'completed';
}
function isValidBucketListCategory(category) {
return category == 'adventure' || category == 'travel' || category == 'food'
|| category == 'learning' || category == 'romance' || category == 'intimacy'
|| category == 'seasonal';
}
// ── Users ─────────────────────────────────────────────────────────────────
// Each user owns exactly their own document.
// hasPremium is server-only: clients may not write it directly.
@ -293,17 +303,51 @@ service cloud.firestore {
}
// Date plans: complete plans assembled from partner preferences.
// Both members can read; create/update/delete by both members.
// Both members can read and delete; writes are field-validated.
// createdAt is immutable after creation (excluded from the update allowed-keys set).
match /date_plans/{planId} {
allow read: if isCouplesMember(coupleId);
allow create, update, delete: if isCouplesMember(coupleId);
allow create: if isCouplesMember(coupleId)
&& request.resource.data.keys().hasAll(['dateIdeaId', 'scheduledDate', 'status', 'createdAt', 'updatedAt'])
&& request.resource.data.keys().hasOnly([
'dateIdeaId', 'scheduledDate', 'scheduledTime', 'budget', 'duration',
'status', 'activity', 'food', 'conversationPrompts', 'optionalChallenge',
'createdAt', 'updatedAt'
])
&& isValidDatePlanStatus(request.resource.data.status);
allow update: if isCouplesMember(coupleId)
// Only the explicitly-listed fields may change on update.
// createdAt is intentionally absent — it cannot be modified after creation.
&& request.resource.data.diff(resource.data).affectedKeys().hasOnly([
'dateIdeaId', 'scheduledDate', 'scheduledTime', 'budget', 'duration',
'status', 'activity', 'food', 'conversationPrompts', 'optionalChallenge',
'updatedAt'
])
&& isValidDatePlanStatus(request.resource.data.status);
allow delete: if isCouplesMember(coupleId);
}
// Bucket list items: shared list for both partners.
// Both members can read; create/update/delete by both members.
// addedBy must match the caller on creation; addedBy and addedAt are immutable.
// Marking an item complete requires the caller to own the completedBy field.
match /bucket_list/{itemId} {
allow read: if isCouplesMember(coupleId);
allow create, update, delete: if isCouplesMember(coupleId);
allow create: if isCouplesMember(coupleId)
&& request.resource.data.keys().hasAll(['title', 'addedBy', 'addedAt', 'isCompleted'])
&& request.resource.data.keys().hasOnly([
'title', 'description', 'category', 'addedBy', 'addedAt',
'completedBy', 'completedAt', 'isCompleted'
])
&& request.resource.data.addedBy == request.auth.uid
&& isValidBucketListCategory(request.resource.data.category);
allow update: if isCouplesMember(coupleId)
&& request.resource.data.diff(resource.data).affectedKeys().hasOnly([
'title', 'description', 'category', 'isCompleted', 'completedBy', 'completedAt'
])
&& isImmutable(['addedBy', 'addedAt'])
// completedBy must be the caller when marking an item complete
&& (!request.resource.data.isCompleted || request.resource.data.completedBy == request.auth.uid);
allow delete: if isCouplesMember(coupleId);
}
}
}