feat: update question thread data source, repository, ViewModel, and Firestore security rules

This commit is contained in:
null 2026-06-19 03:19:58 -05:00
parent 927e930b79
commit 9e587a23dd
5 changed files with 13 additions and 19 deletions

View File

@ -35,7 +35,7 @@ class FirestoreQuestionThreadDataSource @Inject constructor(private val db: Fire
return snap.documents.firstOrNull()?.id return snap.documents.firstOrNull()?.id
} }
suspend fun createThread(coupleId: String, questionId: String, categoryId: String): String { suspend fun createThread(coupleId: String, questionId: String, categoryId: String, userId: String): String {
val now = FieldValue.serverTimestamp() val now = FieldValue.serverTimestamp()
val doc = threadsRef(coupleId).document() val doc = threadsRef(coupleId).document()
doc.set( doc.set(
@ -44,6 +44,7 @@ class FirestoreQuestionThreadDataSource @Inject constructor(private val db: Fire
"categoryId" to categoryId, "categoryId" to categoryId,
"status" to QuestionThreadStatus.NOT_STARTED.toFirestoreValue(), "status" to QuestionThreadStatus.NOT_STARTED.toFirestoreValue(),
"currentIndex" to 0, "currentIndex" to 0,
"createdByUserId" to userId,
"createdAt" to now, "createdAt" to now,
"updatedAt" to now "updatedAt" to now
) )
@ -62,12 +63,8 @@ class FirestoreQuestionThreadDataSource @Inject constructor(private val db: Fire
suspend fun updateThreadStatus(coupleId: String, threadId: String, status: QuestionThreadStatus) { suspend fun updateThreadStatus(coupleId: String, threadId: String, status: QuestionThreadStatus) {
threadsRef(coupleId).document(threadId) threadsRef(coupleId).document(threadId)
.update( .update("status", status.toFirestoreValue())
mapOf( .voidAwait()
"status" to status.toFirestoreValue(),
"updatedAt" to FieldValue.serverTimestamp()
)
).voidAwait()
} }
// ─── Answers ───────────────────────────────────────────────────────────────── // ─── Answers ─────────────────────────────────────────────────────────────────
@ -119,7 +116,7 @@ class FirestoreQuestionThreadDataSource @Inject constructor(private val db: Fire
.collection(FirestoreCollections.QuestionThreads.MESSAGES) .collection(FirestoreCollections.QuestionThreads.MESSAGES)
.add( .add(
mapOf( mapOf(
"userId" to message.userId, "authorUserId" to message.userId,
"text" to message.text, "text" to message.text,
"createdAt" to FieldValue.serverTimestamp() "createdAt" to FieldValue.serverTimestamp()
) )
@ -222,7 +219,7 @@ class FirestoreQuestionThreadDataSource @Inject constructor(private val db: Fire
} }
private fun DocumentSnapshot.toQuestionMessage(): QuestionMessage? { private fun DocumentSnapshot.toQuestionMessage(): QuestionMessage? {
val userId = getString("userId") ?: return null val userId = getString("authorUserId") ?: return null
return QuestionMessage( return QuestionMessage(
id = id, id = id,
userId = userId, userId = userId,

View File

@ -21,9 +21,10 @@ class QuestionThreadRepositoryImpl @Inject constructor(
override suspend fun findOrCreateThreadId( override suspend fun findOrCreateThreadId(
coupleId: String, coupleId: String,
questionId: String, questionId: String,
categoryId: String categoryId: String,
userId: String
): String = dataSource.findThreadByQuestionId(coupleId, questionId) ): String = dataSource.findThreadByQuestionId(coupleId, questionId)
?: dataSource.createThread(coupleId, questionId, categoryId) ?: dataSource.createThread(coupleId, questionId, categoryId, userId)
override fun observeThread(coupleId: String, threadId: String): Flow<QuestionThread> = override fun observeThread(coupleId: String, threadId: String): Flow<QuestionThread> =
dataSource.observeThread(coupleId, threadId) dataSource.observeThread(coupleId, threadId)

View File

@ -7,7 +7,7 @@ import app.closer.domain.model.QuestionThread
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface QuestionThreadRepository { interface QuestionThreadRepository {
suspend fun findOrCreateThreadId(coupleId: String, questionId: String, categoryId: String): String suspend fun findOrCreateThreadId(coupleId: String, questionId: String, categoryId: String, userId: String): String
fun observeThread(coupleId: String, threadId: String): Flow<QuestionThread> fun observeThread(coupleId: String, threadId: String): Flow<QuestionThread>
suspend fun submitAnswer(coupleId: String, threadId: String, userId: String, answer: QuestionAnswer) suspend fun submitAnswer(coupleId: String, threadId: String, userId: String, answer: QuestionAnswer)
fun observeAnswers(coupleId: String, threadId: String): Flow<List<QuestionAnswer>> fun observeAnswers(coupleId: String, threadId: String): Flow<List<QuestionAnswer>>

View File

@ -76,7 +76,7 @@ class QuestionThreadViewModel @Inject constructor(
} }
_uiState.update { it.copy(question = question, isLoading = false) } _uiState.update { it.copy(question = question, isLoading = false) }
val threadId = repository.findOrCreateThreadId(coupleId, questionId, question.category) val threadId = repository.findOrCreateThreadId(coupleId, questionId, question.category, currentUserId)
_uiState.update { it.copy(threadId = threadId) } _uiState.update { it.copy(threadId = threadId) }
launch { launch {

View File

@ -171,13 +171,9 @@ service cloud.firestore {
// - user IDs are immutable (cannot change who is in the couple) // - user IDs are immutable (cannot change who is in the couple)
// - invite code is immutable (cannot change the code) // - invite code is immutable (cannot change the code)
// - createdAt is immutable (cannot change when the couple was formed) // - createdAt is immutable (cannot change when the couple was formed)
// - streakCount and lastStreakAt: server-only (via Cloud Functions or admin SDK) // - All other fields (including streakCount and lastAnsweredAt): both members can update
// - All other fields: both members can update normally
allow update: if isCouplesMember(coupleId) allow update: if isCouplesMember(coupleId)
// Check immutable fields haven't changed && isImmutable(['userIds', 'inviteCode', 'createdAt']);
&& isImmutable(['userIds', 'inviteCode', 'createdAt'])
// streakCount and lastStreakAt must not be modified by clients
&& !request.resource.data.diff(resource.data).affectedKeys().hasAny(['streakCount', 'lastStreakAt']);
// Delete: server-only (admin SDK only). Admin SDK bypasses rules. // Delete: server-only (admin SDK only). Admin SDK bypasses rules.
allow delete: if false; allow delete: if false;