feat: update question thread data source, repository, ViewModel, and Firestore security rules
This commit is contained in:
parent
927e930b79
commit
9e587a23dd
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>>
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue