From e7b45cc84fae7ce0ba9bb0f6f6d711220a7aa2e4 Mon Sep 17 00:00:00 2001 From: null Date: Fri, 19 Jun 2026 20:33:08 -0500 Subject: [PATCH] fix: profile photo temp dir, Firestore rules field-level lockdown (batch v0.2.10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move temp profile photos to filesDir/photos/ subdirectory with mkdirs - Update file_paths.xml to scope FileProvider to photos/ subdirectory - Firestore rules: restrict couple doc updates to only mutable fields (streakCount, lastAnsweredAt, wrappedCoupleKey, kdfSalt, kdfParams, encryptionVersion) — prevents client from overwriting currentQuestionId, activePackId, id --- .../closer/ui/onboarding/CreateProfileScreen.kt | 4 +++- .../app/closer/ui/settings/EditProfileScreen.kt | 4 +++- app/src/main/res/xml/file_paths.xml | 2 +- firestore.rules | 14 ++++++++------ 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/app/closer/ui/onboarding/CreateProfileScreen.kt b/app/src/main/java/app/closer/ui/onboarding/CreateProfileScreen.kt index 128d2642..91a1ffce 100644 --- a/app/src/main/java/app/closer/ui/onboarding/CreateProfileScreen.kt +++ b/app/src/main/java/app/closer/ui/onboarding/CreateProfileScreen.kt @@ -105,7 +105,9 @@ fun CreateProfileScreen( uri?.let { viewModel.setPhotoUri(it.toString()) } } - val cameraFile = remember(context) { File(context.filesDir, "profile_temp.jpg") } + val cameraFile = remember(context) { + File(context.filesDir, "photos/profile_temp.jpg").also { it.parentFile?.mkdirs() } + } val cameraUri = remember(cameraFile) { FileProvider.getUriForFile( context, diff --git a/app/src/main/java/app/closer/ui/settings/EditProfileScreen.kt b/app/src/main/java/app/closer/ui/settings/EditProfileScreen.kt index 6e512684..986221d5 100644 --- a/app/src/main/java/app/closer/ui/settings/EditProfileScreen.kt +++ b/app/src/main/java/app/closer/ui/settings/EditProfileScreen.kt @@ -143,7 +143,9 @@ fun EditProfileContent( uri?.let { viewModel.setPhotoUri(it.toString()) } } - val cameraFile = remember(context) { File(context.filesDir, "profile_edit_temp.jpg") } + val cameraFile = remember(context) { + File(context.filesDir, "photos/profile_edit_temp.jpg").also { it.parentFile?.mkdirs() } + } val cameraUri = remember(cameraFile) { FileProvider.getUriForFile( context, diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml index 600817c7..90464ea2 100644 --- a/app/src/main/res/xml/file_paths.xml +++ b/app/src/main/res/xml/file_paths.xml @@ -1,4 +1,4 @@ - + diff --git a/firestore.rules b/firestore.rules index a55cce9c..c470efc0 100644 --- a/firestore.rules +++ b/firestore.rules @@ -179,16 +179,18 @@ service cloud.firestore { 'wrappedCoupleKey', 'kdfSalt', 'kdfParams', 'encryptionVersion']); // Update: field-level restrictions - // - user IDs are immutable (cannot change who is in the couple) - // - invite code is immutable (cannot change the code) - // - createdAt is immutable (cannot change when the couple was formed) + // - user IDs, invite code, and createdAt are immutable // - encryptionVersion is monotonically non-decreasing (cannot downgrade) - // - wrappedCoupleKey/kdfSalt/kdfParams: mutable by members (passphrase change) - // - All other fields (including streakCount and lastAnsweredAt): both members can update + // - only the explicitly listed mutable fields may change; everything else + // (including currentQuestionId, activePackId, id) is server-only allow update: if isCouplesMember(coupleId) && isImmutable(['userIds', 'inviteCode', 'createdAt']) && (resource.data.encryptionVersion == null - || request.resource.data.encryptionVersion >= resource.data.encryptionVersion); + || request.resource.data.encryptionVersion >= resource.data.encryptionVersion) + && request.resource.data.diff(resource.data).affectedKeys().hasOnly([ + 'streakCount', 'lastAnsweredAt', + 'wrappedCoupleKey', 'kdfSalt', 'kdfParams', 'encryptionVersion' + ]); // Delete: server-only (admin SDK only). Admin SDK bypasses rules. allow delete: if false;