diff --git a/extensions/pagetop-admin/src/handlers.rs b/extensions/pagetop-admin/src/handlers.rs index 8a512d08..c9a3d979 100644 --- a/extensions/pagetop-admin/src/handlers.rs +++ b/extensions/pagetop-admin/src/handlers.rs @@ -4,27 +4,25 @@ use pagetop::prelude::*; use crate::LOCALES_ADMIN; use crate::component::{AdminFrame, ConfigForm}; -use crate::registry::{self, AdminPageKind, AdminPermission, AdminSection}; +use crate::registry::{self, AdminPageKind, AdminSection}; use crate::settings; // **< Dashboard >********************************************************************************** /// GET /admin - Dashboard de administración: lista todas las secciones disponibles. +/// +/// Devuelve [`ErrorPage::AccessDenied`] si el usuario actual no tiene acceso a ninguna página. pub async fn dashboard(request: HttpRequest) -> Result { - let reg = registry::global(); let cx = Context::new(request.clone()); - let sections = reg.ordered_sections(); + let sections = registry::global().ordered_sections(); let title = Lc::t("dashboard-title", &LOCALES_ADMIN); - let content = render_sections(&cx, §ions); + let content = render_sections(&cx, §ions) + .ok_or_else(|| ErrorPage::AccessDenied(Some(request.clone())))?; Page::admin(request) .with_title(title.clone()) - .with_child( - AdminFrame::new() - .with_title(title) - .with_child(Html::with(move |_| content.clone())), - ) + .with_child(AdminFrame::new().with_title(title).with_child(content)) .render() .await } @@ -37,6 +35,9 @@ pub async fn dashboard(request: HttpRequest) -> Result { /// con ninguna [`AdminPage`](crate::registry::AdminPage) registrada explícitamente por una /// extensión; si una extensión reclama esa ruta con su propia página, esa página gana y este /// handler nunca llega a montarse para ella. +/// +/// Devuelve [`ErrorPage::AccessDenied`] si el usuario actual no tiene acceso a ninguna de sus +/// páginas. pub async fn section_page(request: HttpRequest) -> Result { let path = request.path().to_owned(); let reg = registry::global(); @@ -46,22 +47,14 @@ pub async fn section_page(request: HttpRequest) -> Result { .find(|s| s.path == path) .ok_or_else(|| ErrorPage::NotFound(Some(request.clone())))?; - require_permission( - &request, - section.permission.unwrap_or(&AdminPermission::Access), - )?; - let cx = Context::new(request.clone()); let title = section.title.clone(); - let content = render_sections(&cx, &[section]); + let content = render_sections(&cx, &[section]) + .ok_or_else(|| ErrorPage::AccessDenied(Some(request.clone())))?; Page::admin(request) .with_title(title.clone()) - .with_child( - AdminFrame::new() - .with_title(title) - .with_child(Html::with(move |_| content.clone())), - ) + .with_child(AdminFrame::new().with_title(title).with_child(content)) .render() .await } @@ -69,41 +62,56 @@ pub async fn section_page(request: HttpRequest) -> Result { // **< Render compartido >************************************************************************** // Rejilla de secciones con sus páginas (título + descripción). La usan tanto `dashboard()` (todas -// las secciones) como `section_page()` (una sola), filtrando siempre por permiso del usuario actual. -fn render_sections(cx: &Context, sections: &[&AdminSection]) -> Markup { +// las secciones) como `section_page()` (una sola), filtrando siempre por permiso del usuario +// actual. Devuelve `None` si no queda ninguna sección visible con páginas accesibles. +fn render_sections(cx: &Context, candidates: &[&'static AdminSection]) -> Option { let reg = registry::global(); - html! { - div.admin-dashboard-sections { - @for section in sections { - @if section.is_visible(cx) { - @let pages: Vec<_> = reg - .pages_for_section(§ion.key) - .into_iter() - .filter(|p| p.is_accessible(cx)) - .collect(); - @if !pages.is_empty() { - div.admin-dashboard-section { - h2.admin-dashboard-section-title { - a href=(cx.route(section.path.as_str())) { (section.title.using(cx)) } - } - ul.admin-dashboard-section-links { - @for page in pages { - li.admin-dashboard-link { - a href=(cx.route(page.path.as_str())) { (page.title.using(cx)) } - @if let Some(description) = &page.description { - span.admin-dashboard-link-desc { - " - " (description.using(cx)) - } + let sections_with_pages: Vec<_> = candidates + .iter() + .copied() + .filter(|section| section.is_visible(cx)) + .map(|section| { + let pages: Vec<_> = reg + .pages_for_section(§ion.key) + .into_iter() + .filter(|p| p.is_accessible(cx)) + .collect(); + (section, pages) + }) + .filter(|(_, pages)| !pages.is_empty()) + .collect(); + if sections_with_pages.is_empty() { + return None; + } + let mut sections = Container::new().with_prop(PropsOp::add_classes("admin-dashboard-sections")); + for (section, pages) in sections_with_pages { + sections = sections.with_child( + Container::new() + .with_prop(PropsOp::add_classes("admin-dashboard-section")) + .with_child(Html::with(move |cx| { + html! { + h2 class="admin-dashboard-section-title" { + a href=(cx.route(section.path.as_str())) { (section.title.using(cx)) } + } + ul class="admin-dashboard-section-links" { + @for page in &pages { + li class="admin-dashboard-link" { + a href=(cx.route(page.path.as_str())) { + (page.title.using(cx)) + } + @if let Some(description) = &page.description { + span class="admin-dashboard-link-desc" { + " - " (description.using(cx)) } } } } } } - } - } - } + })), + ); } + Some(sections) } // **< Config form - GET >**************************************************************************