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

View File

@ -47,6 +47,16 @@ service cloud.firestore {
return action == 'love' || action == 'maybe' || action == 'skip'; 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 ───────────────────────────────────────────────────────────────── // ── Users ─────────────────────────────────────────────────────────────────
// Each user owns exactly their own document. // Each user owns exactly their own document.
// hasPremium is server-only: clients may not write it directly. // 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. // 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} { match /date_plans/{planId} {
allow read: if isCouplesMember(coupleId); 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. // 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} { match /bucket_list/{itemId} {
allow read: if isCouplesMember(coupleId); 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);
} }
} }
} }