Enforce owner/admin hierarchy for member management
This commit is contained in:
@@ -1351,6 +1351,7 @@ fun ChatScreen(
|
|||||||
members = state.chatMembers,
|
members = state.chatMembers,
|
||||||
bans = state.chatBans,
|
bans = state.chatBans,
|
||||||
selfUserId = state.selfUserId,
|
selfUserId = state.selfUserId,
|
||||||
|
selfRole = state.chatRole,
|
||||||
canManageMembers = state.canManageMembers,
|
canManageMembers = state.canManageMembers,
|
||||||
canTransferOwnership = state.chatRole.equals("owner", ignoreCase = true),
|
canTransferOwnership = state.chatRole.equals("owner", ignoreCase = true),
|
||||||
onPromoteMember = onPromoteMember,
|
onPromoteMember = onPromoteMember,
|
||||||
@@ -3469,6 +3470,7 @@ private fun ChatInfoTabContent(
|
|||||||
members: List<ChatMemberItem>,
|
members: List<ChatMemberItem>,
|
||||||
bans: List<ChatBanItem>,
|
bans: List<ChatBanItem>,
|
||||||
selfUserId: Long?,
|
selfUserId: Long?,
|
||||||
|
selfRole: String?,
|
||||||
canManageMembers: Boolean,
|
canManageMembers: Boolean,
|
||||||
canTransferOwnership: Boolean,
|
canTransferOwnership: Boolean,
|
||||||
onPromoteMember: (Long) -> Unit,
|
onPromoteMember: (Long) -> Unit,
|
||||||
@@ -3484,6 +3486,7 @@ private fun ChatInfoTabContent(
|
|||||||
members = members,
|
members = members,
|
||||||
bans = bans,
|
bans = bans,
|
||||||
selfUserId = selfUserId,
|
selfUserId = selfUserId,
|
||||||
|
selfRole = selfRole,
|
||||||
canManageMembers = canManageMembers,
|
canManageMembers = canManageMembers,
|
||||||
canTransferOwnership = canTransferOwnership,
|
canTransferOwnership = canTransferOwnership,
|
||||||
onPromoteMember = onPromoteMember,
|
onPromoteMember = onPromoteMember,
|
||||||
@@ -3729,6 +3732,7 @@ private fun ChatMembersTabContent(
|
|||||||
members: List<ChatMemberItem>,
|
members: List<ChatMemberItem>,
|
||||||
bans: List<ChatBanItem>,
|
bans: List<ChatBanItem>,
|
||||||
selfUserId: Long?,
|
selfUserId: Long?,
|
||||||
|
selfRole: String?,
|
||||||
canManageMembers: Boolean,
|
canManageMembers: Boolean,
|
||||||
canTransferOwnership: Boolean,
|
canTransferOwnership: Boolean,
|
||||||
onPromoteMember: (Long) -> Unit,
|
onPromoteMember: (Long) -> Unit,
|
||||||
@@ -3804,20 +3808,29 @@ private fun ChatMembersTabContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val isSelf = selfUserId != null && member.userId == selfUserId
|
val isSelf = selfUserId != null && member.userId == selfUserId
|
||||||
if (canManageMembers && !member.role.equals("owner", ignoreCase = true) && !isSelf) {
|
val actorRole = selfRole?.lowercase(Locale.getDefault()).orEmpty()
|
||||||
|
val targetRole = member.role.lowercase(Locale.getDefault())
|
||||||
|
val canManageTarget = canManageMembers &&
|
||||||
|
!isSelf &&
|
||||||
|
targetRole != "owner" &&
|
||||||
|
!(actorRole == "admin" && targetRole == "admin")
|
||||||
|
val canPromote = canManageTarget && targetRole == "member"
|
||||||
|
val canDemote = canManageTarget && targetRole == "admin" && actorRole == "owner"
|
||||||
|
val canTransfer = canTransferOwnership && !isSelf && targetRole != "owner"
|
||||||
|
if (canManageTarget || canTransfer) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.horizontalScroll(rememberScrollState()),
|
.horizontalScroll(rememberScrollState()),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
if (member.role.equals("member", ignoreCase = true)) {
|
if (canPromote) {
|
||||||
AssistChip(
|
AssistChip(
|
||||||
onClick = { onPromoteMember(member.userId) },
|
onClick = { onPromoteMember(member.userId) },
|
||||||
label = { Text("Promote") },
|
label = { Text("Promote") },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (member.role.equals("admin", ignoreCase = true)) {
|
if (canDemote) {
|
||||||
AssistChip(
|
AssistChip(
|
||||||
onClick = {
|
onClick = {
|
||||||
pendingAction = PendingMemberAction(
|
pendingAction = PendingMemberAction(
|
||||||
@@ -3829,7 +3842,7 @@ private fun ChatMembersTabContent(
|
|||||||
label = { Text("Demote") },
|
label = { Text("Demote") },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (canTransferOwnership) {
|
if (canTransfer) {
|
||||||
AssistChip(
|
AssistChip(
|
||||||
onClick = {
|
onClick = {
|
||||||
pendingAction = PendingMemberAction(
|
pendingAction = PendingMemberAction(
|
||||||
@@ -3841,26 +3854,28 @@ private fun ChatMembersTabContent(
|
|||||||
label = { Text("Transfer owner") },
|
label = { Text("Transfer owner") },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AssistChip(
|
if (canManageTarget) {
|
||||||
onClick = {
|
AssistChip(
|
||||||
pendingAction = PendingMemberAction(
|
onClick = {
|
||||||
title = "Ban member",
|
pendingAction = PendingMemberAction(
|
||||||
body = "Ban ${member.name.ifBlank { "@${member.username ?: member.userId}" }}?",
|
title = "Ban member",
|
||||||
onConfirm = { onBanMember(member.userId) },
|
body = "Ban ${member.name.ifBlank { "@${member.username ?: member.userId}" }}?",
|
||||||
)
|
onConfirm = { onBanMember(member.userId) },
|
||||||
},
|
)
|
||||||
label = { Text("Ban") },
|
},
|
||||||
)
|
label = { Text("Ban") },
|
||||||
AssistChip(
|
)
|
||||||
onClick = {
|
AssistChip(
|
||||||
pendingAction = PendingMemberAction(
|
onClick = {
|
||||||
title = "Kick member",
|
pendingAction = PendingMemberAction(
|
||||||
body = "Kick ${member.name.ifBlank { "@${member.username ?: member.userId}" }} from chat?",
|
title = "Kick member",
|
||||||
onConfirm = { onKickMember(member.userId) },
|
body = "Kick ${member.name.ifBlank { "@${member.username ?: member.userId}" }} from chat?",
|
||||||
)
|
onConfirm = { onKickMember(member.userId) },
|
||||||
},
|
)
|
||||||
label = { Text("Kick") },
|
},
|
||||||
)
|
label = { Text("Kick") },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -508,34 +508,22 @@ class ChatViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun promoteMember(userId: Long) {
|
fun promoteMember(userId: Long) {
|
||||||
if (userId == uiState.value.selfUserId) {
|
if (!ensureCanManageTarget(userId = userId, action = "promote")) return
|
||||||
_uiState.update { it.copy(errorMessage = "You cannot change your own role from this screen.") }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
updateMemberRole(userId = userId, role = "admin")
|
updateMemberRole(userId = userId, role = "admin")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun demoteMember(userId: Long) {
|
fun demoteMember(userId: Long) {
|
||||||
if (userId == uiState.value.selfUserId) {
|
if (!ensureCanManageTarget(userId = userId, action = "demote")) return
|
||||||
_uiState.update { it.copy(errorMessage = "You cannot change your own role from this screen.") }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
updateMemberRole(userId = userId, role = "member")
|
updateMemberRole(userId = userId, role = "member")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun transferOwnership(userId: Long) {
|
fun transferOwnership(userId: Long) {
|
||||||
if (userId == uiState.value.selfUserId) {
|
if (!ensureCanManageTarget(userId = userId, action = "transfer_ownership", ownerOnly = true)) return
|
||||||
_uiState.update { it.copy(errorMessage = "Transfer ownership to another member.") }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
updateMemberRole(userId = userId, role = "owner")
|
updateMemberRole(userId = userId, role = "owner")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun kickMember(userId: Long) {
|
fun kickMember(userId: Long) {
|
||||||
if (userId == uiState.value.selfUserId) {
|
if (!ensureCanManageTarget(userId = userId, action = "kick")) return
|
||||||
_uiState.update { it.copy(errorMessage = "You cannot kick yourself.") }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (val result = chatRepository.removeMember(chatId = chatId, userId = userId)) {
|
when (val result = chatRepository.removeMember(chatId = chatId, userId = userId)) {
|
||||||
is AppResult.Success -> refreshMembersAndBans()
|
is AppResult.Success -> refreshMembersAndBans()
|
||||||
@@ -545,10 +533,7 @@ class ChatViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun banMember(userId: Long) {
|
fun banMember(userId: Long) {
|
||||||
if (userId == uiState.value.selfUserId) {
|
if (!ensureCanManageTarget(userId = userId, action = "ban")) return
|
||||||
_uiState.update { it.copy(errorMessage = "You cannot ban yourself.") }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (val result = chatRepository.banMember(chatId = chatId, userId = userId)) {
|
when (val result = chatRepository.banMember(chatId = chatId, userId = userId)) {
|
||||||
is AppResult.Success -> refreshMembersAndBans()
|
is AppResult.Success -> refreshMembersAndBans()
|
||||||
@@ -940,6 +925,45 @@ class ChatViewModel @Inject constructor(
|
|||||||
return uiState.value.messages.firstOrNull { it.id == messageId }
|
return uiState.value.messages.firstOrNull { it.id == messageId }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ensureCanManageTarget(
|
||||||
|
userId: Long,
|
||||||
|
action: String,
|
||||||
|
ownerOnly: Boolean = false,
|
||||||
|
): Boolean {
|
||||||
|
val state = uiState.value
|
||||||
|
val selfId = state.selfUserId
|
||||||
|
if (selfId != null && userId == selfId) {
|
||||||
|
_uiState.update { it.copy(errorMessage = "You cannot apply this action to yourself.") }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val actorRole = state.chatRole?.lowercase()
|
||||||
|
if (actorRole != "owner" && actorRole != "admin") {
|
||||||
|
_uiState.update { it.copy(errorMessage = "You don't have enough permissions.") }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (ownerOnly && actorRole != "owner") {
|
||||||
|
_uiState.update { it.copy(errorMessage = "Only owner can perform this action.") }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val targetRole = state.chatMembers.firstOrNull { it.userId == userId }?.role?.lowercase()
|
||||||
|
if (targetRole == "owner") {
|
||||||
|
_uiState.update { it.copy(errorMessage = "You cannot manage owner account.") }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (actorRole == "admin" && (targetRole == "admin" || targetRole == "owner")) {
|
||||||
|
_uiState.update { it.copy(errorMessage = "Admin cannot manage admins or owner.") }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action == "transfer_ownership" && targetRole == "owner") {
|
||||||
|
_uiState.update { it.copy(errorMessage = "Choose another member for ownership transfer.") }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
private fun AppError.toUiMessage(): String {
|
private fun AppError.toUiMessage(): String {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
AppError.Network -> "Network error."
|
AppError.Network -> "Network error."
|
||||||
|
|||||||
Reference in New Issue
Block a user