fix: real uid in bucket list, Firestore rules hardening for date plans & bucket list
This commit is contained in:
parent
ec315c63e0
commit
19c6b4a6cb
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue