From 99ce18a435c5e6b30ea14fc290e6cf044c15be4d Mon Sep 17 00:00:00 2001 From: androidlover5842 Date: Mon, 2 Feb 2026 05:39:20 +0530 Subject: [PATCH] remove auth boilerplate --- .../com/android/trisolarispms/MainActivity.kt | 125 ++++++------------ .../android/trisolarispms/auth/AuthzPolicy.kt | 59 +++++++++ .../com/android/trisolarispms/auth/Role.kt | 27 ++++ .../trisolarispms/ui/auth/AuthUiState.kt | 4 +- .../trisolarispms/ui/auth/AuthViewModel.kt | 72 +++++----- .../trisolarispms/ui/users/UserCards.kt | 14 +- 6 files changed, 179 insertions(+), 122 deletions(-) create mode 100644 app/src/main/java/com/android/trisolarispms/auth/AuthzPolicy.kt create mode 100644 app/src/main/java/com/android/trisolarispms/auth/Role.kt diff --git a/app/src/main/java/com/android/trisolarispms/MainActivity.kt b/app/src/main/java/com/android/trisolarispms/MainActivity.kt index 7da0b35..d2a9a95 100644 --- a/app/src/main/java/com/android/trisolarispms/MainActivity.kt +++ b/app/src/main/java/com/android/trisolarispms/MainActivity.kt @@ -11,6 +11,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.LaunchedEffect import androidx.lifecycle.viewmodel.compose.viewModel +import com.android.trisolarispms.auth.AuthzPolicy +import com.android.trisolarispms.auth.Role +import com.android.trisolarispms.auth.toRoleNameList +import com.android.trisolarispms.auth.toRoleSet import com.android.trisolarispms.ui.AppRoute import com.android.trisolarispms.ui.auth.AuthScreen import com.android.trisolarispms.ui.auth.AuthViewModel @@ -82,58 +86,16 @@ class MainActivity : ComponentActivity() { val roomFormKey = remember { mutableStateOf(0) } val amenitiesReturnRoute = remember { mutableStateOf(AppRoute.Home) } val currentRoute = route.value + val authz = remember(state.isSuperAdmin, state.propertyRoles) { + AuthzPolicy( + isSuperAdmin = state.isSuperAdmin, + propertyRoles = state.propertyRoles + ) + } val singlePropertyId = state.propertyRoles.keys.firstOrNull() - val singlePropertyIsAdmin = singlePropertyId?.let { - state.propertyRoles[it]?.contains("ADMIN") == true - } ?: false - val canManageProperty: (String) -> Boolean = { propertyId -> - state.isSuperAdmin || (state.propertyRoles[propertyId]?.contains("ADMIN") == true) - } - val canIssueTemporaryCard: (String) -> Boolean = { propertyId -> - state.isSuperAdmin || state.propertyRoles[propertyId]?.any { - it == "ADMIN" || it == "MANAGER" - } == true - } - val canViewCardInfo: (String) -> Boolean = { propertyId -> - state.isSuperAdmin || state.propertyRoles[propertyId]?.any { - it == "ADMIN" || it == "MANAGER" || it == "STAFF" - } == true - } - val canManageRazorpaySettings: (String) -> Boolean = { propertyId -> - state.isSuperAdmin || state.propertyRoles[propertyId]?.contains("ADMIN") == true - } - val canDeleteCashPayment: (String) -> Boolean = { propertyId -> - state.isSuperAdmin || state.propertyRoles[propertyId]?.contains("ADMIN") == true - } - val canAddBookingPayment: (String) -> Boolean = { propertyId -> - state.isSuperAdmin || state.propertyRoles[propertyId]?.any { - it == "ADMIN" || it == "MANAGER" || it == "STAFF" - } == true - } - val canRefundBookingPayment: (String) -> Boolean = { propertyId -> - state.isSuperAdmin || state.propertyRoles[propertyId]?.any { - it == "ADMIN" || it == "MANAGER" - } == true - } - val canManagePropertyUsers: (String) -> Boolean = { propertyId -> - state.isSuperAdmin || state.propertyRoles[propertyId]?.contains("ADMIN") == true - } - val canCreateBookingFor: (String) -> Boolean = { propertyId -> - state.isSuperAdmin || state.propertyRoles[propertyId]?.any { - it == "ADMIN" || it == "MANAGER" || it == "STAFF" - } == true - } + val singlePropertyIsAdmin = singlePropertyId?.let(authz::isPropertyAdmin) ?: false val allowedAccessCodeRoles: (String) -> List = { propertyId -> - if (state.isSuperAdmin) { - listOf("MANAGER", "STAFF", "AGENT") - } else { - val roles = state.propertyRoles[propertyId].orEmpty() - when { - roles.contains("ADMIN") -> listOf("MANAGER", "STAFF", "AGENT") - roles.contains("MANAGER") -> listOf("STAFF", "AGENT") - else -> emptyList() - } - } + authz.allowedAccessCodeRoles(propertyId).toRoleNameList() } BackHandler(enabled = currentRoute != AppRoute.Home) { @@ -142,8 +104,10 @@ class MainActivity : ComponentActivity() { AppRoute.AddProperty -> route.value = AppRoute.Home AppRoute.SuperAdminUsers -> route.value = AppRoute.Home is AppRoute.ActiveRoomStays -> { - val isAdmin = state.propertyRoles[currentRoute.propertyId]?.contains("ADMIN") == true - val blockBack = !state.isSuperAdmin && !isAdmin && state.propertyRoles.size == 1 + val blockBack = authz.shouldBlockBackToHome( + propertyId = currentRoute.propertyId, + propertyCount = state.propertyRoles.size + ) if (!blockBack) { route.value = AppRoute.Home } @@ -280,7 +244,7 @@ class MainActivity : ComponentActivity() { onJoinPropertySuccess = { joinedPropertyId, joinedRoles -> refreshKey.value++ authViewModel.refreshMe() - val isAdmin = joinedRoles.contains("ADMIN") + val isAdmin = joinedRoles.toRoleSet().contains(Role.ADMIN) val shouldAutoOpen = !state.isSuperAdmin && !isAdmin && state.propertyRoles.size <= 1 if (shouldAutoOpen) { route.value = AppRoute.ActiveRoomStays( @@ -361,23 +325,24 @@ class MainActivity : ComponentActivity() { propertyId = currentRoute.propertyId, propertyName = currentRoute.propertyName, onBack = { - val isAdmin = state.propertyRoles[currentRoute.propertyId]?.contains("ADMIN") == true - val blockBack = !state.isSuperAdmin && !isAdmin && state.propertyRoles.size == 1 + val blockBack = authz.shouldBlockBackToHome( + propertyId = currentRoute.propertyId, + propertyCount = state.propertyRoles.size + ) if (!blockBack) { route.value = AppRoute.Home } }, - showBack = run { - val isAdmin = state.propertyRoles[currentRoute.propertyId]?.contains("ADMIN") == true - val blockBack = !state.isSuperAdmin && !isAdmin && state.propertyRoles.size == 1 - !blockBack - }, + showBack = !authz.shouldBlockBackToHome( + propertyId = currentRoute.propertyId, + propertyCount = state.propertyRoles.size + ), onViewRooms = { route.value = AppRoute.Rooms(currentRoute.propertyId) }, onCreateBooking = { route.value = AppRoute.CreateBooking(currentRoute.propertyId) }, - canCreateBooking = canCreateBookingFor(currentRoute.propertyId), - showRazorpaySettings = canManageRazorpaySettings(currentRoute.propertyId), + canCreateBooking = authz.canCreateBookingFor(currentRoute.propertyId), + showRazorpaySettings = authz.canManageRazorpaySettings(currentRoute.propertyId), onRazorpaySettings = { route.value = AppRoute.RazorpaySettings(currentRoute.propertyId) }, - showUserAdmin = canManagePropertyUsers(currentRoute.propertyId), + showUserAdmin = authz.canManagePropertyUsers(currentRoute.propertyId), onUserAdmin = { route.value = AppRoute.PropertyUsers(currentRoute.propertyId) }, onLogout = authViewModel::signOut, onManageRoomStay = { booking -> @@ -584,14 +549,14 @@ class MainActivity : ComponentActivity() { bookingId = currentRoute.bookingId ) }, - canManageDocuments = canManageRazorpaySettings(currentRoute.propertyId) + canManageDocuments = authz.canManageRazorpaySettings(currentRoute.propertyId) ) is AppRoute.BookingPayments -> BookingPaymentsScreen( propertyId = currentRoute.propertyId, bookingId = currentRoute.bookingId, - canAddCash = canAddBookingPayment(currentRoute.propertyId), - canDeleteCash = canDeleteCashPayment(currentRoute.propertyId), - canRefund = canRefundBookingPayment(currentRoute.propertyId), + canAddCash = authz.canAddBookingPayment(currentRoute.propertyId), + canDeleteCash = authz.canDeleteCashPayment(currentRoute.propertyId), + canRefund = authz.canRefundBookingPayment(currentRoute.propertyId), onBack = { route.value = AppRoute.BookingDetailsTabs( currentRoute.propertyId, @@ -605,17 +570,11 @@ class MainActivity : ComponentActivity() { ) is AppRoute.PropertyUsers -> PropertyUsersScreen( propertyId = currentRoute.propertyId, - allowedRoleAssignments = when { - state.isSuperAdmin -> listOf("ADMIN", "MANAGER", "STAFF", "AGENT") - state.propertyRoles[currentRoute.propertyId]?.contains("ADMIN") == true -> - listOf("ADMIN", "MANAGER", "STAFF", "AGENT") - state.propertyRoles[currentRoute.propertyId]?.contains("MANAGER") == true -> - listOf("STAFF", "AGENT") - else -> emptyList() - }, - canDisableAdmin = state.isSuperAdmin || - state.propertyRoles[currentRoute.propertyId]?.contains("ADMIN") == true, - canDisableManager = state.propertyRoles[currentRoute.propertyId]?.contains("MANAGER") == true, + allowedRoleAssignments = authz + .allowedRoleAssignments(currentRoute.propertyId) + .toRoleNameList(), + canDisableAdmin = authz.canDisableAdmin(currentRoute.propertyId), + canDisableManager = authz.canDisableManager(currentRoute.propertyId), onBack = { route.value = AppRoute.ActiveRoomStays( currentRoute.propertyId, @@ -645,16 +604,16 @@ class MainActivity : ComponentActivity() { }, onViewRoomTypes = { route.value = AppRoute.RoomTypes(currentRoute.propertyId) }, onViewCardInfo = { route.value = AppRoute.CardInfo(currentRoute.propertyId) }, - canManageRooms = canManageProperty(currentRoute.propertyId), - canViewCardInfo = canViewCardInfo(currentRoute.propertyId), - canIssueTemporaryCard = canIssueTemporaryCard(currentRoute.propertyId), + canManageRooms = authz.canManageProperty(currentRoute.propertyId), + canViewCardInfo = authz.canViewCardInfo(currentRoute.propertyId), + canIssueTemporaryCard = authz.canIssueTemporaryCard(currentRoute.propertyId), onEditRoom = { selectedRoom.value = it roomFormKey.value++ route.value = AppRoute.EditRoom(currentRoute.propertyId, it.id ?: "") }, onIssueTemporaryCard = { - if (it.id != null && canIssueTemporaryCard(currentRoute.propertyId)) { + if (it.id != null && authz.canIssueTemporaryCard(currentRoute.propertyId)) { selectedRoom.value = it route.value = AppRoute.IssueTemporaryCard(currentRoute.propertyId, it.id) } @@ -664,7 +623,7 @@ class MainActivity : ComponentActivity() { propertyId = currentRoute.propertyId, onBack = { route.value = AppRoute.Rooms(currentRoute.propertyId) }, onAdd = { route.value = AppRoute.AddRoomType(currentRoute.propertyId) }, - canManageRoomTypes = canManageProperty(currentRoute.propertyId), + canManageRoomTypes = authz.canManageProperty(currentRoute.propertyId), onEdit = { selectedRoomType.value = it route.value = AppRoute.EditRoomType(currentRoute.propertyId, it.id ?: "") diff --git a/app/src/main/java/com/android/trisolarispms/auth/AuthzPolicy.kt b/app/src/main/java/com/android/trisolarispms/auth/AuthzPolicy.kt new file mode 100644 index 0000000..7a06d66 --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/auth/AuthzPolicy.kt @@ -0,0 +1,59 @@ +package com.android.trisolarispms.auth + +class AuthzPolicy( + private val isSuperAdmin: Boolean, + propertyRoles: Map> +) { + private val rolesByProperty: Map> = propertyRoles.mapValues { it.value.toSet() } + + fun isPropertyAdmin(propertyId: String): Boolean = hasRole(propertyId, Role.ADMIN) + + fun canManageProperty(propertyId: String): Boolean = hasRole(propertyId, Role.ADMIN) + + fun canIssueTemporaryCard(propertyId: String): Boolean = + hasAnyRole(propertyId, Role.ADMIN, Role.MANAGER) + + fun canViewCardInfo(propertyId: String): Boolean = + hasAnyRole(propertyId, Role.ADMIN, Role.MANAGER, Role.STAFF) + + fun canManageRazorpaySettings(propertyId: String): Boolean = hasRole(propertyId, Role.ADMIN) + + fun canDeleteCashPayment(propertyId: String): Boolean = hasRole(propertyId, Role.ADMIN) + + fun canAddBookingPayment(propertyId: String): Boolean = + hasAnyRole(propertyId, Role.ADMIN, Role.MANAGER, Role.STAFF) + + fun canRefundBookingPayment(propertyId: String): Boolean = + hasAnyRole(propertyId, Role.ADMIN, Role.MANAGER) + + fun canManagePropertyUsers(propertyId: String): Boolean = hasRole(propertyId, Role.ADMIN) + + fun canCreateBookingFor(propertyId: String): Boolean = + hasAnyRole(propertyId, Role.ADMIN, Role.MANAGER, Role.STAFF) + + fun canDisableAdmin(propertyId: String): Boolean = hasRole(propertyId, Role.ADMIN) + + fun canDisableManager(propertyId: String): Boolean = hasRole(propertyId, Role.MANAGER) + + fun allowedAccessCodeRoles(propertyId: String): List = when { + isSuperAdmin || hasRole(propertyId, Role.ADMIN) -> listOf(Role.MANAGER, Role.STAFF, Role.AGENT) + hasRole(propertyId, Role.MANAGER) -> listOf(Role.STAFF, Role.AGENT) + else -> emptyList() + } + + fun allowedRoleAssignments(propertyId: String): List = when { + isSuperAdmin || hasRole(propertyId, Role.ADMIN) -> + listOf(Role.ADMIN, Role.MANAGER, Role.STAFF, Role.AGENT) + hasRole(propertyId, Role.MANAGER) -> listOf(Role.STAFF, Role.AGENT) + else -> emptyList() + } + + fun shouldBlockBackToHome(propertyId: String, propertyCount: Int): Boolean = + !isSuperAdmin && !isPropertyAdmin(propertyId) && propertyCount == 1 + + private fun hasRole(propertyId: String, requiredRole: Role): Boolean = + isSuperAdmin || (rolesByProperty[propertyId]?.contains(requiredRole) == true) + + private fun hasAnyRole(propertyId: String, vararg requiredRoles: Role): Boolean = + isSuperAdmin || (rolesByProperty[propertyId]?.any { role -> role in requiredRoles } == true) +} diff --git a/app/src/main/java/com/android/trisolarispms/auth/Role.kt b/app/src/main/java/com/android/trisolarispms/auth/Role.kt new file mode 100644 index 0000000..1eb953c --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/auth/Role.kt @@ -0,0 +1,27 @@ +package com.android.trisolarispms.auth + +import java.util.Locale + +enum class Role { + ADMIN, + MANAGER, + STAFF, + HOUSEKEEPING, + FINANCE, + GUIDE, + SUPERVISOR, + AGENT; + + companion object { + fun from(value: String?): Role? { + val normalized = value?.trim()?.uppercase(Locale.US) ?: return null + return entries.firstOrNull { it.name == normalized } + } + } +} + +fun Collection?.toRoles(): List = this.orEmpty().mapNotNull(Role::from) + +fun Collection?.toRoleSet(): Set = toRoles().toSet() + +fun Collection.toRoleNameList(): List = map { it.name } diff --git a/app/src/main/java/com/android/trisolarispms/ui/auth/AuthUiState.kt b/app/src/main/java/com/android/trisolarispms/ui/auth/AuthUiState.kt index c90aa1c..edf0a36 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/auth/AuthUiState.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/auth/AuthUiState.kt @@ -1,5 +1,7 @@ package com.android.trisolarispms.ui.auth +import com.android.trisolarispms.auth.Role + data class AuthUiState( val countryCode: String = "+91", val phoneCountryCode: String = "IN", @@ -19,5 +21,5 @@ data class AuthUiState( val nameInput: String = "", val needsName: Boolean = false, val unauthorized: Boolean = false, - val propertyRoles: Map> = emptyMap() + val propertyRoles: Map> = emptyMap() ) diff --git a/app/src/main/java/com/android/trisolarispms/ui/auth/AuthViewModel.kt b/app/src/main/java/com/android/trisolarispms/ui/auth/AuthViewModel.kt index 2a599b7..c060a5e 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/auth/AuthViewModel.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/auth/AuthViewModel.kt @@ -3,6 +3,8 @@ package com.android.trisolarispms.ui.auth import androidx.activity.ComponentActivity import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.android.trisolarispms.auth.Role +import com.android.trisolarispms.auth.toRoles import com.android.trisolarispms.data.api.ApiClient import com.google.firebase.FirebaseException import com.google.firebase.auth.FirebaseAuth @@ -208,50 +210,25 @@ class AuthViewModel( response: retrofit2.Response ) { val body = response.body() + val status = body?.status val userName = body?.user?.name val isSuperAdmin = body?.user?.superAdmin == true val propertyRoles = body?.properties .orEmpty() .mapNotNull { entry -> val id = entry.propertyId - id?.let { it to entry.roles.orEmpty() } + id?.let { it to entry.roles.toRoles() } } .toMap() when { - response.isSuccessful && (body?.status == "OK" || body?.status == "SUPER_ADMIN") -> { - _state.update { - it.copy( - isLoading = false, - userId = userId, - apiVerified = true, - needsName = userName.isNullOrBlank(), - nameInput = userName ?: "", - userName = userName, - isSuperAdmin = isSuperAdmin, - noProperties = body?.status == "NO_PROPERTIES", - unauthorized = false, - propertyRoles = propertyRoles, - error = null - ) - } - } - response.isSuccessful && body?.status == "NO_PROPERTIES" -> { - _state.update { - it.copy( - isLoading = false, - userId = userId, - apiVerified = true, - needsName = userName.isNullOrBlank(), - nameInput = userName ?: "", - userName = userName, - isSuperAdmin = isSuperAdmin, - noProperties = true, - unauthorized = false, - propertyRoles = propertyRoles, - error = null - ) - } - } + response.isSuccessful && (status == "OK" || status == "SUPER_ADMIN" || status == "NO_PROPERTIES") -> + setVerifiedState( + userId = userId, + userName = userName, + isSuperAdmin = isSuperAdmin, + propertyRoles = propertyRoles, + noProperties = status == "NO_PROPERTIES" + ) response.code() == 401 -> { _state.update { it.copy( @@ -281,6 +258,30 @@ class AuthViewModel( } } + private fun setVerifiedState( + userId: String?, + userName: String?, + isSuperAdmin: Boolean, + propertyRoles: Map>, + noProperties: Boolean + ) { + _state.update { + it.copy( + isLoading = false, + userId = userId, + apiVerified = true, + needsName = userName.isNullOrBlank(), + nameInput = userName ?: "", + userName = userName, + isSuperAdmin = isSuperAdmin, + noProperties = noProperties, + unauthorized = false, + propertyRoles = propertyRoles, + error = null + ) + } + } + fun signOut() { auth.signOut() _state.update { AuthUiState() } @@ -335,5 +336,4 @@ class AuthViewModel( } } } - } diff --git a/app/src/main/java/com/android/trisolarispms/ui/users/UserCards.kt b/app/src/main/java/com/android/trisolarispms/ui/users/UserCards.kt index 753ae21..1aaada6 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/users/UserCards.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/users/UserCards.kt @@ -14,6 +14,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.android.trisolarispms.auth.Role +import com.android.trisolarispms.auth.toRoles @Composable fun PropertyUserCard( @@ -73,6 +75,14 @@ fun canDisableUser( if (user.userId.isNullOrBlank()) return false if (canDisableAdmin) return true if (!canDisableManager) return false - val allowed = setOf("STAFF", "AGENT", "HOUSEKEEPING", "FINANCE", "GUIDE", "SUPERVISOR") - return user.roles.all { allowed.contains(it) } + val allowed = setOf( + Role.STAFF, + Role.AGENT, + Role.HOUSEKEEPING, + Role.FINANCE, + Role.GUIDE, + Role.SUPERVISOR + ) + val parsedRoles = user.roles.toRoles() + return parsedRoles.size == user.roles.size && parsedRoles.all { it in allowed } }