From a169455784dac33bab251f774c259aa9b73d119e Mon Sep 17 00:00:00 2001 From: Radhika canopas <74301808+cp-radhika-s@users.noreply.github.com> Date: Thu, 8 Feb 2024 15:22:55 +0530 Subject: [PATCH] Refactor db structure and firestore security rules (#18) * Setup firestore * Minor map marker fix * Minor login fixes * WIP - firebase rule * Change member and space collection * Refactor auth service * Update firestore.indexes.json * Refactor user location collection * Add security rule * Add security rules * Fixes for security rules --- .firebaserc | 5 + .../auth/methods/SignInMethodViewModel.kt | 6 +- .../auth/phone/SignInWithPhoneViewModel.kt | 6 +- .../PhoneVerificationViewModel.kt | 12 +- .../ui/flow/home/map/component/MapMarker.kt | 2 +- .../space/create/CreateSpaceHomeScreen.kt | 8 + .../space/create/CreateSpaceHomeViewModel.kt | 11 +- .../ui/flow/onboard/OnboardViewModel.kt | 2 +- .../onboard/components/CreateSpaceOnboard.kt | 7 + .../ui/flow/settings/SettingsViewModel.kt | 4 +- .../auth/methods/SignInMethodViewModelTest.kt | 11 +- .../phone/SignInWithPhoneViewModelTest.kt | 17 ++- .../PhoneVerificationViewModelTest.kt | 17 ++- .../catchme/data/models/space/ApiSpace.kt | 2 + .../catchme/data/models/user/ApiUser.kt | 19 ++- .../location/LocationUpdateReceiver.kt | 19 ++- .../catchme/data/service/auth/AuthService.kt | 100 +++--------- .../data/service/auth/FirebaseAuthService.kt | 1 + .../service/location/ApiLocationService.kt | 33 +++- .../data/service/space/ApiSpaceService.kt | 56 ++++--- .../data/service/user/ApiUserService.kt | 78 +++++++++- .../catchme/data/utils/FirestoreExts.kt | 1 + firebase.json | 6 + firestore.indexes.json | 57 +++++++ firestore.rules | 142 ++++++++++++++++++ 25 files changed, 469 insertions(+), 153 deletions(-) create mode 100644 .firebaserc create mode 100644 firebase.json create mode 100644 firestore.indexes.json create mode 100644 firestore.rules diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 00000000..058e9d42 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "catchme-dev-3a803" + } +} diff --git a/app/src/main/java/com/canopas/catchme/ui/flow/auth/methods/SignInMethodViewModel.kt b/app/src/main/java/com/canopas/catchme/ui/flow/auth/methods/SignInMethodViewModel.kt index ff400d5d..1b07739d 100644 --- a/app/src/main/java/com/canopas/catchme/ui/flow/auth/methods/SignInMethodViewModel.kt +++ b/app/src/main/java/com/canopas/catchme/ui/flow/auth/methods/SignInMethodViewModel.kt @@ -37,7 +37,11 @@ class SignInMethodViewModel @Inject constructor( _state.emit(_state.value.copy(showGoogleLoading = true)) try { val firebaseToken = firebaseAuth.signInWithGoogleAuthCredential(account.idToken) - val isNewUSer = authService.verifiedGoogleLogin(firebaseToken, account) + val isNewUSer = authService.verifiedGoogleLogin( + firebaseAuth.currentUserUid, + firebaseToken, + account + ) onSignUp(isNewUSer) _state.emit(_state.value.copy(showGoogleLoading = false)) } catch (e: Exception) { diff --git a/app/src/main/java/com/canopas/catchme/ui/flow/auth/phone/SignInWithPhoneViewModel.kt b/app/src/main/java/com/canopas/catchme/ui/flow/auth/phone/SignInWithPhoneViewModel.kt index 859e74f6..69e30439 100644 --- a/app/src/main/java/com/canopas/catchme/ui/flow/auth/phone/SignInWithPhoneViewModel.kt +++ b/app/src/main/java/com/canopas/catchme/ui/flow/auth/phone/SignInWithPhoneViewModel.kt @@ -51,7 +51,11 @@ class SignInWithPhoneViewModel @Inject constructor( val firebaseIdToken = fbAuthService.signInWithPhoneAuthCredential(result.credential) val isNewUser = - authService.verifiedPhoneLogin(firebaseIdToken, _state.value.phone) + authService.verifiedPhoneLogin( + fbAuthService.currentUserUid, + firebaseIdToken, + _state.value.phone + ) appNavigator.navigateBack( route = AppDestinations.signIn.path, result = mapOf( diff --git a/app/src/main/java/com/canopas/catchme/ui/flow/auth/verification/PhoneVerificationViewModel.kt b/app/src/main/java/com/canopas/catchme/ui/flow/auth/verification/PhoneVerificationViewModel.kt index 293b84cb..9f891f18 100644 --- a/app/src/main/java/com/canopas/catchme/ui/flow/auth/verification/PhoneVerificationViewModel.kt +++ b/app/src/main/java/com/canopas/catchme/ui/flow/auth/verification/PhoneVerificationViewModel.kt @@ -66,7 +66,11 @@ class PhoneVerificationViewModel @Inject constructor( _state.value.otp ) - val isNewUser = authService.verifiedPhoneLogin(firebaseIdToken, _state.value.phone) + val isNewUser = authService.verifiedPhoneLogin( + firebaseAuth.currentUserUid, + firebaseIdToken, + _state.value.phone + ) appNavigator.navigateBack( route = AppDestinations.signIn.path, result = mapOf(KEY_RESULT to RESULT_OKAY, EXTRA_RESULT_IS_NEW_USER to isNewUser) @@ -87,7 +91,11 @@ class PhoneVerificationViewModel @Inject constructor( val firebaseIdToken = firebaseAuth.signInWithPhoneAuthCredential(result.credential) val isNewUser = - authService.verifiedPhoneLogin(firebaseIdToken, _state.value.phone) + authService.verifiedPhoneLogin( + firebaseAuth.currentUserUid, + firebaseIdToken, + _state.value.phone + ) appNavigator.navigateBack( route = AppDestinations.signIn.path, result = mapOf( diff --git a/app/src/main/java/com/canopas/catchme/ui/flow/home/map/component/MapMarker.kt b/app/src/main/java/com/canopas/catchme/ui/flow/home/map/component/MapMarker.kt index 498d064f..9d66e205 100644 --- a/app/src/main/java/com/canopas/catchme/ui/flow/home/map/component/MapMarker.kt +++ b/app/src/main/java/com/canopas/catchme/ui/flow/home/map/component/MapMarker.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset @@ -40,6 +39,7 @@ fun MapMarker( keys = arrayOf(user.id, isSelected), state = markerState, title = user.fullName, + zIndex = if (isSelected) 1f else 0f, anchor = Offset(0.0f, 1f), onClick = { onClick() diff --git a/app/src/main/java/com/canopas/catchme/ui/flow/home/space/create/CreateSpaceHomeScreen.kt b/app/src/main/java/com/canopas/catchme/ui/flow/home/space/create/CreateSpaceHomeScreen.kt index 42e57149..52156153 100644 --- a/app/src/main/java/com/canopas/catchme/ui/flow/home/space/create/CreateSpaceHomeScreen.kt +++ b/app/src/main/java/com/canopas/catchme/ui/flow/home/space/create/CreateSpaceHomeScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel +import com.canopas.catchme.ui.component.AppBanner import com.canopas.catchme.ui.component.CreateSpace import com.canopas.catchme.ui.theme.AppTheme @@ -22,6 +23,13 @@ import com.canopas.catchme.ui.theme.AppTheme fun CreateSpaceHomeScreen() { val viewModel = hiltViewModel() val state by viewModel.state.collectAsState() + + if (state.error != null) { + AppBanner(msg = state.error!!) { + viewModel.resetErrorState() + } + } + Scaffold( topBar = { TopAppBar( diff --git a/app/src/main/java/com/canopas/catchme/ui/flow/home/space/create/CreateSpaceHomeViewModel.kt b/app/src/main/java/com/canopas/catchme/ui/flow/home/space/create/CreateSpaceHomeViewModel.kt index 822bf5e7..e79c5f80 100644 --- a/app/src/main/java/com/canopas/catchme/ui/flow/home/space/create/CreateSpaceHomeViewModel.kt +++ b/app/src/main/java/com/canopas/catchme/ui/flow/home/space/create/CreateSpaceHomeViewModel.kt @@ -36,15 +36,22 @@ class CreateSpaceHomeViewModel @Inject constructor( _state.emit(_state.value.copy(creatingSpace = true)) val invitationCode = spaceRepository.createSpaceAndGetInviteCode(_state.value.spaceName) appNavigator.navigateTo( - AppDestinations.SpaceInvitation.spaceInvitation(invitationCode, _state.value.spaceName).path, + AppDestinations.SpaceInvitation.spaceInvitation( + invitationCode, + _state.value.spaceName + ).path, AppDestinations.createSpace.path, inclusive = true ) } catch (e: Exception) { Timber.e(e, "Unable to create space") - _state.emit(_state.value.copy(error = e.localizedMessage)) + _state.emit(_state.value.copy(creatingSpace = false, error = e.localizedMessage)) } } + + fun resetErrorState() { + _state.value = _state.value.copy(error = null) + } } data class CreateSpaceHomeState( diff --git a/app/src/main/java/com/canopas/catchme/ui/flow/onboard/OnboardViewModel.kt b/app/src/main/java/com/canopas/catchme/ui/flow/onboard/OnboardViewModel.kt index e9af24f5..81909f42 100644 --- a/app/src/main/java/com/canopas/catchme/ui/flow/onboard/OnboardViewModel.kt +++ b/app/src/main/java/com/canopas/catchme/ui/flow/onboard/OnboardViewModel.kt @@ -103,7 +103,7 @@ class OnboardViewModel @Inject constructor( ) } catch (e: Exception) { Timber.e(e, "Unable to create space") - _state.emit(_state.value.copy(error = e.localizedMessage)) + _state.emit(_state.value.copy(creatingSpace = false, error = e.localizedMessage)) } } diff --git a/app/src/main/java/com/canopas/catchme/ui/flow/onboard/components/CreateSpaceOnboard.kt b/app/src/main/java/com/canopas/catchme/ui/flow/onboard/components/CreateSpaceOnboard.kt index 27c7a7bc..cd47343a 100644 --- a/app/src/main/java/com/canopas/catchme/ui/flow/onboard/components/CreateSpaceOnboard.kt +++ b/app/src/main/java/com/canopas/catchme/ui/flow/onboard/components/CreateSpaceOnboard.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import com.canopas.catchme.R +import com.canopas.catchme.ui.component.AppBanner import com.canopas.catchme.ui.component.CreateSpace import com.canopas.catchme.ui.flow.onboard.OnboardItems import com.canopas.catchme.ui.flow.onboard.OnboardViewModel @@ -71,4 +72,10 @@ fun CreateSpaceOnboard() { viewModel.createSpace(spaceName) } } + + if (state.error != null) { + AppBanner(msg = state.error!!) { + viewModel.resetErrorState() + } + } } diff --git a/app/src/main/java/com/canopas/catchme/ui/flow/settings/SettingsViewModel.kt b/app/src/main/java/com/canopas/catchme/ui/flow/settings/SettingsViewModel.kt index 328c58b3..a92fbd83 100644 --- a/app/src/main/java/com/canopas/catchme/ui/flow/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/canopas/catchme/ui/flow/settings/SettingsViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.viewModelScope import com.canopas.catchme.data.models.user.ApiUser import com.canopas.catchme.data.repository.SpaceRepository import com.canopas.catchme.data.service.auth.AuthService -import com.canopas.catchme.data.service.user.ApiUserService import com.canopas.catchme.data.utils.AppDispatcher import com.canopas.catchme.ui.navigation.AppDestinations import com.canopas.catchme.ui.navigation.HomeNavigator @@ -20,7 +19,6 @@ import javax.inject.Inject class SettingsViewModel @Inject constructor( private val navigator: HomeNavigator, private val appNavigator: MainNavigator, - private val userService: ApiUserService, private val authService: AuthService, private val appDispatcher: AppDispatcher, private val spaceRepository: SpaceRepository @@ -37,7 +35,7 @@ class SettingsViewModel @Inject constructor( val user = authService.currentUser _state.emit(_state.value.copy(user = user)) user?.let { - val updatedUser = userService.getUser(it.id) + val updatedUser = authService.getUser() _state.emit(_state.value.copy(user = updatedUser)) updatedUser?.let { authService.saveUser(it) diff --git a/app/src/test/java/com/canopas/catchme/ui/flow/auth/methods/SignInMethodViewModelTest.kt b/app/src/test/java/com/canopas/catchme/ui/flow/auth/methods/SignInMethodViewModelTest.kt index 9b8143a4..db5b8aa0 100644 --- a/app/src/test/java/com/canopas/catchme/ui/flow/auth/methods/SignInMethodViewModelTest.kt +++ b/app/src/test/java/com/canopas/catchme/ui/flow/auth/methods/SignInMethodViewModelTest.kt @@ -70,10 +70,11 @@ class SignInMethodViewModelTest { fun `proceedGoogleSignIn should invoke verifiedGoogleLogin`() = runTest { val account = mock() whenever(account.idToken).thenReturn("token") + whenever(firebaseAuth.currentUserUid).thenReturn("uid") whenever(firebaseAuth.signInWithGoogleAuthCredential("token")) .thenReturn("firebaseToken") viewModel.proceedGoogleSignIn(account) - verify(authService).verifiedGoogleLogin("firebaseToken", account) + verify(authService).verifiedGoogleLogin("uid", "firebaseToken", account) } @Test @@ -82,7 +83,9 @@ class SignInMethodViewModelTest { whenever(account.idToken).thenReturn("token") whenever(firebaseAuth.signInWithGoogleAuthCredential("token")) .thenReturn("firebaseToken") - whenever(authService.verifiedGoogleLogin("firebaseToken", account)) + whenever(firebaseAuth.currentUserUid).thenReturn("uid") + + whenever(authService.verifiedGoogleLogin("uid", "firebaseToken", account)) .thenReturn(false) viewModel.proceedGoogleSignIn(account) verify(navigator).navigateTo("home", "sign-in", true) @@ -92,9 +95,11 @@ class SignInMethodViewModelTest { fun `proceedGoogleSignIn should navigate to onboard screen`() = runTest { val account = mock() whenever(account.idToken).thenReturn("token") + whenever(firebaseAuth.currentUserUid).thenReturn("uid") whenever(firebaseAuth.signInWithGoogleAuthCredential("token")) .thenReturn("firebaseToken") - whenever(authService.verifiedGoogleLogin("firebaseToken", account)) + + whenever(authService.verifiedGoogleLogin("uid", "firebaseToken", account)) .thenReturn(true) viewModel.proceedGoogleSignIn(account) verify(navigator).navigateTo("onboard", "sign-in", true) diff --git a/app/src/test/java/com/canopas/catchme/ui/flow/auth/phone/SignInWithPhoneViewModelTest.kt b/app/src/test/java/com/canopas/catchme/ui/flow/auth/phone/SignInWithPhoneViewModelTest.kt index 78b25a57..171bf7a0 100644 --- a/app/src/test/java/com/canopas/catchme/ui/flow/auth/phone/SignInWithPhoneViewModelTest.kt +++ b/app/src/test/java/com/canopas/catchme/ui/flow/auth/phone/SignInWithPhoneViewModelTest.kt @@ -61,10 +61,13 @@ class SignInWithPhoneViewModelTest { val context = mock() val credential = mock() viewModel.onPhoneChange("1234567890") + whenever(firebaseAuth.currentUserUid).thenReturn("uid") whenever(firebaseAuth.verifyPhoneNumber(context, "1234567890")) .thenReturn(flowOf(PhoneAuthState.VerificationCompleted(credential))) whenever(firebaseAuth.signInWithPhoneAuthCredential(credential)).thenReturn("firebaseIdToken") - whenever(authService.verifiedPhoneLogin("firebaseIdToken", "1234567890")).thenReturn(true) + whenever(authService.verifiedPhoneLogin("uid", "firebaseIdToken", "1234567890")).thenReturn( + true + ) viewModel.verifyPhoneNumber(context) assert(!viewModel.state.value.verifying) } @@ -74,10 +77,13 @@ class SignInWithPhoneViewModelTest { val context = mock() val credential = mock() viewModel.onPhoneChange("1234567890") + whenever(firebaseAuth.currentUserUid).thenReturn("uid") whenever(firebaseAuth.verifyPhoneNumber(context, "1234567890")) .thenReturn(flowOf(PhoneAuthState.VerificationCompleted(credential))) whenever(firebaseAuth.signInWithPhoneAuthCredential(credential)).thenReturn("firebaseIdToken") - whenever(authService.verifiedPhoneLogin("firebaseIdToken", "1234567890")).thenReturn(true) + whenever(authService.verifiedPhoneLogin("uid", "firebaseIdToken", "1234567890")).thenReturn( + true + ) viewModel.verifyPhoneNumber(context) verify(navigator).navigateBack( @@ -91,12 +97,15 @@ class SignInWithPhoneViewModelTest { val context = mock() val credential = mock() viewModel.onPhoneChange("1234567890") + whenever(firebaseAuth.currentUserUid).thenReturn("uid") whenever(firebaseAuth.verifyPhoneNumber(context, "1234567890")) .thenReturn(flowOf(PhoneAuthState.VerificationCompleted(credential))) whenever(firebaseAuth.signInWithPhoneAuthCredential(credential)).thenReturn("firebaseIdToken") - whenever(authService.verifiedPhoneLogin("firebaseIdToken", "1234567890")).thenReturn(true) + whenever(authService.verifiedPhoneLogin("uid", "firebaseIdToken", "1234567890")).thenReturn( + true + ) viewModel.verifyPhoneNumber(context) - verify(authService).verifiedPhoneLogin("firebaseIdToken", "1234567890") + verify(authService).verifiedPhoneLogin("uid", "firebaseIdToken", "1234567890") } @Test diff --git a/app/src/test/java/com/canopas/catchme/ui/flow/auth/verification/PhoneVerificationViewModelTest.kt b/app/src/test/java/com/canopas/catchme/ui/flow/auth/verification/PhoneVerificationViewModelTest.kt index a706b4ec..c533a636 100644 --- a/app/src/test/java/com/canopas/catchme/ui/flow/auth/verification/PhoneVerificationViewModelTest.kt +++ b/app/src/test/java/com/canopas/catchme/ui/flow/auth/verification/PhoneVerificationViewModelTest.kt @@ -24,7 +24,6 @@ import org.junit.Test import org.mockito.kotlin.clearInvocations import org.mockito.kotlin.doSuspendableAnswer import org.mockito.kotlin.mock -import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever @@ -126,11 +125,12 @@ class PhoneVerificationViewModelTest { "12356" ) ).thenReturn("firebaseIdToken") + whenever(firebaseAuth.currentUserUid).thenReturn("uid") viewModel.updateOTP("12356") viewModel.verifyOTP() - verify(authService).verifiedPhoneLogin("firebaseIdToken", "1234567890") + verify(authService).verifiedPhoneLogin("uid", "firebaseIdToken", "1234567890") } @Test @@ -141,7 +141,10 @@ class PhoneVerificationViewModelTest { "12356" ) ).thenReturn("firebaseIdToken") - whenever(authService.verifiedPhoneLogin("firebaseIdToken", "1234567890")).thenReturn(false) + whenever(firebaseAuth.currentUserUid).thenReturn("uid") + whenever(authService.verifiedPhoneLogin("uid", "firebaseIdToken", "1234567890")).thenReturn( + false + ) viewModel.updateOTP("12356") viewModel.verifyOTP() @@ -189,7 +192,8 @@ class PhoneVerificationViewModelTest { val credential = mock() whenever(firebaseAuth.verifyPhoneNumber(context, "1234567890")) .thenReturn(flowOf(PhoneAuthState.VerificationCompleted(credential))) - whenever(authService.verifiedPhoneLogin("firebaseIdToken", "1234567890")).thenReturn(false) + whenever(firebaseAuth.currentUserUid).thenReturn("uid") + whenever(authService.verifiedPhoneLogin("uid", "firebaseIdToken", "1234567890")).thenReturn(false) whenever(firebaseAuth.signInWithPhoneAuthCredential(credential)).thenReturn("firebaseIdToken") viewModel.resendCode(context) @@ -205,11 +209,12 @@ class PhoneVerificationViewModelTest { val credential = mock() whenever(firebaseAuth.verifyPhoneNumber(context, "1234567890")) .thenReturn(flowOf(PhoneAuthState.VerificationCompleted(credential))) + whenever(firebaseAuth.currentUserUid).thenReturn("uid") whenever(firebaseAuth.signInWithPhoneAuthCredential(credential)).thenReturn("firebaseIdToken") - whenever(authService.verifiedPhoneLogin("firebaseIdToken", "1234567890")).thenReturn(true) + whenever(authService.verifiedPhoneLogin("uid", "firebaseIdToken", "1234567890")).thenReturn(true) viewModel.resendCode(context) - verify(authService).verifiedPhoneLogin("firebaseIdToken", "1234567890") + verify(authService).verifiedPhoneLogin("uid", "firebaseIdToken", "1234567890") } @Test diff --git a/data/src/main/java/com/canopas/catchme/data/models/space/ApiSpace.kt b/data/src/main/java/com/canopas/catchme/data/models/space/ApiSpace.kt index f146b64c..d394519c 100644 --- a/data/src/main/java/com/canopas/catchme/data/models/space/ApiSpace.kt +++ b/data/src/main/java/com/canopas/catchme/data/models/space/ApiSpace.kt @@ -1,6 +1,7 @@ package com.canopas.catchme.data.models.space import androidx.annotation.Keep +import com.google.firebase.firestore.Exclude import java.util.UUID import java.util.concurrent.TimeUnit @@ -32,6 +33,7 @@ data class ApiSpaceInvitation( val code: String = "", val created_at: Long? = System.currentTimeMillis() ) { + @get:Exclude val isExpired: Boolean get() { if (created_at == null) return true diff --git a/data/src/main/java/com/canopas/catchme/data/models/user/ApiUser.kt b/data/src/main/java/com/canopas/catchme/data/models/user/ApiUser.kt index cafc80ab..41c82001 100644 --- a/data/src/main/java/com/canopas/catchme/data/models/user/ApiUser.kt +++ b/data/src/main/java/com/canopas/catchme/data/models/user/ApiUser.kt @@ -1,6 +1,7 @@ package com.canopas.catchme.data.models.user import androidx.annotation.Keep +import com.google.firebase.firestore.Exclude import java.util.UUID const val LOGIN_TYPE_GOOGLE = 1 @@ -13,13 +14,15 @@ data class ApiUser( val phone: String? = null, val email: String? = null, val auth_type: Int? = null, - val first_name: String? = null, - val last_name: String? = null, - val profile_image: String? = null, + val first_name: String? = "", + val last_name: String? = "", + val profile_image: String? = "", val location_enabled: Boolean = true, + val space_ids: List? = emptyList(), val provider_firebase_id_token: String? = null, val created_at: Long? = System.currentTimeMillis() ) { + @get:Exclude val fullName: String get() = "$first_name $last_name" } @@ -27,12 +30,12 @@ data class ApiUser( data class ApiUserSession( val id: String = UUID.randomUUID().toString(), val user_id: String, - val device_id: String? = null, - val fcm_token: String? = null, - val device_name: String? = null, + val device_id: String? = "", + val fcm_token: String? = "", + val device_name: String? = "", val platform: Int = LOGIN_DEVICE_TYPE_ANDROID, val session_active: Boolean = true, - val app_version: Long? = null, - val battery_status: String? = null, + val app_version: Long? = 0, + val battery_status: String? = "", val created_at: Long? = System.currentTimeMillis() ) diff --git a/data/src/main/java/com/canopas/catchme/data/receiver/location/LocationUpdateReceiver.kt b/data/src/main/java/com/canopas/catchme/data/receiver/location/LocationUpdateReceiver.kt index 01948587..2920a69a 100644 --- a/data/src/main/java/com/canopas/catchme/data/receiver/location/LocationUpdateReceiver.kt +++ b/data/src/main/java/com/canopas/catchme/data/receiver/location/LocationUpdateReceiver.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +import timber.log.Timber import java.util.Date import javax.inject.Inject @@ -29,13 +30,17 @@ class LocationUpdateReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { LocationResult.extractResult(intent)?.let { locationResult -> scope.launch { - locationResult.locations.map { - locationService.saveCurrentLocation( - authService.currentUser?.id ?: "", - it.latitude, - it.longitude, - Date().time - ) + try { + locationResult.locations.map { + locationService.saveCurrentLocation( + authService.currentUser?.id ?: "", + it.latitude, + it.longitude, + Date().time + ) + } + } catch (e: Exception) { + Timber.e(e, "Error while saving location") } } } diff --git a/data/src/main/java/com/canopas/catchme/data/service/auth/AuthService.kt b/data/src/main/java/com/canopas/catchme/data/service/auth/AuthService.kt index 712c5e42..437ed7e3 100644 --- a/data/src/main/java/com/canopas/catchme/data/service/auth/AuthService.kt +++ b/data/src/main/java/com/canopas/catchme/data/service/auth/AuthService.kt @@ -2,90 +2,42 @@ package com.canopas.catchme.data.service.auth import com.canopas.catchme.data.models.user.ApiUser import com.canopas.catchme.data.models.user.ApiUserSession -import com.canopas.catchme.data.models.user.LOGIN_TYPE_GOOGLE -import com.canopas.catchme.data.models.user.LOGIN_TYPE_PHONE +import com.canopas.catchme.data.service.user.ApiUserService import com.canopas.catchme.data.storage.UserPreferences -import com.canopas.catchme.data.utils.Device -import com.canopas.catchme.data.utils.FirestoreConst.FIRESTORE_COLLECTION_USERS -import com.canopas.catchme.data.utils.FirestoreConst.FIRESTORE_COLLECTION_USER_SESSIONS import com.google.android.gms.auth.api.signin.GoogleSignInAccount -import com.google.firebase.firestore.FirebaseFirestore -import com.google.firebase.firestore.QueryDocumentSnapshot -import kotlinx.coroutines.tasks.await import javax.inject.Inject import javax.inject.Singleton @Singleton class AuthService @Inject constructor( - db: FirebaseFirestore, - private val device: Device, - private val userPreferences: UserPreferences + private val userPreferences: UserPreferences, + private val apiUserService: ApiUserService ) { - private val userRef = db.collection(FIRESTORE_COLLECTION_USERS) - private val sessionRef = db.collection(FIRESTORE_COLLECTION_USER_SESSIONS) - - suspend fun verifiedPhoneLogin(firebaseToken: String?, phoneNumber: String): Boolean { - val snapshot = userRef.whereEqualTo("phone", phoneNumber).get().await().firstOrNull() - return processLogin(firebaseToken, null, phoneNumber, snapshot) + suspend fun verifiedPhoneLogin( + uid: String?, + firebaseToken: String?, + phoneNumber: String + ): Boolean { + return processLogin(uid, firebaseToken, null, phoneNumber) } - suspend fun verifiedGoogleLogin(firebaseToken: String?, account: GoogleSignInAccount): Boolean { - val snapshot = userRef.whereEqualTo("email", account.email).get().await().firstOrNull() - return processLogin(firebaseToken, account, null, snapshot) + suspend fun verifiedGoogleLogin( + uid: String?, + firebaseToken: String?, + account: GoogleSignInAccount + ): Boolean { + return processLogin(uid, firebaseToken, account, null) } private suspend fun processLogin( + uid: String?, firebaseToken: String?, account: GoogleSignInAccount? = null, - phoneNumber: String? = null, - snapshot: QueryDocumentSnapshot? + phoneNumber: String? = null ): Boolean { - val isNewUser = snapshot?.data == null - if (isNewUser) { - val userDocRef = userRef.document() - val userId = userDocRef.id - val user = ApiUser( - id = userId, - email = account?.email, - phone = phoneNumber, - auth_type = if (account != null) LOGIN_TYPE_GOOGLE else LOGIN_TYPE_PHONE, - first_name = account?.givenName, - last_name = account?.familyName, - provider_firebase_id_token = firebaseToken, - profile_image = account?.photoUrl?.toString() - ) - val sessionDocRef = sessionRef.document() - val session = ApiUserSession( - id = sessionDocRef.id, - user_id = userId, - device_id = device.getId(), - device_name = device.deviceName(), - session_active = true, - app_version = device.versionCode, - battery_status = null - ) - - userDocRef.set(user).await() - sessionDocRef.set(session).await() - saveUser(user, session) - } else { - val docId = snapshot!!.id - val sessionDocRef = sessionRef.document() - - val session = ApiUserSession( - id = sessionDocRef.id, - user_id = docId, - device_id = device.getId(), - device_name = device.deviceName(), - session_active = true, - app_version = device.versionCode, - battery_status = null - ) - sessionDocRef.set(session).await() - val currentUser = snapshot.toObject(ApiUser::class.java) - saveUser(currentUser, session) - } - + val (isNewUser, user, session) = + apiUserService.saveUser(uid, firebaseToken, account, phoneNumber) + saveUser(user, session) return isNewUser } @@ -97,7 +49,7 @@ class AuthService @Inject constructor( userPreferences.currentUser = newUser } - var currentUserSession: ApiUserSession? + private var currentUserSession: ApiUserSession? get() { return userPreferences.currentUserSession } @@ -119,7 +71,7 @@ class AuthService @Inject constructor( } suspend fun updateUser(user: ApiUser) { - userRef.document(user.id).set(user).await() + apiUserService.updateUser(user) currentUser = user } @@ -132,13 +84,9 @@ class AuthService @Inject constructor( suspend fun deleteAccount() { val currentUser = currentUser ?: return - userRef.document(currentUser.id).delete().await() - sessionRef.whereEqualTo("user_id", currentUser.id).get().await().documents.forEach { - it.reference.delete().await() - } + apiUserService.deleteUser(currentUser.id) signOut() } - suspend fun getUser(): ApiUser? = - userRef.document(currentUser?.id ?: "").get().await().toObject(ApiUser::class.java) + suspend fun getUser(): ApiUser? = apiUserService.getUser(currentUser?.id ?: "") } diff --git a/data/src/main/java/com/canopas/catchme/data/service/auth/FirebaseAuthService.kt b/data/src/main/java/com/canopas/catchme/data/service/auth/FirebaseAuthService.kt index 6727c843..5dcff9b5 100644 --- a/data/src/main/java/com/canopas/catchme/data/service/auth/FirebaseAuthService.kt +++ b/data/src/main/java/com/canopas/catchme/data/service/auth/FirebaseAuthService.kt @@ -19,6 +19,7 @@ import javax.inject.Inject class FirebaseAuthService @Inject constructor( private val firebaseAuth: FirebaseAuth ) { + val currentUserUid get() = firebaseAuth.currentUser?.uid fun verifyPhoneNumber( context: Context, diff --git a/data/src/main/java/com/canopas/catchme/data/service/location/ApiLocationService.kt b/data/src/main/java/com/canopas/catchme/data/service/location/ApiLocationService.kt index eac8e6f0..0f1ad3b2 100644 --- a/data/src/main/java/com/canopas/catchme/data/service/location/ApiLocationService.kt +++ b/data/src/main/java/com/canopas/catchme/data/service/location/ApiLocationService.kt @@ -11,9 +11,28 @@ import javax.inject.Singleton @Singleton class ApiLocationService @Inject constructor( - private val db: FirebaseFirestore + db: FirebaseFirestore, + private val locationManager: LocationManager ) { - private val locationRef = db.collection(FirestoreConst.FIRESTORE_COLLECTION_USER_LOCATIONS) + private val userRef = db.collection(FirestoreConst.FIRESTORE_COLLECTION_USERS) + private fun locationRef(userId: String) = userRef.document(userId).collection(FirestoreConst.FIRESTORE_COLLECTION_USER_LOCATIONS) + + suspend fun saveLastKnownLocation( + userId: String + ) { + val lastLocation = locationManager.getLastLocation() ?: return + val docRef = locationRef(userId).document() + + val location = ApiLocation( + id = docRef.id, + user_id = userId, + latitude = lastLocation.latitude, + longitude = lastLocation.longitude, + created_at = System.currentTimeMillis() + ) + + docRef.set(location).await() + } suspend fun saveCurrentLocation( userId: String, @@ -21,7 +40,7 @@ class ApiLocationService @Inject constructor( longitude: Double, recordedAt: Long ) { - val docRef = locationRef.document() + val docRef = locationRef(userId).document() val location = ApiLocation( id = docRef.id, @@ -31,22 +50,22 @@ class ApiLocationService @Inject constructor( created_at = recordedAt ) - locationRef.document(location.id).set(location).await() + docRef.set(location).await() } suspend fun getCurrentLocation(userId: String) = - locationRef.whereEqualTo("user_id", userId) + locationRef(userId).whereEqualTo("user_id", userId) .orderBy("created_at", Query.Direction.DESCENDING).limit(1) .snapshotFlow(ApiLocation::class.java) fun getLocationHistoryQuery(userId: String, from: Long, to: Long) = - locationRef.whereEqualTo("user_id", userId) + locationRef(userId).whereEqualTo("user_id", userId) .whereGreaterThanOrEqualTo("created_at", from) .whereLessThan("created_at", to) .orderBy("created_at", Query.Direction.DESCENDING).limit(8) suspend fun deleteLocations(userId: String) { - locationRef.whereEqualTo("user_id", userId).get().await().documents.forEach { + locationRef(userId).whereEqualTo("user_id", userId).get().await().documents.forEach { it.reference.delete().await() } } diff --git a/data/src/main/java/com/canopas/catchme/data/service/space/ApiSpaceService.kt b/data/src/main/java/com/canopas/catchme/data/service/space/ApiSpaceService.kt index d4063c46..312bc5a4 100644 --- a/data/src/main/java/com/canopas/catchme/data/service/space/ApiSpaceService.kt +++ b/data/src/main/java/com/canopas/catchme/data/service/space/ApiSpaceService.kt @@ -5,8 +5,10 @@ import com.canopas.catchme.data.models.space.ApiSpaceMember import com.canopas.catchme.data.models.space.SPACE_MEMBER_ROLE_ADMIN import com.canopas.catchme.data.models.space.SPACE_MEMBER_ROLE_MEMBER import com.canopas.catchme.data.service.auth.AuthService +import com.canopas.catchme.data.service.user.ApiUserService import com.canopas.catchme.data.utils.FirestoreConst import com.canopas.catchme.data.utils.FirestoreConst.FIRESTORE_COLLECTION_SPACES +import com.canopas.catchme.data.utils.FirestoreConst.FIRESTORE_COLLECTION_SPACE_MEMBERS import com.canopas.catchme.data.utils.snapshotFlow import com.google.firebase.firestore.FirebaseFirestore import kotlinx.coroutines.tasks.await @@ -15,11 +17,13 @@ import javax.inject.Singleton @Singleton class ApiSpaceService @Inject constructor( - db: FirebaseFirestore, - private val authService: AuthService + private val db: FirebaseFirestore, + private val authService: AuthService, + private val apiUserService: ApiUserService ) { private val spaceRef = db.collection(FIRESTORE_COLLECTION_SPACES) - private val spaceMemberRef = db.collection(FirestoreConst.FIRESTORE_COLLECTION_SPACE_MEMBERS) + private fun spaceMemberRef(spaceId: String) = + spaceRef.document(spaceId).collection(FirestoreConst.FIRESTORE_COLLECTION_SPACE_MEMBERS) suspend fun createSpace(spaceName: String): String { val docRef = spaceRef.document() @@ -33,35 +37,28 @@ class ApiSpaceService @Inject constructor( suspend fun joinSpace(spaceId: String, role: Int = SPACE_MEMBER_ROLE_MEMBER) { val userId = authService.currentUser?.id ?: "" - addMember(spaceId, userId, role) - } - - private suspend fun addMember( - spaceId: String, - userId: String, - role: Int = SPACE_MEMBER_ROLE_MEMBER - ) { - val docRef = spaceMemberRef.document() - val member = ApiSpaceMember( - id = docRef.id, - space_id = spaceId, - user_id = userId, - role = role, - location_enabled = true - ) - - docRef.set(member).await() + spaceMemberRef(spaceId) + .document(userId).also { + val member = ApiSpaceMember( + space_id = spaceId, + user_id = userId, + role = role, + location_enabled = true + ) + it.set(member).await() + } + apiUserService.addSpaceId(userId, spaceId) } suspend fun enableLocation(spaceId: String, userId: String, enable: Boolean) { - spaceMemberRef.whereEqualTo("space_id", spaceId) + spaceMemberRef(spaceId) .whereEqualTo("user_id", userId).get() .await().documents.firstOrNull() ?.reference?.update("location_enabled", enable)?.await() } suspend fun isMember(spaceId: String, userId: String): Boolean { - val query = spaceMemberRef.whereEqualTo("space_id", spaceId) + val query = spaceMemberRef(spaceId) .whereEqualTo("user_id", userId) val result = query.get().await() return result.documents.isNotEmpty() @@ -70,14 +67,15 @@ class ApiSpaceService @Inject constructor( suspend fun getSpace(spaceId: String) = spaceRef.document(spaceId).get().await().toObject(ApiSpace::class.java) - suspend fun getSpaceMemberByUserId(userId: String) = - spaceMemberRef.whereEqualTo("user_id", userId).snapshotFlow(ApiSpaceMember::class.java) + fun getSpaceMemberByUserId(userId: String) = + db.collectionGroup(FIRESTORE_COLLECTION_SPACE_MEMBERS).whereEqualTo("user_id", userId) + .snapshotFlow(ApiSpaceMember::class.java) - suspend fun getMemberBySpaceId(spaceId: String) = - spaceMemberRef.whereEqualTo("space_id", spaceId).snapshotFlow(ApiSpaceMember::class.java) + fun getMemberBySpaceId(spaceId: String) = + spaceMemberRef(spaceId).snapshotFlow(ApiSpaceMember::class.java) suspend fun deleteMembers(spaceId: String) { - spaceMemberRef.whereEqualTo("space_id", spaceId).get().await().documents.forEach { doc -> + spaceMemberRef(spaceId).get().await().documents.forEach { doc -> doc.reference.delete().await() } } @@ -88,7 +86,7 @@ class ApiSpaceService @Inject constructor( } suspend fun removeUserFromSpace(spaceId: String, userId: String) { - spaceMemberRef.whereEqualTo("space_id", spaceId) + spaceMemberRef(spaceId) .whereEqualTo("user_id", userId).get().await().documents.forEach { it.reference.delete().await() } diff --git a/data/src/main/java/com/canopas/catchme/data/service/user/ApiUserService.kt b/data/src/main/java/com/canopas/catchme/data/service/user/ApiUserService.kt index ad9cd8c8..eea7dcf0 100644 --- a/data/src/main/java/com/canopas/catchme/data/service/user/ApiUserService.kt +++ b/data/src/main/java/com/canopas/catchme/data/service/user/ApiUserService.kt @@ -1,7 +1,15 @@ package com.canopas.catchme.data.service.user import com.canopas.catchme.data.models.user.ApiUser +import com.canopas.catchme.data.models.user.ApiUserSession +import com.canopas.catchme.data.models.user.LOGIN_TYPE_GOOGLE +import com.canopas.catchme.data.models.user.LOGIN_TYPE_PHONE +import com.canopas.catchme.data.service.location.ApiLocationService +import com.canopas.catchme.data.utils.Device +import com.canopas.catchme.data.utils.FirestoreConst import com.canopas.catchme.data.utils.FirestoreConst.FIRESTORE_COLLECTION_USERS +import com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.google.firebase.firestore.FieldValue import com.google.firebase.firestore.FirebaseFirestore import kotlinx.coroutines.tasks.await import javax.inject.Inject @@ -9,12 +17,78 @@ import javax.inject.Singleton @Singleton class ApiUserService @Inject constructor( - db: FirebaseFirestore - + db: FirebaseFirestore, + private val device: Device, + private val locationService: ApiLocationService ) { private val userRef = db.collection(FIRESTORE_COLLECTION_USERS) + private fun sessionRef(userId: String) = + userRef.document(userId).collection(FirestoreConst.FIRESTORE_COLLECTION_USER_SESSIONS) suspend fun getUser(userId: String): ApiUser? { return userRef.document(userId).get().await().toObject(ApiUser::class.java) } + + suspend fun saveUser( + uid: String?, + firebaseToken: String?, + account: GoogleSignInAccount? = null, + phoneNumber: String? = null + ): Triple { + val savedUser = if (uid.isNullOrEmpty()) null else getUser(uid) + val isExists = savedUser != null + + if (isExists) { + val sessionDocRef = sessionRef(savedUser!!.id).document() + val session = ApiUserSession( + id = sessionDocRef.id, + user_id = savedUser.id, + device_id = device.getId(), + device_name = device.deviceName(), + session_active = true, + app_version = device.versionCode + ) + sessionDocRef.set(session).await() + return Triple(false, savedUser, session) + } else { + val user = ApiUser( + id = uid!!, + email = account?.email ?: "", + phone = phoneNumber ?: "", + auth_type = if (account != null) LOGIN_TYPE_GOOGLE else LOGIN_TYPE_PHONE, + first_name = account?.givenName ?: "", + last_name = account?.familyName ?: "", + provider_firebase_id_token = firebaseToken, + profile_image = account?.photoUrl?.toString() ?: "" + ) + userRef.document(uid).set(user).await() + val sessionDocRef = sessionRef(user.id).document() + val session = ApiUserSession( + id = sessionDocRef.id, + user_id = user.id, + device_id = device.getId(), + device_name = device.deviceName(), + session_active = true, + app_version = device.versionCode + ) + sessionDocRef.set(session).await() + locationService.saveLastKnownLocation(user.id) + return Triple(true, user, session) + } + } + + suspend fun deleteUser(userId: String) { + sessionRef(userId).whereEqualTo("user_id", userId).get().await().documents.forEach { + it.reference.delete().await() + } + userRef.document(userId).delete().await() + } + + suspend fun updateUser(user: ApiUser) { + userRef.document(user.id).set(user).await() + } + + suspend fun addSpaceId(userId: String, spaceId: String) { + userRef.document(userId).update("space_ids", FieldValue.arrayUnion(spaceId)).await() + } } diff --git a/data/src/main/java/com/canopas/catchme/data/utils/FirestoreExts.kt b/data/src/main/java/com/canopas/catchme/data/utils/FirestoreExts.kt index 2accc84d..00218def 100644 --- a/data/src/main/java/com/canopas/catchme/data/utils/FirestoreExts.kt +++ b/data/src/main/java/com/canopas/catchme/data/utils/FirestoreExts.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.callbackFlow fun Query.snapshotFlow(dataType: Class): Flow> = callbackFlow { val listenerRegistration = addSnapshotListener { value, error -> if (error != null) { + trySend(emptyList()) close() return@addSnapshotListener } diff --git a/firebase.json b/firebase.json new file mode 100644 index 00000000..d4d918a8 --- /dev/null +++ b/firebase.json @@ -0,0 +1,6 @@ +{ + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json" + } +} diff --git a/firestore.indexes.json b/firestore.indexes.json new file mode 100644 index 00000000..c120b470 --- /dev/null +++ b/firestore.indexes.json @@ -0,0 +1,57 @@ +{ + "indexes": [ + { + "collectionGroup": "user_locations", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "user_id", + "order": "ASCENDING" + }, + { + "fieldPath": "created_at", + "order": "ASCENDING" + } + ] + }, + { + "collectionGroup": "user_locations", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "user_id", + "order": "ASCENDING" + }, + { + "fieldPath": "created_at", + "order": "DESCENDING" + } + ] + } + ], + "fieldOverrides": [ + { + "collectionGroup": "space_members", + "fieldPath": "user_id", + "ttl": false, + "indexes": [ + { + "order": "ASCENDING", + "queryScope": "COLLECTION" + }, + { + "order": "DESCENDING", + "queryScope": "COLLECTION" + }, + { + "arrayConfig": "CONTAINS", + "queryScope": "COLLECTION" + }, + { + "order": "ASCENDING", + "queryScope": "COLLECTION_GROUP" + } + ] + } + ] +} diff --git a/firestore.rules b/firestore.rules new file mode 100644 index 00000000..907d6dd2 --- /dev/null +++ b/firestore.rules @@ -0,0 +1,142 @@ +rules_version = '2'; + +service cloud.firestore { + match /databases/{database}/documents { + + function isAuthorized() { + return request.auth != null; + } + + function readUserLocation(){ + let requestedUserSpaceIds = get(/databases/$(database)/documents/users/$(request.auth.uid)).data.space_ids; + let resourceUserSpaceIds = get(/databases/$(database)/documents/users/$(resource.data.user_id)).data.space_ids; + return requestedUserSpaceIds.hasAny(resourceUserSpaceIds); + } + + match /users/{docId} { + allow create : if isAuthorized() && request.auth.uid == docId && + request.resource.data.keys().hasAll(["id", "auth_type", "location_enabled","provider_firebase_id_token","created_at"]) && + request.resource.data.keys().hasAny(["email", "phone"]) && + request.resource.data.id is string && + request.resource.data.auth_type is int && + (request.resource.data.auth_type == 1 || request.resource.data.auth_type == 2) && + request.resource.data.location_enabled is bool && + request.resource.data.provider_firebase_id_token is string && + request.resource.data.created_at is int && + request.resource.data.get('first_name', '') is string && + request.resource.data.get('phone', '') is string && + request.resource.data.get('email', '') is string && + request.resource.data.get('last_name', '') is string && + request.resource.data.get('profile_image', '') is string && + request.resource.data.get('space_ids', []) is list; + + allow update: if isAuthorized() && request.auth.uid == resource.data.id && + request.resource.data.diff(resource.data).affectedKeys().hasAny(['first_name', 'last_name', 'profile_image', 'location_enabled', 'space_ids']) && + request.resource.data.first_name is string && + request.resource.data.get('last_name', '') is string && + request.resource.data.location_enabled is bool && + request.resource.data.get('space_ids', []) is list; + + allow delete: if isAuthorized() && request.auth.uid == resource.data.id; + allow read: if isAuthorized() && (request.auth.uid == docId || + get(/databases/$(database)/documents/users/$(request.auth.uid)).data.space_ids.hasAny(resource.data.space_ids)); + + match /user_locations/{docId} { + allow read: if isAuthorized() && readUserLocation(); + allow update: if false; + allow delete: if isAuthorized() && request.auth.uid == resource.data.user_id; + allow create: if isAuthorized() && request.auth.uid == request.resource.data.user_id && + request.resource.data.keys().hasAll(["id", "user_id", "latitude","longitude","created_at"]) && + request.resource.data.id is string && + request.resource.data.user_id is string && + request.resource.data.latitude is number && + request.resource.data.longitude is number && + request.resource.data.created_at is int; + } + + match /user_sessions/{docId} { + allow read: if isAuthorized() && request.auth.uid == resource.data.user_id; + allow create : if isAuthorized() && request.auth.uid == request.resource.data.user_id && + request.resource.data.keys().hasAll(["id", "user_id", "device_id", + "fcm_token","device_name","platform","session_active","app_version","created_at"]) && + request.resource.data.id is string && + request.resource.data.user_id is string && + request.resource.data.device_id is string && + request.resource.data.fcm_token is string && + request.resource.data.device_name is string && + request.resource.data.platform is int && + request.resource.data.platform == 1 && + request.resource.data.session_active is bool && + request.resource.data.app_version is int && + request.resource.data.created_at is int; + allow delete: if isAuthorized() && request.auth.uid == resource.data.user_id; + allow update: if false; + } + } + + function isSpaceAdmin(spaceId) { + let adminId = get(/databases/$(database)/documents/spaces/$(spaceId)).data.admin_id; + return request.auth.uid == adminId; + } + + function isSpaceMember(spaceId) { + let isMember = exists(/databases/$(database)/documents/spaces/$(spaceId)/space_members/$(request.auth.uid)); + return isMember; + } + + match /spaces/{docId} { + allow read: if true; + allow delete: if isAuthorized() && request.auth.uid == resource.data.admin_id; + allow update: if isAuthorized() && request.auth.uid == resource.data.admin_id && + request.resource.data.diff(resource.data).affectedKeys().hasOnly(["name"]) && + request.resource.data.name is string; + allow create: if isAuthorized() && + request.resource.data.keys().hasAll(["id", "admin_id", "name", "created_at"]) && + request.resource.data.id is string && + request.resource.data.admin_id is string && + request.resource.data.name is string && + request.resource.data.created_at is int; + + + } + + match /{path=**}/space_members/{member} { + allow read: if isAuthorized() && (request.auth.uid == resource.data.user_id || isSpaceMember(resource.data.space_id)); + allow write: if false; + } + + match /spaces/{spaceId}/space_members/{member} { + allow read: if isAuthorized() && (request.auth.uid == resource.data.user_id || isSpaceMember(spaceId)); + allow delete: if isAuthorized() && (isSpaceAdmin(resource.data.space_id) || request.auth.uid == resource.data.user_id); + allow update: if isAuthorized() && request.auth.uid == resource.data.user_id && + request.resource.data.diff(resource.data).affectedKeys().hasOnly(["location_enabled"])&& + request.resource.data.location_enabled is bool; + allow create: if isAuthorized() && (isSpaceAdmin(request.resource.data.space_id) || request.auth.uid == request.resource.data.user_id) && + request.resource.data.keys().hasAll(["id", "space_id", "user_id", "role", "location_enabled", "created_at"]) && + request.resource.data.id is string && + request.resource.data.space_id is string && + request.resource.data.user_id is string && + request.resource.data.role is int && + (request.resource.data.role == 1 || request.resource.data.role == 2) && + request.resource.data.location_enabled is bool && + request.resource.data.created_at is int; + } + + match /space_invitations/{docId} { + allow read: if isAuthorized(); + allow delete: if isAuthorized() && isSpaceAdmin(resource.data.space_id); + allow update: if isAuthorized() && isSpaceAdmin(resource.data.space_id) && + request.resource.data.diff(resource.data).affectedKeys().hasOnly(["code"]) && + request.resource.data.code is string && + request.resource.data.code.length == 6; + + allow create: if isAuthorized() && isSpaceAdmin(request.resource.data.space_id) && + request.resource.data.keys().hasAll(["id", "code", "space_id", "created_at"]) && + request.resource.data.id is string && + request.resource.data.space_id is string && + request.resource.data.code is string && + request.resource.data.code.size() == 6 && + request.resource.data.created_at is int; + } + } +} \ No newline at end of file