From 0e96bcce64020555e1d0ff3905c733b66adc7dc4 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Thu, 2 Jul 2026 20:52:13 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20(auth):=20A=C3=B1ade=20CurrentUser?= =?UTF-8?q?=20y=20sistema=20de=20permisos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nuevo módulo `auth` que exporta `CurrentUser`, `CheckPermission` y `has_permission()`. - `Context` y `Page` exponen `current_user()` vía `Contextual`. - `HttpRequest` accede a extensiones de middleware con `extension()`. - `Extension` incluye nuevo método `configure_middleware`. - `try_dispatch_actions` para despachar acciones con control del flujo. --- src/app.rs | 3 +- src/auth.rs | 170 ++++++++++++++++++ .../component/after_render_component.rs | 5 +- .../component/before_render_component.rs | 5 +- .../component/transform_markup_component.rs | 5 +- src/base/action/page/after_render_body.rs | 5 +- src/base/action/page/before_render_body.rs | 5 +- src/core/action.rs | 2 +- src/core/action/all.rs | 45 ++++- src/core/action/definition.rs | 6 +- src/core/action/list.rs | 41 +++-- src/core/component/context.rs | 69 +++++-- src/core/component/definition.rs | 2 +- src/core/component/message.rs | 2 +- src/core/extension/all.rs | 14 ++ src/core/extension/definition.rs | 50 +++++- src/html/assets/javascript.rs | 8 +- src/lib.rs | 2 + src/prelude.rs | 2 + src/response/page.rs | 11 +- src/web.rs | 80 +++++++-- tests/auth.rs | 96 ++++++++++ 22 files changed, 544 insertions(+), 84 deletions(-) create mode 100644 src/auth.rs create mode 100644 tests/auth.rs diff --git a/src/app.rs b/src/app.rs index 4a009fb7..366f900e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -110,9 +110,10 @@ impl Application { } } - // Construye el router con las rutas de todas las extensiones habilitadas. + // Construye el router con las rutas y el middleware de todas las extensiones habilitadas. fn build_router() -> Router { let router = extension::all::configure_routes(Router::new()); + let router = extension::all::configure_middleware(router); router.fallback(route_not_found) } diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 00000000..8fd10ea0 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,170 @@ +//! Identidad del usuario y sistema de autorización extensible. +//! +//! Define el tipo mínimo [`CurrentUser`] que PageTop inyecta en el [`Context`] de cada petición. +//! Incluye también la acción [`CheckPermission`] para que las extensiones puedan implementar sus +//! propios modelos de permisos, y la función auxiliar [`has_permission()`]. +//! +//! La resolución concreta del usuario (sesión en BD, LDAP, OAuth, ...) y la lógica de permisos +//! (RBAC, grupos LDAP, ...) son responsabilidad de las extensiones de autenticación. + +use crate::core::action::{ActionDispatcher, ActionKey, try_dispatch_actions}; +use crate::core::component::Context; +use crate::{UniqueId, Weight}; + +// **< CurrentUser >******************************************************************************** + +/// Identidad mínima del usuario que ejecuta la petición actual. +/// +/// Se almacena automáticamente en el [`Context`] a partir de la petición HTTP (ver +/// [`Context::new()`](crate::core::component::Context::new)). La identidad se extrae de las +/// extensiones de la petición, que una extensión de autenticación inyecta mediante su *middleware*. +/// +/// Se accede con [`Contextual::current_user()`](crate::core::component::Contextual::current_user). +/// +/// Los datos extendidos del usuario autenticado (roles, permisos, cuenta completa, ...) son +/// responsabilidad de la extensión de autenticación y se obtienen a través de +/// [`HttpRequest::extension`](crate::web::HttpRequest::extension). +#[derive(Clone, Debug)] +pub enum CurrentUser { + /// Usuario no autenticado. + Anonymous, + /// Usuario autenticado con su identificador y nombre visible. + Authenticated { + /// Identificador único del usuario en el sistema. + id: i32, + /// Nombre visible del usuario. + display_name: String, + }, +} + +impl CurrentUser { + /// Devuelve `true` si el usuario no está autenticado. + pub fn is_anonymous(&self) -> bool { + matches!(self, CurrentUser::Anonymous) + } + + /// Devuelve `true` si el usuario está autenticado. + pub fn is_authenticated(&self) -> bool { + matches!(self, CurrentUser::Authenticated { .. }) + } + + /// Devuelve el identificador del usuario, o `None` si es anónimo. + pub fn id(&self) -> Option { + match self { + CurrentUser::Anonymous => None, + CurrentUser::Authenticated { id, .. } => Some(*id), + } + } + + /// Devuelve el nombre visible del usuario, o `None` si es anónimo. + pub fn display_name(&self) -> Option<&str> { + match self { + CurrentUser::Anonymous => None, + CurrentUser::Authenticated { display_name, .. } => Some(display_name), + } + } +} + +// **< CheckPermission >**************************************************************************** + +/// Tipo de función para comprobar si el usuario actual tiene un permiso concreto. +/// +/// Se invoca con: +/// +/// - `cx`: el contexto de renderizado desde el que se puede acceder a la petición HTTP y a +/// cualquier dato inyectado por el *middleware* de autenticación. +/// - `key`: clave del permiso a comprobar (p. ej. `"myapp.edit_posts"`). +/// - `granted`: referencia mutable; el *handler* debe asignarla a `true` si concede el permiso. +pub type FnCheckPermission = fn(cx: &Context, key: &str, granted: &mut bool); + +/// Acción para comprobar si el usuario actual tiene un permiso concreto. +/// +/// Las extensiones de autenticación registran *handlers* de esta acción para implementar su modelo +/// de permisos. Los *handlers* son aditivos: si cualquiera de ellos asigna `granted = true`, el +/// permiso se concede. +/// +/// # Ejemplo +/// +/// ```rust,no_run +/// # use pagetop::prelude::*; +/// fn check_my_permissions(cx: &Context, key: &str, granted: &mut bool) { +/// // Leer datos extendidos de autenticación desde las extensiones de la petición. +/// // Si el usuario tiene el permiso, asignar `*granted = true`. +/// } +/// +/// pub struct MyAuth; +/// +/// impl Extension for MyAuth { +/// fn actions(&self) -> Vec { +/// actions![CheckPermission::new(check_my_permissions)] +/// } +/// } +/// ``` +pub struct CheckPermission { + f: FnCheckPermission, + weight: Weight, +} + +impl ActionDispatcher for CheckPermission { + fn weight(&self) -> Weight { + self.weight + } +} + +impl CheckPermission { + /// Registra una nueva acción para la comprobación de permisos. + pub fn new(f: FnCheckPermission) -> Self { + CheckPermission { f, weight: 0 } + } + + /// Opcional. Acciones con pesos más bajos se aplican antes. Se pueden usar valores negativos. + pub fn with_weight(mut self, value: Weight) -> Self { + self.weight = value; + self + } + + // Despacha las acciones registradas con salida anticipada en cuanto una concede el permiso. + #[inline] + pub(crate) fn check(cx: &Context, key: &str) -> bool { + let mut granted = false; + try_dispatch_actions( + &ActionKey::new(UniqueId::of::(), None, None), + |action: &Self| { + (action.f)(cx, key, &mut granted); + if granted { + std::ops::ControlFlow::Break(()) + } else { + std::ops::ControlFlow::Continue(()) + } + }, + ); + granted + } +} + +// **< has_permission >***************************************************************************** + +/// Comprueba si el usuario actual tiene el permiso indicado. +/// +/// Despacha la acción [`CheckPermission`]: cualquier extensión registrada puede conceder el permiso +/// asignando `granted = true` en su *handler*. Si no hay extensiones de autenticación activas, +/// devuelve `false` para cualquier usuario, incluido el anónimo. +/// +/// La decisión de conceder o denegar permisos al usuario anónimo también es responsabilidad de cada +/// extensión. +/// +/// # Ejemplo +/// +/// ```rust,no_run +/// # use pagetop::prelude::*; +/// async fn my_handler(request: HttpRequest) -> Result { +/// let mut page = Page::new(request.clone()); +/// if !has_permission(page.context(), "myapp.edit") { +/// return Err(ErrorPage::NotFound(request)); +/// } +/// page.render() +/// } +/// ``` +pub fn has_permission(cx: &Context, key: &str) -> bool { + CheckPermission::check(cx, key) +} diff --git a/src/base/action/component/after_render_component.rs b/src/base/action/component/after_render_component.rs index 08eefdb5..c7da161c 100644 --- a/src/base/action/component/after_render_component.rs +++ b/src/base/action/component/after_render_component.rs @@ -10,19 +10,16 @@ pub struct AfterRender { weight: Weight, } -/// Filtro para despachar [`FnActionWithComponent`] después de renderizar un componente `C`. +// Filtro para despachar `FnActionWithComponent` después de renderizar un componente `C`. impl ActionDispatcher for AfterRender { - /// Devuelve el identificador de tipo ([`UniqueId`]) del componente `C`. fn referer_type_id(&self) -> Option { self.referer_type_id } - /// Devuelve el identificador del componente. fn referer_id(&self) -> Option { self.referer_id.clone() } - /// Devuelve el peso para definir el orden de ejecución. fn weight(&self) -> Weight { self.weight } diff --git a/src/base/action/component/before_render_component.rs b/src/base/action/component/before_render_component.rs index e91589a2..2443f05d 100644 --- a/src/base/action/component/before_render_component.rs +++ b/src/base/action/component/before_render_component.rs @@ -10,19 +10,16 @@ pub struct BeforeRender { weight: Weight, } -/// Filtro para despachar [`FnActionWithComponent`] antes de renderizar un componente `C`. +// Filtro para despachar `FnActionWithComponent` antes de renderizar un componente `C`. impl ActionDispatcher for BeforeRender { - /// Devuelve el identificador de tipo ([`UniqueId`]) del componente `C`. fn referer_type_id(&self) -> Option { self.referer_type_id } - /// Devuelve el identificador del componente. fn referer_id(&self) -> Option { self.referer_id.clone() } - /// Devuelve el peso para definir el orden de ejecución. fn weight(&self) -> Weight { self.weight } diff --git a/src/base/action/component/transform_markup_component.rs b/src/base/action/component/transform_markup_component.rs index bed2e192..ab4e2f77 100644 --- a/src/base/action/component/transform_markup_component.rs +++ b/src/base/action/component/transform_markup_component.rs @@ -10,19 +10,16 @@ pub struct TransformMarkup { weight: Weight, } -/// Filtro para despachar [`FnActionTransformMarkup`] sobre el renderizado de un componente `C`. +// Filtro para despachar `FnActionTransformMarkup` sobre el renderizado de un componente `C`. impl ActionDispatcher for TransformMarkup { - /// Devuelve el identificador de tipo ([`UniqueId`]) del componente `C`. fn referer_type_id(&self) -> Option { self.referer_type_id } - /// Devuelve el identificador del componente. fn referer_id(&self) -> Option { self.referer_id.clone() } - /// Devuelve el peso para definir el orden de ejecución. fn weight(&self) -> Weight { self.weight } diff --git a/src/base/action/page/after_render_body.rs b/src/base/action/page/after_render_body.rs index 74ee0df1..ba1d90ea 100644 --- a/src/base/action/page/after_render_body.rs +++ b/src/base/action/page/after_render_body.rs @@ -14,8 +14,8 @@ pub struct AfterRenderBody { weight: Weight, } +// Filtro para despachar `FnActionWithPage` después de renderizar el cuerpo de la página. impl ActionDispatcher for AfterRenderBody { - /// Devuelve el peso para definir el orden de ejecución. fn weight(&self) -> Weight { self.weight } @@ -35,8 +35,7 @@ impl AfterRenderBody { } /// Despacha las acciones. - #[inline(always)] - #[allow(clippy::inline_always)] + #[inline] pub(crate) fn dispatch(page: &mut Page) { dispatch_actions( &ActionKey::new(UniqueId::of::(), None, None), diff --git a/src/base/action/page/before_render_body.rs b/src/base/action/page/before_render_body.rs index 7a72854f..2861ebca 100644 --- a/src/base/action/page/before_render_body.rs +++ b/src/base/action/page/before_render_body.rs @@ -14,8 +14,8 @@ pub struct BeforeRenderBody { weight: Weight, } +// Filtro para despachar `FnActionWithPage` antes de renderizar el cuerpo de la página. impl ActionDispatcher for BeforeRenderBody { - /// Devuelve el peso para definir el orden de ejecución. fn weight(&self) -> Weight { self.weight } @@ -35,8 +35,7 @@ impl BeforeRenderBody { } /// Despacha las acciones. - #[inline(always)] - #[allow(clippy::inline_always)] + #[inline] pub(crate) fn dispatch(page: &mut Page) { dispatch_actions( &ActionKey::new(UniqueId::of::(), None, None), diff --git a/src/core/action.rs b/src/core/action.rs index ceb5eef4..5a10662f 100644 --- a/src/core/action.rs +++ b/src/core/action.rs @@ -12,7 +12,7 @@ use list::ActionsList; mod all; pub(crate) use all::add_action; -pub use all::dispatch_actions; +pub use all::{dispatch_actions, try_dispatch_actions}; // **< actions! >*********************************************************************************** diff --git a/src/core/action/all.rs b/src/core/action/all.rs index 70abd6b4..eb531d33 100644 --- a/src/core/action/all.rs +++ b/src/core/action/all.rs @@ -45,9 +45,9 @@ pub(crate) fn add_action(action: ActionBox) { /// # Parámetros genéricos /// /// - `A`: Tipo de acción que esperamos procesar. Debe implementar [`ActionDispatcher`]. -/// - `F`: Función asociada a cada acción, devuelve un valor de tipo `B`. +/// - `F`: Función que se aplica para una acción dada. /// -/// # Ejemplo de uso +/// # Ejemplo /// /// ```rust,ignore /// pub(crate) fn dispatch(component: &mut C, cx: &mut Context) { @@ -61,12 +61,47 @@ pub(crate) fn add_action(action: ActionBox) { /// ); /// } /// ``` -pub fn dispatch_actions(key: &ActionKey, f: F) +pub fn dispatch_actions(key: &ActionKey, f: F) where A: ActionDispatcher, - F: FnMut(&A) -> B, + F: FnMut(&A), { if let Some(list) = ACTIONS.read().get(key) { - list.iter_map(f); + list.for_each(f); + } +} + +/// Despacha las funciones asociadas a una [`ActionKey`] con posible salida anticipada. +/// +/// Funciona igual que [`dispatch_actions`], pero el *closure* puede devolver +/// [`std::ops::ControlFlow::Continue`] para continuar ejecutando la siguiente acción; o +/// [`std::ops::ControlFlow::Break`] para detener la iteración inmediatamente. +/// +/// # Ejemplo +/// +/// ```rust,ignore +/// pub(crate) fn check(cx: &Context, key: &str) -> bool { +/// let mut granted = false; +/// try_dispatch_actions( +/// &ActionKey::new(UniqueId::of::(), None, None), +/// |action: &Self| { +/// (action.f)(cx, key, &mut granted); +/// if granted { +/// std::ops::ControlFlow::Break(()) +/// } else { +/// std::ops::ControlFlow::Continue(()) +/// } +/// }, +/// ); +/// granted +/// } +/// ``` +pub fn try_dispatch_actions(key: &ActionKey, f: F) +where + A: ActionDispatcher, + F: FnMut(&A) -> std::ops::ControlFlow<()>, +{ + if let Some(list) = ACTIONS.read().get(key) { + list.try_for_each(f); } } diff --git a/src/core/action/definition.rs b/src/core/action/definition.rs index 7392db9f..329f2648 100644 --- a/src/core/action/definition.rs +++ b/src/core/action/definition.rs @@ -45,17 +45,17 @@ impl ActionKey { /// Las acciones tienen que sobrescribir los métodos para el filtro que apliquen. Por defecto /// implementa un filtro nulo. pub trait ActionDispatcher: AnyInfo + Send + Sync { - /// Identificador de tipo ([`UniqueId`]) del objeto referido. En este caso devuelve `None`. + /// Devuelve el identificador de tipo ([`UniqueId`]) del objeto referido. fn referer_type_id(&self) -> Option { None } - /// Identificador del objeto referido. En este caso devuelve `None`. + /// Devuelve el identificador del objeto referido. fn referer_id(&self) -> Option { None } - /// Funciones con pesos más bajos se aplican antes. En este caso siempre devuelve `0`. + /// Devuelve el peso para definir el orden de ejecución. fn weight(&self) -> Weight { 0 } diff --git a/src/core/action/list.rs b/src/core/action/list.rs index f47d3d6e..a62c750e 100644 --- a/src/core/action/list.rs +++ b/src/core/action/list.rs @@ -19,24 +19,35 @@ impl ActionsList { list.sort_by_key(|a| a.weight()); } - pub fn iter_map(&self, mut f: F) + pub fn for_each(&self, mut f: F) where - Self: Sized, A: ActionDispatcher, - F: FnMut(&A) -> B, + F: FnMut(&A), { - let _: Vec<_> = self - .0 - .read() - .iter() - .rev() - .map(|a| { - if let Some(action) = (**a).downcast_ref::() { - f(action); - } else { - trace::error!("Failed to downcast action of type {}", (**a).type_name()); + let list = self.0.read(); + for a in list.iter().rev() { + if let Some(action) = (**a).downcast_ref::() { + f(action); + } else { + trace::error!("Failed to downcast action of type {}", (**a).type_name()); + } + } + } + + pub fn try_for_each(&self, mut f: F) + where + A: ActionDispatcher, + F: FnMut(&A) -> std::ops::ControlFlow<()>, + { + let list = self.0.read(); + for a in list.iter().rev() { + if let Some(action) = (**a).downcast_ref::() { + if f(action).is_break() { + break; } - }) - .collect(); + } else { + trace::error!("Failed to downcast action of type {}", (**a).type_name()); + } + } } } diff --git a/src/core/component/context.rs b/src/core/component/context.rs index d3b13cd3..ad98810b 100644 --- a/src/core/component/context.rs +++ b/src/core/component/context.rs @@ -1,3 +1,4 @@ +use crate::auth::CurrentUser; use crate::core::TypeInfo; use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage}; use crate::core::theme::all::DEFAULT_THEME; @@ -156,6 +157,26 @@ pub trait Contextual: LangId { /// Devuelve una referencia a la petición HTTP asociada, si existe. fn request(&self) -> Option<&HttpRequest>; + /// Devuelve la identidad del usuario actual. + /// + /// Si ninguna extensión de autenticación ha inyectado un + /// [`CurrentUser`](crate::auth::CurrentUser) en las extensiones de la petición HTTP, devuelve + /// `&CurrentUser::Anonymous`. + /// + /// # Ejemplo + /// + /// ```rust,no_run + /// # use pagetop::prelude::*; + /// async fn greet(request: HttpRequest) -> Result { + /// let mut page = Page::new(request); + /// if page.current_user().is_authenticated() { + /// // Personalizar la página para el usuario autenticado. + /// } + /// page.render() + /// } + /// ``` + fn current_user(&self) -> &CurrentUser; + /// Devuelve el tema que se usará para renderizar el documento. fn theme(&self) -> ThemeRef; @@ -284,18 +305,19 @@ pub trait Contextual: LangId { /// ``` #[rustfmt::skip] pub struct Context { - request : Option, // Petición HTTP de origen. - locale : RequestLocale, // Idioma asociado a la petición. - theme : ThemeRef, // Referencia al tema usado para renderizar. - template : TemplateRef, // Plantilla usada para renderizar. - favicon : Option, // Favicon, si se ha definido. - stylesheets: Assets, // Hojas de estilo CSS. - javascripts: Assets, // Scripts JavaScript. - body_props : Props, // Identificador, clases CSS y atributos del . - regions : ChildrenInRegions, // Regiones de componentes para renderizar. - params : HashMap<&'static str, (Box, &'static str)>, // Parámetros en ejecución. - id_counters: RefCell>, // RefCell permite mutar desde build_id(&self). - messages : Vec, // Mensajes de usuario acumulados. + request : Option, // Petición HTTP de origen. + locale : RequestLocale, // Idioma asociado a la petición. + current_user: CurrentUser, // Identidad del usuario actual. + theme : ThemeRef, // Referencia al tema usado para renderizar. + template : TemplateRef, // Plantilla usada para renderizar. + favicon : Option, // Favicon, si se ha definido. + stylesheets : Assets, // Hojas de estilo CSS. + javascripts : Assets, // Scripts JavaScript. + body_props : Props, // Identificador, clases CSS y atributos del . + regions : ChildrenInRegions, // Regiones de componentes para renderizar. + params : HashMap<&'static str, (Box, &'static str)>, // Parámetros en ejecución. + id_counters : RefCell>, // RefCell permite mutar desde build_id(&self). + messages : Vec, // Mensajes de usuario acumulados. } impl Default for Context { @@ -312,9 +334,11 @@ impl Context { #[rustfmt::skip] pub fn new(request: Option) -> Self { let locale = RequestLocale::from_request(request.as_ref()); + let current_user = Self::resolve_current_user(request.as_ref()); Context { request, locale, + current_user, theme : *DEFAULT_THEME, template : DEFAULT_THEME.default_template(), favicon : None, @@ -328,6 +352,15 @@ impl Context { } } + // Extrae el `CurrentUser` inyectado por *middleware* en las extensiones de la petición, o + // `CurrentUser::Anonymous` si no hay petición o ninguna extensión de autenticación está activa. + fn resolve_current_user(request: Option<&HttpRequest>) -> CurrentUser { + request + .and_then(|r| r.extension::()) + .cloned() + .unwrap_or(CurrentUser::Anonymous) + } + // **< Context RENDER >************************************************************************* /// Renderiza los recursos del contexto. @@ -421,9 +454,9 @@ impl Context { } } - /// Acumula un [`StatusMessage`] en el contexto para notificar al visitante. + /// Acumula un [`StatusMessage`] en el contexto para notificar al usuario. /// - /// Pueden generarse en cualquier punto del ciclo de una petición web (manejadores, renderizado, + /// Pueden generarse en cualquier punto del ciclo de una petición web (*handlers*, renderizado, /// lógica de negocio, etc.) que tengan acceso al contexto, y mostrarlos luego, por ejemplo, en /// la página final devuelta al usuario. /// @@ -470,8 +503,10 @@ impl Contextual for Context { #[builder_fn] fn with_request(mut self, request: Option) -> Self { self.request = request; - // Recalcula el locale según la nueva petición y la política de negociación configurada. + // Recalcula el *locale* y el usuario actual según la nueva petición y la política de + // negociación configurada. self.locale = RequestLocale::from_request(self.request.as_ref()); + self.current_user = Self::resolve_current_user(self.request.as_ref()); self } @@ -555,6 +590,10 @@ impl Contextual for Context { self.request.as_ref() } + fn current_user(&self) -> &CurrentUser { + &self.current_user + } + fn theme(&self) -> ThemeRef { self.theme } diff --git a/src/core/component/definition.rs b/src/core/component/definition.rs index f5af637e..0970c58b 100644 --- a/src/core/component/definition.rs +++ b/src/core/component/definition.rs @@ -4,7 +4,7 @@ use crate::core::theme::ThemeRef; use crate::core::{AnyInfo, TypeInfo}; use crate::html::{Markup, html}; -/// Permite clonar un componente. +/// Habilita el clonado de componentes. /// /// Se implementa automáticamente para todo tipo que implemente [`Component`] y [`Clone`]. El método /// [`clone_box`](Self::clone_box) devuelve una copia en la *pila* del componente original, lo que diff --git a/src/core/component/message.rs b/src/core/component/message.rs index 5d3125b4..b3990df6 100644 --- a/src/core/component/message.rs +++ b/src/core/component/message.rs @@ -17,7 +17,7 @@ pub enum MessageLevel { /// /// Representa un mensaje con carácter informativo, una advertencia o un error. A diferencia de /// [`ComponentError`](super::ComponentError), no está ligado a un fallo interno de renderizado, -/// puede generarse en cualquier punto del procesamiento de una petición web (manejadores, +/// puede generarse en cualquier punto del procesamiento de una petición web (*handlers*, /// renderizado, lógica de negocio, etc.). /// /// El texto se almacena como [`L10n`] para resolverse con el idioma del contexto en el momento de diff --git a/src/core/extension/all.rs b/src/core/extension/all.rs index f9081d83..bc26cf43 100644 --- a/src/core/extension/all.rs +++ b/src/core/extension/all.rs @@ -99,3 +99,17 @@ pub fn configure_routes(router: Router) -> Router { router } + +// **< CONFIGURA EL MIDDLEWARE GLOBAL >************************************************************* + +/// Aplica las capas de *middleware* globales de todas las extensiones sobre el router ya enrutado. +/// +/// Se llama después de [`configure_routes`] para garantizar que las capas envuelven todas las +/// rutas de la aplicación, independientemente del orden de las extensiones. +pub fn configure_middleware(router: Router) -> Router { + EXTENSIONS + .get() + .into_iter() + .flatten() + .fold(router, |r, e| e.configure_middleware(r)) +} diff --git a/src/core/extension/definition.rs b/src/core/extension/definition.rs index 496001f9..9187f692 100644 --- a/src/core/extension/definition.rs +++ b/src/core/extension/definition.rs @@ -100,7 +100,6 @@ pub trait Extension: AnyInfo + Send + Sync { /// | Ruta HTTP | `.route("/path", web::get(handler))` | /// | Rutas bajo prefijo común | `.nest("/prefix", sub_router)` | /// | Archivos estáticos | `serve_static_files!(router, [...] => "/path")` | - /// | Capa de *middleware* | `.layer(some_layer)` | /// | Estado compartido entre *handlers* | `.with_state(my_state)` | /// /// # Ejemplos @@ -143,7 +142,16 @@ pub trait Extension: AnyInfo + Send + Sync { /// } /// ``` /// - /// ## Rutas con capa de *middleware* + /// ## Rutas con *middleware* acotado a esta extensión + /// + /// Cada `Router` mantiene su propia pila de *middleware* independiente. Cuando se crea un + /// `Router::new()` separado y se llama a `.layer()` sobre él, esa capa sólo se aplica a las + /// rutas de ese *router* concreto. Al fusionarlo con `.merge()` en el `router` principal, cada + /// ruta se añade con su *middleware* asociado, sin tocar las demás. + /// + /// Otra cosa es llamar a `.layer()` directamente sobre el `router` principal que se recibe como + /// parámetro, porque ese objeto ya contiene todas las rutas acumuladas por extensiones + /// anteriores, y la capa se aplica a todas las rutas, no sólo a las propias. /// /// ```rust,ignore /// # use pagetop::prelude::*; @@ -151,13 +159,17 @@ pub trait Extension: AnyInfo + Send + Sync { /// /// impl Extension for Api { /// fn configure_router(&self, router: Router) -> Router { - /// router + /// let api = Router::new() /// .route("/api/data", web::get(get_data)) - /// .layer(auth_layer()) + /// .layer(auth_layer()); + /// router.merge(api) /// } /// } /// ``` /// + /// Para *middleware* que deba cubrir **todas** las rutas, usar + /// [`configure_middleware`](Self::configure_middleware). + /// /// ## Archivos estáticos /// /// La macro [`serve_static_files!`](crate::serve_static_files) sombrea `router` internamente, @@ -177,6 +189,36 @@ pub trait Extension: AnyInfo + Send + Sync { fn configure_router(&self, router: Router) -> Router { router } + + /// Añade capas de *middleware* globales al *router* ya completamente preparado. + /// + /// Se invoca **después** de que todas las extensiones hayan registrado sus rutas con + /// [`configure_router`](Self::configure_router), de modo que las capas añadidas aquí se aplican + /// a **todas** las rutas de la aplicación, independientemente del orden de las extensiones. + /// + /// Usar este método cuando el *middleware* deba interceptar cualquier petición entrante (p. ej. + /// resolución de sesión, autenticación, cabeceras de seguridad, ...). + /// + /// # Ejemplo + /// + /// ```rust,no_run + /// # use pagetop::prelude::*; + /// # use axum::middleware; + /// # async fn session_middleware( + /// # req: axum::extract::Request, + /// # next: middleware::Next, + /// # ) -> axum::response::Response { next.run(req).await } + /// pub struct MyAuth; + /// + /// impl Extension for MyAuth { + /// fn configure_middleware(&self, router: Router) -> Router { + /// router.layer(middleware::from_fn(session_middleware)) + /// } + /// } + /// ``` + fn configure_middleware(&self, router: Router) -> Router { + router + } } /// Representa una referencia a una extensión. diff --git a/src/html/assets/javascript.rs b/src/html/assets/javascript.rs index 03d3cff4..1ddbff28 100644 --- a/src/html/assets/javascript.rs +++ b/src/html/assets/javascript.rs @@ -15,7 +15,7 @@ use crate::{AutoDefault, CowStr, Weight, util}; /// ejecuta en cuanto esté listo, **sin garantizar** el orden relativo respecto a otros scripts. /// - [`Inline`] - Inserta el código directamente en la etiqueta `