improve auth policy
This commit is contained in:
56
AGENTS.md
56
AGENTS.md
@@ -527,3 +527,59 @@ GET /properties/{propertyId}/bookings/{bookingId}/balance
|
|||||||
## 7) Compose Notes
|
## 7) Compose Notes
|
||||||
|
|
||||||
- Use `androidx.compose.foundation.text.KeyboardOptions` for keyboard options imports.
|
- Use `androidx.compose.foundation.text.KeyboardOptions` for keyboard options imports.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8) Engineering Structure & Anti-Boilerplate Rules
|
||||||
|
|
||||||
|
### Non-negotiable coding rules
|
||||||
|
|
||||||
|
- Never add duplicate business logic in multiple files.
|
||||||
|
- Never copy-paste permission checks, navigation decisions, mapping logic, or API call patterns.
|
||||||
|
- If similar logic appears 2+ times, extract shared function/class immediately.
|
||||||
|
- Prefer typed models/enums over raw strings for roles/status/flags.
|
||||||
|
- Keep files small and purpose-driven; split before a file becomes hard to scan.
|
||||||
|
|
||||||
|
### Required project structure (current baseline)
|
||||||
|
|
||||||
|
- `core/` -> cross-cutting business primitives/policies (e.g., auth policy, role enum).
|
||||||
|
- `data/api/core/` -> API client, constants, token providers, aggregated API service.
|
||||||
|
- `data/api/service/` -> Retrofit endpoint interfaces only.
|
||||||
|
- `data/api/model/` -> DTO/request/response models.
|
||||||
|
- `ui/navigation/` -> route model, navigation orchestrators, back-navigation rules.
|
||||||
|
- `ui/<feature>/` -> screen + state + viewmodel for that feature.
|
||||||
|
|
||||||
|
### How to implement future logic (mandatory workflow)
|
||||||
|
|
||||||
|
1. Define/extend domain type first (enum/data model/policy) instead of raw literals.
|
||||||
|
2. Add/extend API contract in `data/api/service` and models in `data/api/model`.
|
||||||
|
3. Add shared logic once (policy/helper/mapper) in `core` or feature-common layer.
|
||||||
|
4. Keep ViewModel thin: orchestrate calls, state, and errors only.
|
||||||
|
5. Keep UI dumb: consume state and callbacks; avoid business rules in composables.
|
||||||
|
6. If navigation changes, update `ui/navigation` only (single source of truth).
|
||||||
|
7. Before finishing, remove any newly introduced duplication and compile-check.
|
||||||
|
|
||||||
|
### PR/refactor acceptance checklist
|
||||||
|
|
||||||
|
- No repeated role/permission checks across screens.
|
||||||
|
- No repeated model mapping blocks (extract mapper/helper).
|
||||||
|
- No giant god-file when it can be split by domain responsibility.
|
||||||
|
- Imports/packages follow the structure above.
|
||||||
|
- Build passes: `./gradlew :app:compileDebugKotlin`.
|
||||||
|
|
||||||
|
### Guest Documents Authorization (mandatory)
|
||||||
|
|
||||||
|
- View access: `ADMIN`, `MANAGER` (and super admin).
|
||||||
|
- Modify access (upload/delete): allowed only when booking status is `OPEN` or `CHECKED_IN`.
|
||||||
|
- For `CHECKED_OUT`, `CANCELLED`, `NO_SHOW`: documents are read-only.
|
||||||
|
- Never couple guest document permissions with Razorpay/settings permissions.
|
||||||
|
|
||||||
|
### Permission design guardrail
|
||||||
|
|
||||||
|
- Do not reuse one feature's permission gate for another unrelated feature.
|
||||||
|
- Add explicit policy methods in `core/auth/AuthzPolicy` for each feature capability.
|
||||||
|
|
||||||
|
### Refactor safety rule
|
||||||
|
|
||||||
|
- Any package/file movement must include import updates in same change.
|
||||||
|
- After refactor, compile check is mandatory: `./gradlew :app:compileDebugKotlin`.
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ class AuthzPolicy(
|
|||||||
fun canRefundBookingPayment(propertyId: String): Boolean =
|
fun canRefundBookingPayment(propertyId: String): Boolean =
|
||||||
hasAnyRole(propertyId, Role.ADMIN, Role.MANAGER)
|
hasAnyRole(propertyId, Role.ADMIN, Role.MANAGER)
|
||||||
|
|
||||||
|
fun canManageGuestDocuments(propertyId: String): Boolean =
|
||||||
|
hasAnyRole(propertyId, Role.ADMIN, Role.MANAGER)
|
||||||
|
|
||||||
fun canManagePropertyUsers(propertyId: String): Boolean = hasRole(propertyId, Role.ADMIN)
|
fun canManagePropertyUsers(propertyId: String): Boolean = hasRole(propertyId, Role.ADMIN)
|
||||||
|
|
||||||
fun canCreateBookingFor(propertyId: String): Boolean =
|
fun canCreateBookingFor(propertyId: String): Boolean =
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ fun GuestDocumentsTab(
|
|||||||
guestId: String,
|
guestId: String,
|
||||||
bookingId: String,
|
bookingId: String,
|
||||||
canManageDocuments: Boolean,
|
canManageDocuments: Boolean,
|
||||||
|
canModifyDocuments: Boolean,
|
||||||
viewModel: GuestDocumentsViewModel = viewModel(key = "guestDocs:$propertyId:$guestId")
|
viewModel: GuestDocumentsViewModel = viewModel(key = "guestDocs:$propertyId:$guestId")
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
@@ -164,6 +165,13 @@ fun GuestDocumentsTab(
|
|||||||
Text(text = "You don't have access to view documents.")
|
Text(text = "You don't have access to view documents.")
|
||||||
return@Column
|
return@Column
|
||||||
}
|
}
|
||||||
|
if (!canModifyDocuments) {
|
||||||
|
Text(
|
||||||
|
text = "Read-only: documents can be modified only when booking is OPEN or CHECKED_IN.",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
if (!state.isLoading && state.documents.isEmpty()) {
|
if (!state.isLoading && state.documents.isEmpty()) {
|
||||||
Text(text = "No documents yet")
|
Text(text = "No documents yet")
|
||||||
}
|
}
|
||||||
@@ -190,7 +198,7 @@ fun GuestDocumentsTab(
|
|||||||
guestId = guestId,
|
guestId = guestId,
|
||||||
doc = doc,
|
doc = doc,
|
||||||
imageLoader = imageLoader,
|
imageLoader = imageLoader,
|
||||||
canDelete = canManageDocuments,
|
canDelete = canModifyDocuments,
|
||||||
onDelete = { documentId ->
|
onDelete = { documentId ->
|
||||||
viewModel.deleteDocument(propertyId, guestId, documentId)
|
viewModel.deleteDocument(propertyId, guestId, documentId)
|
||||||
}
|
}
|
||||||
@@ -199,7 +207,7 @@ fun GuestDocumentsTab(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canManageDocuments) {
|
if (canModifyDocuments) {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = { showPicker.value = true },
|
onClick = { showPicker.value = true },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -214,7 +222,7 @@ fun GuestDocumentsTab(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showPicker.value) {
|
if (showPicker.value && canModifyDocuments) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { showPicker.value = false },
|
onDismissRequest = { showPicker.value = false },
|
||||||
title = { Text("Add document") },
|
title = { Text("Add document") },
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ internal fun renderBookingRoutes(
|
|||||||
bookingId = currentRoute.bookingId
|
bookingId = currentRoute.bookingId
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
canManageDocuments = authz.canManageRazorpaySettings(currentRoute.propertyId)
|
canManageDocuments = authz.canManageGuestDocuments(currentRoute.propertyId)
|
||||||
)
|
)
|
||||||
|
|
||||||
is AppRoute.BookingPayments -> BookingPaymentsScreen(
|
is AppRoute.BookingPayments -> BookingPaymentsScreen(
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ fun BookingDetailsTabsScreen(
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val staysState by staysViewModel.state.collectAsState()
|
val staysState by staysViewModel.state.collectAsState()
|
||||||
val detailsState by detailsViewModel.state.collectAsState()
|
val detailsState by detailsViewModel.state.collectAsState()
|
||||||
|
val canModifyDocuments = canManageDocuments && when (detailsState.details?.status) {
|
||||||
|
"OPEN", "CHECKED_IN" -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(propertyId, bookingId, guestId) {
|
LaunchedEffect(propertyId, bookingId, guestId) {
|
||||||
staysViewModel.load(propertyId, bookingId)
|
staysViewModel.load(propertyId, bookingId)
|
||||||
@@ -164,7 +168,8 @@ fun BookingDetailsTabsScreen(
|
|||||||
propertyId = propertyId,
|
propertyId = propertyId,
|
||||||
guestId = resolvedGuestId,
|
guestId = resolvedGuestId,
|
||||||
bookingId = bookingId,
|
bookingId = bookingId,
|
||||||
canManageDocuments = canManageDocuments
|
canManageDocuments = canManageDocuments,
|
||||||
|
canModifyDocuments = canModifyDocuments
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
Reference in New Issue
Block a user