💄 (user): Muestra sólo permisos concedidos al rol
This commit is contained in:
parent
f337119219
commit
806ed9e0dc
6 changed files with 78 additions and 64 deletions
|
|
@ -38,19 +38,6 @@
|
|||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.user-admin-permission-list {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.user-admin-permission-granted {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-admin-permission-missing {
|
||||
color: var(--val-color--text--muted);
|
||||
}
|
||||
|
||||
.user-form-error {
|
||||
padding: 0.625rem 0.875rem;
|
||||
margin-bottom: 1rem;
|
||||
|
|
|
|||
|
|
@ -88,6 +88,15 @@ impl Component for RoleTable {
|
|||
} else {
|
||||
None
|
||||
};
|
||||
let no_permissions_badge = if role.locked || role.has_permissions {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Badge::neutral(Lc::t("badge-no-permissions", &LOCALES_USER))
|
||||
.render(cx)
|
||||
.await,
|
||||
)
|
||||
};
|
||||
|
||||
table.alter_row(
|
||||
table::Row::new()
|
||||
|
|
@ -96,6 +105,7 @@ impl Component for RoleTable {
|
|||
.with_cell(Html::with(move |_cx| {
|
||||
html! {
|
||||
@if let Some(badge) = &system_badge { (badge) }
|
||||
@if let Some(badge) = &no_permissions_badge { (badge) }
|
||||
}
|
||||
}))
|
||||
.with_cell(role.user_count.to_string())
|
||||
|
|
@ -166,10 +176,9 @@ impl RoleTable {
|
|||
self
|
||||
}
|
||||
|
||||
// URL del listado con el estado actual (orden, página): es el valor que viaja como
|
||||
// `waypoint` en los enlaces de ver/editar/permisos, para poder volver exactamente a este
|
||||
// mismo estado. Se construye con `cx.route()` para que preserve el parámetro `lang` cuando
|
||||
// corresponda.
|
||||
// URL del listado con el estado actual (orden, página): es el valor que viaja como `waypoint`
|
||||
// en los enlaces de ver/editar/permisos, para poder volver exactamente a este mismo estado. Se
|
||||
// construye con `cx.route()` para que preserve el parámetro `lang` cuando corresponda.
|
||||
fn list_href(&self, cx: &Context) -> String {
|
||||
cx.route(ADMIN_ROLES_PATH)
|
||||
.alter_param("sort", self.sort().as_str())
|
||||
|
|
@ -219,8 +228,8 @@ fn label_cell(role: &RoleListItem, waypoint: &Waypoint) -> Html {
|
|||
}
|
||||
|
||||
// Construye la celda de acciones: gestionar permisos siempre, y editar/borrar sólo si el rol no
|
||||
// está bloqueado por el sistema. Devuelve un componente `Html` para que el marcado se genere
|
||||
// cuando `Table` renderice la celda, no al construir la fila.
|
||||
// está bloqueado por el sistema. Devuelve un componente `Html` para que el marcado se genere cuando
|
||||
// `Table` renderice la celda, no al construir la fila.
|
||||
async fn actions_cell(
|
||||
role: &RoleListItem,
|
||||
waypoint: &Waypoint,
|
||||
|
|
@ -235,10 +244,10 @@ async fn actions_cell(
|
|||
|
||||
let permissions_href = waypoint.append_to(cx.route(role_path(id, "permissions")));
|
||||
|
||||
// Los botones se renderizan aquí, no dentro del `Html::with()` de abajo: necesitan pasar por
|
||||
// su propio ciclo de renderizado (`.render().await`) para que el tema activo los estilice
|
||||
// igual (ver `pagetop-bootsier::theme::bs::button`), incluida la traducción de `data-dialog-*`
|
||||
// que usa el botón de borrado.
|
||||
// Los botones se renderizan aquí, no dentro del `Html::with()` de abajo: necesitan pasar por su
|
||||
// propio ciclo de renderizado (`.render().await`) para que el tema activo los estilice igual
|
||||
// (ver `pagetop-bootsier::theme::bs::button`), incluida la traducción de `data-dialog-*` que
|
||||
// usa el botón de borrado.
|
||||
let permissions_button = Button::anchor(
|
||||
Lc::t("btn-manage-permissions", &LOCALES_USER),
|
||||
permissions_href,
|
||||
|
|
@ -258,8 +267,8 @@ async fn actions_cell(
|
|||
.render(cx)
|
||||
.await;
|
||||
|
||||
// Viaja como query string para que, tanto si el borrado falla como si tiene éxito, la
|
||||
// tabla vuelva a mostrarse en la misma página/orden en que estaba, en vez de reiniciarse.
|
||||
// Viaja como query string para que, tanto si el borrado falla como si tiene éxito, la tabla
|
||||
// vuelva a mostrarse en la misma página/orden en que estaba, en vez de reiniciarse.
|
||||
let confirm_href = cx
|
||||
.route(role_path(id, "delete/confirm"))
|
||||
.alter_param("sort", sort.as_str())
|
||||
|
|
|
|||
|
|
@ -348,31 +348,33 @@ async fn role_view_details(role: &role::Model, cx: &mut Context) -> Block {
|
|||
.with_child(table)
|
||||
}
|
||||
|
||||
// Un bloque por grupo del catálogo de permisos: cada permiso concedido se marca con la clase
|
||||
// `user-admin-permission-granted` (negrita, vía CSS del tema); el resto con
|
||||
// `user-admin-permission-missing` (gris claro, vía CSS del tema).
|
||||
// Un bloque por grupo del catálogo con los permisos concedidos al rol; los grupos sin ninguno no se
|
||||
// muestran. Si el rol no tiene ningún permiso, un único bloque lo indica.
|
||||
fn role_view_permissions(groups: &PermissionGroups) -> Vec<Block> {
|
||||
groups
|
||||
let blocks: Vec<Block> = groups
|
||||
.iter()
|
||||
.filter(|(_, perms)| perms.iter().any(|(_, _, granted)| *granted))
|
||||
.map(|(group_label, perms)| {
|
||||
let perms = perms.clone();
|
||||
let mut table = Table::new().with_prop(PropsOp::add_classes("user-admin-table"));
|
||||
for (_key, label, _) in perms.iter().filter(|(_, _, granted)| *granted) {
|
||||
table = table.with_row(table::Row::new().with_cell(label.clone()));
|
||||
}
|
||||
Block::new()
|
||||
.with_title(group_label.clone())
|
||||
.with_child(Html::with(move |cx| {
|
||||
html! {
|
||||
ul class="user-admin-permission-list" {
|
||||
@for (_key, label, granted) in &perms {
|
||||
@if *granted {
|
||||
li class="user-admin-permission-granted" { (label.using(cx)) }
|
||||
} @else {
|
||||
li class="user-admin-permission-missing" { (label.using(cx)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
.with_child(table)
|
||||
})
|
||||
.collect()
|
||||
.collect();
|
||||
|
||||
if blocks.is_empty() {
|
||||
return vec![
|
||||
Block::new()
|
||||
.with_title(Lc::t("title-admin-permissions", &LOCALES_USER))
|
||||
.with_child(
|
||||
Table::new().with_empty(Lc::t("empty-role-permissions", &LOCALES_USER)),
|
||||
),
|
||||
];
|
||||
}
|
||||
blocks
|
||||
}
|
||||
|
||||
// **< delete_post >********************************************************************************
|
||||
|
|
@ -537,8 +539,8 @@ pub(crate) struct RolePermissionsFormData {
|
|||
|
||||
/// POST /admin/user/roles/{id}/permissions - Reemplaza el conjunto de permisos de un rol.
|
||||
///
|
||||
/// Usa `RawForm` + `serde_qs` en lugar de `axum::extract::Form` (basado en `serde_urlencoded`,
|
||||
/// que no deserializa claves repetidas como `permission_keys=a&permission_keys=b` en un `Vec<T>`).
|
||||
/// Usa `RawForm` + `serde_qs` en lugar de `axum::extract::Form` (basado en `serde_urlencoded`, que
|
||||
/// no deserializa claves repetidas como `permission_keys=a&permission_keys=b` en un `Vec<T>`).
|
||||
pub(crate) async fn permissions_post(
|
||||
request: HttpRequest,
|
||||
web::Path(id): web::Path<i32>,
|
||||
|
|
|
|||
|
|
@ -140,14 +140,16 @@ link-back-to-list = Back to list
|
|||
|
||||
# **< Admin: confirmations and badges >**
|
||||
|
||||
confirm-delete-role = Delete this role? This cannot be undone.
|
||||
confirm-change-status = Change this account's status?
|
||||
confirm-grant-admin = Grant unrestricted access to this account?
|
||||
confirm-revoke-admin = Revoke this account's unrestricted access?
|
||||
badge-system-role = System
|
||||
badge-admin = Administrator
|
||||
empty-users-list = No users found.
|
||||
empty-roles-list = No roles found.
|
||||
confirm-delete-role = Delete this role? This cannot be undone.
|
||||
confirm-change-status = Change this account's status?
|
||||
confirm-grant-admin = Grant unrestricted access to this account?
|
||||
confirm-revoke-admin = Revoke this account's unrestricted access?
|
||||
badge-system-role = System
|
||||
badge-admin = Administrator
|
||||
badge-no-permissions = No permissions
|
||||
empty-users-list = No users found.
|
||||
empty-roles-list = No roles found.
|
||||
empty-role-permissions = This role has no permissions.
|
||||
|
||||
# **< Admin: error messages >**
|
||||
|
||||
|
|
|
|||
|
|
@ -140,14 +140,16 @@ link-back-to-list = Volver al listado
|
|||
|
||||
# **< Administración: confirmaciones y distintivos >**
|
||||
|
||||
confirm-delete-role = ¿Eliminar este rol? Esta acción no se puede deshacer.
|
||||
confirm-change-status = ¿Cambiar el estado de esta cuenta?
|
||||
confirm-grant-admin = ¿Conceder acceso irrestricto a esta cuenta?
|
||||
confirm-revoke-admin = ¿Revocar el acceso irrestricto de esta cuenta?
|
||||
badge-system-role = Sistema
|
||||
badge-admin = Administrador
|
||||
empty-users-list = No se han encontrado usuarios.
|
||||
empty-roles-list = No se han encontrado roles.
|
||||
confirm-delete-role = ¿Eliminar este rol? Esta acción no se puede deshacer.
|
||||
confirm-change-status = ¿Cambiar el estado de esta cuenta?
|
||||
confirm-grant-admin = ¿Conceder acceso irrestricto a esta cuenta?
|
||||
confirm-revoke-admin = ¿Revocar el acceso irrestricto de esta cuenta?
|
||||
badge-system-role = Sistema
|
||||
badge-admin = Administrador
|
||||
badge-no-permissions = Sin permisos
|
||||
empty-users-list = No se han encontrado usuarios.
|
||||
empty-roles-list = No se han encontrado roles.
|
||||
empty-role-permissions = Este rol no tiene permisos.
|
||||
|
||||
# **< Administración: mensajes de error >**
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! Servicio de administración de roles: listado, CRUD y permisos.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use pagetop::prelude::*;
|
||||
use pagetop_seaorm::db::{
|
||||
|
|
@ -47,6 +47,7 @@ pub(crate) struct RoleListItem {
|
|||
pub label: String,
|
||||
pub locked: bool,
|
||||
pub user_count: u64,
|
||||
pub has_permissions: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct RoleListParams {
|
||||
|
|
@ -104,7 +105,7 @@ pub(crate) async fn list_roles_page(
|
|||
async fn role_items(roles: Vec<role::Model>) -> Result<Vec<RoleListItem>, AuthError> {
|
||||
let role_ids: Vec<i32> = roles.iter().map(|r| r.id).collect();
|
||||
let counts: Vec<(i32, i64)> = user_role::Entity::find()
|
||||
.filter(user_role::Column::RoleId.is_in(role_ids))
|
||||
.filter(user_role::Column::RoleId.is_in(role_ids.clone()))
|
||||
.select_only()
|
||||
.column(user_role::Column::RoleId)
|
||||
.column_as(user_role::Column::RoleId.count(), "count")
|
||||
|
|
@ -116,11 +117,22 @@ async fn role_items(roles: Vec<role::Model>) -> Result<Vec<RoleListItem>, AuthEr
|
|||
.into_iter()
|
||||
.map(|(role_id, count)| (role_id, count as u64))
|
||||
.collect();
|
||||
let with_permissions: HashSet<i32> = role_permission::Entity::find()
|
||||
.filter(role_permission::Column::RoleId.is_in(role_ids))
|
||||
.select_only()
|
||||
.column(role_permission::Column::RoleId)
|
||||
.distinct()
|
||||
.into_tuple()
|
||||
.all(dbconn())
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
Ok(roles
|
||||
.into_iter()
|
||||
.map(|role| RoleListItem {
|
||||
user_count: counts_by_role.get(&role.id).copied().unwrap_or(0),
|
||||
has_permissions: with_permissions.contains(&role.id),
|
||||
id: role.id,
|
||||
machine_name: role.machine_name,
|
||||
label: role.label,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue