✨ (auth): Añade permisos tipados
Sustituye las claves de permiso `&str` sueltas por el trait `Permission` (clave, etiqueta y grupo), y añade `require_permission()` para cortar un handler con `ErrorPage::AccessDenied` antes de construir nada. Para que `require_permission()` pueda fallar sin un `Context` previo, `ErrorPage` pasa a envolver `Option<HttpRequest>`, `Context::new()` recibe ahora `HttpRequest` sin `Option`, y se añade `Context::admin()` para la plantilla de administración.
This commit is contained in:
parent
ea0dc021ce
commit
747e64ce34
10 changed files with 297 additions and 120 deletions
187
src/auth.rs
187
src/auth.rs
|
|
@ -1,31 +1,44 @@
|
||||||
//! Identidad del usuario y sistema de autorización extensible.
|
//! 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.
|
//! Define el tipo [`CurrentUser`] que PageTop inyecta en el [`Context`] con la información mínima
|
||||||
//! Incluye también la acción [`CheckPermission`] para que las extensiones puedan implementar sus
|
//! sobre el usuario que ejecuta la petición actual ([`HttpRequest`]).
|
||||||
//! propios modelos de permisos, y la función auxiliar [`has_permission()`].
|
//!
|
||||||
|
//! Incluye la acción [`CheckPermission`] para que las extensiones puedan implementar sus propios
|
||||||
|
//! modelos de permisos. Y también las funciones auxiliares [`has_permission()`] y
|
||||||
|
//! [`require_permission()`] para validar en el comienzo de cada handler, antes de construir ni
|
||||||
|
//! ejecutar nada, si la petición está autorizada.
|
||||||
//!
|
//!
|
||||||
//! La resolución concreta del usuario (sesión en BD, LDAP, OAuth, ...) y la lógica de permisos
|
//! 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. Un concepto
|
//! (RBAC, grupos LDAP, ...) son responsabilidad de las extensiones de autenticación. Un concepto
|
||||||
//! como "administrador" que tiene todos los permisos no es responsabilidad del core: cada extensión
|
//! como "administrador" que tiene todos los permisos no es responsabilidad de PageTop: cada
|
||||||
//! decide si existe y, si es así, lo aplica dentro de su propio handler [`CheckPermission`].
|
//! extensión decide si existe y, si es así, lo aplica dentro de su propio handler
|
||||||
|
//! [`CheckPermission`].
|
||||||
|
//!
|
||||||
|
//! [`Context`]: crate::core::component::Context
|
||||||
|
|
||||||
use crate::core::action::{ActionDispatcher, ActionKey, try_dispatch_actions};
|
use crate::core::action::{ActionDispatcher, ActionKey, try_dispatch_actions};
|
||||||
use crate::core::component::Context;
|
use crate::locale::L10n;
|
||||||
use crate::{UniqueId, Weight};
|
use crate::response::ErrorPage;
|
||||||
|
use crate::web::HttpRequest;
|
||||||
|
use crate::{CowStr, UniqueId, Weight};
|
||||||
|
|
||||||
// **< CurrentUser >********************************************************************************
|
// **< CurrentUser >********************************************************************************
|
||||||
|
|
||||||
/// Identidad mínima del usuario que ejecuta la petición actual.
|
/// 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
|
/// Se almacena automáticamente en el [`Context`] a partir de la petición HTTP. La identidad se
|
||||||
/// [`Context::new()`](crate::core::component::Context::new)). La identidad se extrae de las
|
/// extrae de las extensiones de la petición, que una extensión de autenticación inyecta mediante su
|
||||||
/// extensiones de la petición, que una extensión de autenticación inyecta mediante su middleware.
|
/// middleware.
|
||||||
///
|
///
|
||||||
/// Se accede con [`Contextual::current_user()`](crate::core::component::Contextual::current_user).
|
/// Se accede usando [`Contextual::current_user()`].
|
||||||
///
|
///
|
||||||
/// Los datos extendidos del usuario autenticado (roles, permisos, cuenta completa, ...) son
|
/// 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
|
/// responsabilidad de la extensión de autenticación y se obtienen a través de
|
||||||
/// [`HttpRequest::extension`](crate::web::HttpRequest::extension).
|
/// [`HttpRequest::extension`].
|
||||||
|
///
|
||||||
|
/// [`Context`]: crate::core::component::Context
|
||||||
|
/// [`Contextual::current_user()`]: crate::core::component::Contextual::current_user
|
||||||
|
/// [`HttpRequest::extension`]: crate::web::HttpRequest::extension
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum CurrentUser {
|
pub enum CurrentUser {
|
||||||
/// Usuario no autenticado.
|
/// Usuario no autenticado.
|
||||||
|
|
@ -67,31 +80,93 @@ impl CurrentUser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// **< Permission >*********************************************************************************
|
||||||
|
|
||||||
|
/// Clave tipada de un permiso de acceso.
|
||||||
|
///
|
||||||
|
/// Cada extensión que lo requiera puede definir su propio enum de permisos e implementar este trait
|
||||||
|
/// para obtener la clave textual que finalmente se compara contra su modelo de permisos (RBAC en
|
||||||
|
/// base de datos, grupos LDAP, ...).
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// # use pagetop::auth::Permission;
|
||||||
|
/// # use pagetop::CowStr;
|
||||||
|
/// #[derive(Clone, Copy, Debug)]
|
||||||
|
/// pub enum MyPermission {
|
||||||
|
/// EditPosts,
|
||||||
|
/// DeletePosts,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl Permission for MyPermission {
|
||||||
|
/// fn key(&self) -> CowStr {
|
||||||
|
/// match self {
|
||||||
|
/// Self::EditPosts => "my_extension.edit_posts".into(),
|
||||||
|
/// Self::DeletePosts => "my_extension.delete_posts".into(),
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub trait Permission: Send + Sync {
|
||||||
|
/// Clave única del permiso (p. ej. `"my_extension.edit_posts"`).
|
||||||
|
fn key(&self) -> CowStr;
|
||||||
|
|
||||||
|
/// Descripción breve para humanos (p. ej. en una pantalla de asignación de permisos a roles).
|
||||||
|
///
|
||||||
|
/// Por defecto devuelve la propia clave; una extensión que registre sus permisos en un catálogo
|
||||||
|
/// visible debería sobrescribirlo con un texto traducible.
|
||||||
|
fn label(&self) -> L10n {
|
||||||
|
L10n::n(self.key())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Identificador estable de la categoría del permiso, usado para agrupar en un catálogo (p.
|
||||||
|
/// ej. `"administration"`). Por defecto no pertenece a ningún grupo.
|
||||||
|
fn group(&self) -> &'static str {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Título traducible de [`group()`](Self::group), mostrado en la UI de administración.
|
||||||
|
///
|
||||||
|
/// Por defecto reutiliza el propio identificador del grupo como texto fijo.
|
||||||
|
fn group_label(&self) -> L10n {
|
||||||
|
L10n::n(self.group())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Referencia estática a un permiso de acceso.
|
||||||
|
///
|
||||||
|
/// Es el tipo que recorre toda la API de autorización ([`has_permission()`],
|
||||||
|
/// [`require_permission()`] o [`CheckPermission`]).
|
||||||
|
pub type PermissionRef = &'static dyn Permission;
|
||||||
|
|
||||||
// **< CheckPermission >****************************************************************************
|
// **< CheckPermission >****************************************************************************
|
||||||
|
|
||||||
/// Tipo de función para comprobar si el usuario actual tiene un permiso concreto.
|
/// Tipo de función para comprobar si el usuario actual tiene un permiso concreto.
|
||||||
///
|
///
|
||||||
/// Se invoca con:
|
/// Se invoca con:
|
||||||
///
|
///
|
||||||
/// - `cx`: el contexto de renderizado desde el que se puede acceder a la petición HTTP y a
|
/// - `request`: petición HTTP desde la que se accede a los datos inyectados por el middleware de
|
||||||
/// cualquier dato inyectado por el middleware de autenticación.
|
/// autenticación.
|
||||||
/// - `key`: clave del permiso a comprobar (p. ej. `"myapp.edit_posts"`).
|
/// - `perm`: permiso a comprobar; el handler usará [`Permission::key()`] para identificarlo contra
|
||||||
|
/// su propio modelo de permisos.
|
||||||
/// - `granted`: referencia mutable; el handler debe asignarla a `true` si concede el permiso.
|
/// - `granted`: referencia mutable; el handler debe asignarla a `true` si concede el permiso.
|
||||||
pub type FnCheckPermission = fn(cx: &Context, key: &str, granted: &mut bool);
|
pub type FnActionCheckPerm = fn(request: &HttpRequest, perm: PermissionRef, granted: &mut bool);
|
||||||
|
|
||||||
/// Acción para comprobar si el usuario actual tiene un permiso concreto.
|
/// 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
|
/// Las extensiones de autenticación pueden registrar su handler sobre esta acción para implementar
|
||||||
/// permisos. Los handlers son aditivos: si cualquiera de ellos asigna `granted = true`, el permiso
|
/// su modelo de permisos. Los handlers son aditivos de tal forma que si cualquiera de ellos asigna
|
||||||
/// se concede.
|
/// `granted = true`, el permiso se concede.
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// fn check_my_permissions(cx: &Context, key: &str, granted: &mut bool) {
|
/// fn check_my_permissions(request: &HttpRequest, perm: PermissionRef, granted: &mut bool) {
|
||||||
/// // Leer datos extendidos de autenticación desde las extensiones de la petición.
|
/// // Leer los datos extendidos de autenticación inyectados en la petición.
|
||||||
/// // Si el usuario tiene el permiso, asignar `*granted = true`.
|
/// // Comparar `perm.key()` contra el modelo propio.
|
||||||
|
/// // Si concede el permiso, asignar `*granted = true`.
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// pub struct MyAuth;
|
/// pub struct MyAuth;
|
||||||
|
|
@ -104,7 +179,7 @@ pub type FnCheckPermission = fn(cx: &Context, key: &str, granted: &mut bool);
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub struct CheckPermission {
|
pub struct CheckPermission {
|
||||||
f: FnCheckPermission,
|
f: FnActionCheckPerm,
|
||||||
weight: Weight,
|
weight: Weight,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,7 +191,7 @@ impl ActionDispatcher for CheckPermission {
|
||||||
|
|
||||||
impl CheckPermission {
|
impl CheckPermission {
|
||||||
/// Registra una nueva acción para la comprobación de permisos.
|
/// Registra una nueva acción para la comprobación de permisos.
|
||||||
pub fn new(f: FnCheckPermission) -> Self {
|
pub fn new(f: FnActionCheckPerm) -> Self {
|
||||||
CheckPermission { f, weight: 0 }
|
CheckPermission { f, weight: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,12 +203,12 @@ impl CheckPermission {
|
||||||
|
|
||||||
// Despacha las acciones registradas con salida anticipada en cuanto una concede el permiso.
|
// Despacha las acciones registradas con salida anticipada en cuanto una concede el permiso.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn check(cx: &Context, key: &str) -> bool {
|
pub(crate) fn check(request: &HttpRequest, perm: PermissionRef) -> bool {
|
||||||
let mut granted = false;
|
let mut granted = false;
|
||||||
try_dispatch_actions(
|
try_dispatch_actions(
|
||||||
&ActionKey::new(UniqueId::of::<Self>(), None, None),
|
&ActionKey::new(UniqueId::of::<Self>(), None, None),
|
||||||
|action: &Self| {
|
|action: &Self| {
|
||||||
(action.f)(cx, key, &mut granted);
|
(action.f)(request, perm, &mut granted);
|
||||||
if granted {
|
if granted {
|
||||||
std::ops::ControlFlow::Break(())
|
std::ops::ControlFlow::Break(())
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -160,14 +235,62 @@ impl CheckPermission {
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
|
/// # #[derive(Clone, Copy, Debug)]
|
||||||
|
/// # enum MyPermission { Edit }
|
||||||
|
/// # impl Permission for MyPermission {
|
||||||
|
/// # fn key(&self) -> CowStr { "myapp.edit".into() }
|
||||||
|
/// # }
|
||||||
/// async fn my_handler(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
/// async fn my_handler(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
/// let mut page = Page::new(request.clone());
|
/// if !has_permission(&request, &MyPermission::Edit) {
|
||||||
/// if !has_permission(page.context(), "myapp.edit") {
|
/// return Err(ErrorPage::NotFound(Some(request)));
|
||||||
/// return Err(ErrorPage::NotFound(request));
|
|
||||||
/// }
|
/// }
|
||||||
/// page.render().await
|
/// Page::new(request).render().await
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn has_permission(cx: &Context, key: &str) -> bool {
|
pub fn has_permission(request: &HttpRequest, perm: PermissionRef) -> bool {
|
||||||
CheckPermission::check(cx, key)
|
CheckPermission::check(request, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< require_permission >*************************************************************************
|
||||||
|
|
||||||
|
/// Comprueba un permiso y devuelve `Err(ErrorPage::AccessDenied)` si se deniega.
|
||||||
|
///
|
||||||
|
/// Ejecuta [`has_permission()`] para el caso más habitual: detener un handler con una respuesta 403
|
||||||
|
/// en cuanto falta el permiso, sin repetir el `if`/`return` en cada punto de comprobación. Se hace
|
||||||
|
/// directamente sobre la petición, antes de construir ni ejecutar nada (`Context`, `Page`,
|
||||||
|
/// consultas a datos, etc.), para no hacer ningún trabajo si la petición no está autorizada.
|
||||||
|
///
|
||||||
|
/// Si la aplicación necesita ocultar la existencia del recurso a quien no tiene permiso (devolver
|
||||||
|
/// un 404 en vez de un 403), no se puede reutilizar esta función: hay que llamar a
|
||||||
|
/// `has_permission()` directamente, como en su propio ejemplo.
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
|
/// # #[derive(Clone, Copy, Debug)]
|
||||||
|
/// # enum MyPermission { Edit }
|
||||||
|
/// # impl Permission for MyPermission {
|
||||||
|
/// # fn key(&self) -> CowStr { "myapp.edit".into() }
|
||||||
|
/// # }
|
||||||
|
/// async fn my_handler(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
|
/// // Comprueba si la petición está autorizada.
|
||||||
|
/// require_permission(&request, &MyPermission::Edit)?;
|
||||||
|
///
|
||||||
|
/// // Ejecuta las instrucciones propias de la petición.
|
||||||
|
/// Page::new(request)
|
||||||
|
/// .with_child(Html::with(|_| html! { p { "You have permission!" } }))
|
||||||
|
/// .render()
|
||||||
|
/// .await
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
// `ErrorPage` incluye `Option<HttpRequest>` en cada variante y es el tipo de error ya establecido
|
||||||
|
// para toda la respuesta HTTP; boxearlo aquí sólo para esta función no compensa.
|
||||||
|
#[allow(clippy::result_large_err)]
|
||||||
|
pub fn require_permission(request: &HttpRequest, perm: PermissionRef) -> Result<(), ErrorPage> {
|
||||||
|
if has_permission(request, perm) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ErrorPage::AccessDenied(Some(request.clone())))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ pub enum ContextError {
|
||||||
/// `Contextual` extiende [`LangId`] para establecer el idioma del documento y añade métodos para:
|
/// `Contextual` extiende [`LangId`] para establecer el idioma del documento y añade métodos para:
|
||||||
///
|
///
|
||||||
/// - Almacenar la **petición HTTP** de origen.
|
/// - Almacenar la **petición HTTP** de origen.
|
||||||
/// - Seleccionar el **tema** y la **plantilla** de renderizado.
|
/// - Seleccionar la **plantilla** y el **tema** de renderizado.
|
||||||
/// - Administrar **recursos** del documento como el icono [`Favicon`], las hojas de estilo
|
/// - Administrar **recursos** del documento como el icono [`Favicon`], las hojas de estilo
|
||||||
/// [`StyleSheet`] o los scripts [`JavaScript`] mediante [`AssetsOp`].
|
/// [`StyleSheet`] o los scripts [`JavaScript`] mediante [`AssetsOp`].
|
||||||
/// - Leer y mantener **parámetros dinámicos tipados** de contexto.
|
/// - Leer y mantener **parámetros dinámicos tipados** de contexto.
|
||||||
|
|
@ -77,8 +77,8 @@ pub enum ContextError {
|
||||||
/// # use pagetop_aliner::Aliner;
|
/// # use pagetop_aliner::Aliner;
|
||||||
/// fn prepare_context<C: Contextual>(cx: C) -> C {
|
/// fn prepare_context<C: Contextual>(cx: C) -> C {
|
||||||
/// cx.with_langid(&Locale::resolve("es-ES"))
|
/// cx.with_langid(&Locale::resolve("es-ES"))
|
||||||
/// .with_theme(&Aliner)
|
|
||||||
/// .with_template(&CoreTemplate::Standard)
|
/// .with_template(&CoreTemplate::Standard)
|
||||||
|
/// .with_theme(&Aliner)
|
||||||
/// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
|
/// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
|
||||||
/// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/app.css")))
|
/// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/app.css")))
|
||||||
/// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/app.js")))
|
/// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/app.js")))
|
||||||
|
|
@ -93,17 +93,23 @@ pub trait Contextual: LangId {
|
||||||
fn with_langid(self, language: &impl LangId) -> Self;
|
fn with_langid(self, language: &impl LangId) -> Self;
|
||||||
|
|
||||||
/// Almacena la petición HTTP de origen en el contexto.
|
/// Almacena la petición HTTP de origen en el contexto.
|
||||||
|
///
|
||||||
|
/// También recalcula el idioma ([`RequestLocale::from_request()`]) y
|
||||||
|
/// [`current_user()`](Self::current_user) a partir de la petición indicada, descartando
|
||||||
|
/// cualquier idioma forzado antes con [`with_langid()`](Self::with_langid) o el usuario ya
|
||||||
|
/// resuelto. Si necesitas forzar el idioma o el usuario, llama a `with_request()` primero en
|
||||||
|
/// la cadena de construcción, nunca después.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_request(self, request: Option<HttpRequest>) -> Self;
|
fn with_request(self, request: Option<HttpRequest>) -> Self;
|
||||||
|
|
||||||
/// Especifica el tema para renderizar el documento.
|
|
||||||
#[builder_fn]
|
|
||||||
fn with_theme(self, theme: ThemeRef) -> Self;
|
|
||||||
|
|
||||||
/// Especifica la plantilla para renderizar el documento.
|
/// Especifica la plantilla para renderizar el documento.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_template(self, template: TemplateRef) -> Self;
|
fn with_template(self, template: TemplateRef) -> Self;
|
||||||
|
|
||||||
|
/// Especifica el tema para renderizar el documento.
|
||||||
|
#[builder_fn]
|
||||||
|
fn with_theme(self, theme: ThemeRef) -> Self;
|
||||||
|
|
||||||
/// Añade o modifica un parámetro dinámico del contexto.
|
/// Añade o modifica un parámetro dinámico del contexto.
|
||||||
///
|
///
|
||||||
/// El valor se almacena junto con el nombre de su tipo, lo que permite generar mensajes de
|
/// El valor se almacena junto con el nombre de su tipo, lo que permite generar mensajes de
|
||||||
|
|
@ -114,7 +120,7 @@ pub trait Contextual: LangId {
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// let cx = Context::new(None)
|
/// let cx = Context::default()
|
||||||
/// .with_param("user_id", 42_i32)
|
/// .with_param("user_id", 42_i32)
|
||||||
/// .with_param("title", "Hello".to_string())
|
/// .with_param("title", "Hello".to_string())
|
||||||
/// .with_param("flags", vec!["a", "b"]);
|
/// .with_param("flags", vec!["a", "b"]);
|
||||||
|
|
@ -165,12 +171,12 @@ pub trait Contextual: LangId {
|
||||||
/// ```
|
/// ```
|
||||||
fn current_user(&self) -> &CurrentUser;
|
fn current_user(&self) -> &CurrentUser;
|
||||||
|
|
||||||
/// Devuelve el tema que se usará para renderizar el documento.
|
|
||||||
fn theme(&self) -> ThemeRef;
|
|
||||||
|
|
||||||
/// Devuelve la plantilla configurada para renderizar el documento.
|
/// Devuelve la plantilla configurada para renderizar el documento.
|
||||||
fn template(&self) -> TemplateRef;
|
fn template(&self) -> TemplateRef;
|
||||||
|
|
||||||
|
/// Devuelve el tema que se usará para renderizar el documento.
|
||||||
|
fn theme(&self) -> ThemeRef;
|
||||||
|
|
||||||
/// Recupera una *referencia tipada* al parámetro solicitado.
|
/// Recupera una *referencia tipada* al parámetro solicitado.
|
||||||
///
|
///
|
||||||
/// Devuelve:
|
/// Devuelve:
|
||||||
|
|
@ -183,7 +189,7 @@ pub trait Contextual: LangId {
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// let cx = Context::new(None)
|
/// let cx = Context::default()
|
||||||
/// .with_param("user_id", 42_i32)
|
/// .with_param("user_id", 42_i32)
|
||||||
/// .with_param("title", "Hello".to_string());
|
/// .with_param("title", "Hello".to_string());
|
||||||
///
|
///
|
||||||
|
|
@ -230,7 +236,7 @@ pub trait Contextual: LangId {
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// let mut cx = Context::new(None).with_param("temp", 1u8);
|
/// let mut cx = Context::default().with_param("temp", 1u8);
|
||||||
/// assert!(cx.remove_param("temp"));
|
/// assert!(cx.remove_param("temp"));
|
||||||
/// assert!(!cx.remove_param("temp")); // ya no existe
|
/// assert!(!cx.remove_param("temp")); // ya no existe
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -243,7 +249,7 @@ pub trait Contextual: LangId {
|
||||||
/// [`Page::new()`](crate::response::Page::new) o [`Page::admin()`](crate::response::Page::admin)),
|
/// [`Page::new()`](crate::response::Page::new) o [`Page::admin()`](crate::response::Page::admin)),
|
||||||
/// y es la única vía por la que un componente, una acción o el tema activo conocen: la petición
|
/// y es la única vía por la que un componente, una acción o el tema activo conocen: la petición
|
||||||
/// HTTP de origen, el idioma negociado, el usuario autenticado
|
/// HTTP de origen, el idioma negociado, el usuario autenticado
|
||||||
/// ([`current_user()`](Contextual::current_user)), el tema y la plantilla en uso, y los recursos
|
/// ([`current_user()`](Contextual::current_user)), la plantilla y el tema en uso, y los recursos
|
||||||
/// (favicon, hojas de estilo, scripts) acumulados hasta ese momento. Otros datos que los
|
/// (favicon, hojas de estilo, scripts) acumulados hasta ese momento. Otros datos que los
|
||||||
/// componentes necesiten durante el renderizado pueden ser parámetros dinámicos tipados con
|
/// componentes necesiten durante el renderizado pueden ser parámetros dinámicos tipados con
|
||||||
/// [`with_param()`](Contextual::with_param)/[`param()`](Contextual::param).
|
/// [`with_param()`](Contextual::with_param)/[`param()`](Contextual::param).
|
||||||
|
|
@ -267,8 +273,8 @@ pub trait Contextual: LangId {
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// # use pagetop_aliner::Aliner;
|
/// # use pagetop_aliner::Aliner;
|
||||||
/// fn new_context(request: HttpRequest) -> Context {
|
/// # fn new_context(request: HttpRequest) -> Context {
|
||||||
/// Context::new(Some(request))
|
/// let cx = Context::new(request)
|
||||||
/// // Establece el idioma del documento a español.
|
/// // Establece el idioma del documento a español.
|
||||||
/// .with_langid(&Locale::resolve("es-ES"))
|
/// .with_langid(&Locale::resolve("es-ES"))
|
||||||
/// // Establece el tema para renderizar.
|
/// // Establece el tema para renderizar.
|
||||||
|
|
@ -280,8 +286,8 @@ pub trait Contextual: LangId {
|
||||||
/// // Añade un script JavaScript.
|
/// // Añade un script JavaScript.
|
||||||
/// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/main.js")))
|
/// .with_assets(AssetsOp::AddJavaScript(JavaScript::defer("/js/main.js")))
|
||||||
/// // Añade un parámetro dinámico al contexto.
|
/// // Añade un parámetro dinámico al contexto.
|
||||||
/// .with_param("user_id", 42)
|
/// .with_param("user_id", 42);
|
||||||
/// }
|
/// # cx }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// Y hace operaciones con un contexto dado:
|
/// Y hace operaciones con un contexto dado:
|
||||||
|
|
@ -309,8 +315,8 @@ pub struct Context {
|
||||||
request : Option<HttpRequest>, // Petición HTTP de origen.
|
request : Option<HttpRequest>, // Petición HTTP de origen.
|
||||||
locale : RequestLocale, // Idioma asociado a la petición.
|
locale : RequestLocale, // Idioma asociado a la petición.
|
||||||
current_user: CurrentUser, // Identidad del usuario actual.
|
current_user: CurrentUser, // Identidad del usuario actual.
|
||||||
theme : ThemeRef, // Referencia al tema usado para renderizar.
|
|
||||||
template : TemplateRef, // Plantilla usada para renderizar.
|
template : TemplateRef, // Plantilla usada para renderizar.
|
||||||
|
theme : ThemeRef, // Referencia al tema usado para renderizar.
|
||||||
favicon : Option<Favicon>, // Favicon, si se ha definido.
|
favicon : Option<Favicon>, // Favicon, si se ha definido.
|
||||||
preloads : Assets<Preload>, // Recursos para precarga.
|
preloads : Assets<Preload>, // Recursos para precarga.
|
||||||
stylesheets : Assets<StyleSheet>, // Hojas de estilo CSS.
|
stylesheets : Assets<StyleSheet>, // Hojas de estilo CSS.
|
||||||
|
|
@ -324,25 +330,25 @@ pub struct Context {
|
||||||
|
|
||||||
impl Default for Context {
|
impl Default for Context {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Context::new(None)
|
Self::base(None, &CoreTemplate::Standard)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
/// Crea un nuevo contexto asociado a una petición HTTP.
|
// Construye el `Context` compartido por `new()`, `admin()` y `Default::default()`, evitando
|
||||||
///
|
// duplicar la lista de campos entre ambos (y la recursión que tendría `new()` llamando a
|
||||||
/// El contexto inicializa el idioma, el tema y la plantilla por defecto, sin favicon ni otros
|
// `Default::default()`, o viceversa). Recibe la plantilla para que `admin()` no tenga que
|
||||||
/// recursos cargados.
|
// construir con la plantilla estándar y sobrescribirla después.
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
pub fn new(request: Option<HttpRequest>) -> Self {
|
fn base(request: Option<HttpRequest>, template: TemplateRef) -> Self {
|
||||||
let locale = RequestLocale::from_request(request.as_ref());
|
let locale = RequestLocale::from_request(request.as_ref());
|
||||||
let current_user = Self::resolve_current_user(request.as_ref());
|
let current_user = Self::resolve_current_user(request.as_ref());
|
||||||
Context {
|
Context {
|
||||||
request,
|
request,
|
||||||
locale,
|
locale,
|
||||||
current_user,
|
current_user,
|
||||||
|
template,
|
||||||
theme : *DEFAULT_THEME,
|
theme : *DEFAULT_THEME,
|
||||||
template : &CoreTemplate::Standard,
|
|
||||||
favicon : None,
|
favicon : None,
|
||||||
preloads : Assets::<Preload>::new(),
|
preloads : Assets::<Preload>::new(),
|
||||||
stylesheets: Assets::<StyleSheet>::new(),
|
stylesheets: Assets::<StyleSheet>::new(),
|
||||||
|
|
@ -355,6 +361,25 @@ impl Context {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Crea un nuevo contexto asociado a una petición HTTP.
|
||||||
|
///
|
||||||
|
/// El contexto inicializa el idioma, el tema y la plantilla por defecto, sin favicon ni otros
|
||||||
|
/// recursos cargados.
|
||||||
|
///
|
||||||
|
/// Para un contexto sin petición (renderizar un componente de forma aislada, en tests o fuera
|
||||||
|
/// del ciclo de una petición web), usa [`Context::default()`].
|
||||||
|
pub fn new(request: HttpRequest) -> Self {
|
||||||
|
Self::base(Some(request), &CoreTemplate::Standard)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea un nuevo contexto asociado a una petición HTTP, con la plantilla de administración.
|
||||||
|
///
|
||||||
|
/// El contexto inicializa el idioma, el tema y la plantilla [`CoreTemplate::Admin`], sin
|
||||||
|
/// favicon ni otros recursos cargados.
|
||||||
|
pub fn admin(request: HttpRequest) -> Self {
|
||||||
|
Self::base(Some(request), &CoreTemplate::Admin)
|
||||||
|
}
|
||||||
|
|
||||||
// Extrae el `CurrentUser` inyectado por middleware en las extensiones de la petición, o
|
// 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.
|
// `CurrentUser::Anonymous` si no hay petición o ninguna extensión de autenticación está activa.
|
||||||
fn resolve_current_user(request: Option<&HttpRequest>) -> CurrentUser {
|
fn resolve_current_user(request: Option<&HttpRequest>) -> CurrentUser {
|
||||||
|
|
@ -483,7 +508,7 @@ impl Context {
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// # let mut cx = Context::new(None);
|
/// # let mut cx = Context::default();
|
||||||
/// cx.push_message(MessageLevel::Warning, L10n::n("Session is not valid"));
|
/// cx.push_message(MessageLevel::Warning, L10n::n("Session is not valid"));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn push_message(&mut self, level: MessageLevel, text: L10n) {
|
pub fn push_message(&mut self, level: MessageLevel, text: L10n) {
|
||||||
|
|
@ -501,14 +526,19 @@ impl Context {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Permite a [`Context`](crate::core::component::Context) actuar como proveedor de idioma.
|
/// Permite a [`Context`] actuar como proveedor de idioma.
|
||||||
///
|
///
|
||||||
/// Internamente delega en [`RequestLocale`], que tiene en cuenta la petición HTTP, la configuración
|
/// Internamente delega en [`RequestLocale`], que tiene en cuenta la petición HTTP, la configuración
|
||||||
/// global de idioma de la aplicación, la cabecera `Accept-Language` y/o el idioma de respaldo.
|
/// global de idioma de la aplicación, la cabecera `Accept-Language` y/o el idioma de respaldo.
|
||||||
///
|
///
|
||||||
/// Todo ello según la negociación indicada en [`global::SETTINGS.app.lang_negotiation`]. Esto
|
/// Todo ello según la negociación indicada en [`global::SETTINGS.app.lang_negotiation`]. Esto
|
||||||
/// permite que el [`Context`] se use como fuente de idioma coherente en
|
/// permite que el [`Context`] se use como fuente de idioma coherente en [`L10n::lookup()`] o
|
||||||
/// [`L10n::lookup()`](crate::locale::L10n::lookup) o [`L10n::using()`](crate::locale::L10n::using).
|
/// [`L10n::using()`].
|
||||||
|
///
|
||||||
|
/// [`Context`]: crate::core::component::Context
|
||||||
|
/// [`global::SETTINGS.app.lang_negotiation`]: crate::global::App::lang_negotiation
|
||||||
|
/// [`L10n::lookup()`]: crate::locale::L10n::lookup
|
||||||
|
/// [`L10n::using()`]: crate::locale::L10n::using
|
||||||
impl LangId for Context {
|
impl LangId for Context {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn langid(&self) -> &'static LanguageIdentifier {
|
fn langid(&self) -> &'static LanguageIdentifier {
|
||||||
|
|
@ -536,14 +566,14 @@ impl Contextual for Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_theme(mut self, theme: ThemeRef) -> Self {
|
fn with_template(mut self, template: TemplateRef) -> Self {
|
||||||
self.theme = theme;
|
self.template = template;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_template(mut self, template: TemplateRef) -> Self {
|
fn with_theme(mut self, theme: ThemeRef) -> Self {
|
||||||
self.template = template;
|
self.theme = theme;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -619,14 +649,14 @@ impl Contextual for Context {
|
||||||
&self.current_user
|
&self.current_user
|
||||||
}
|
}
|
||||||
|
|
||||||
fn theme(&self) -> ThemeRef {
|
|
||||||
self.theme
|
|
||||||
}
|
|
||||||
|
|
||||||
fn template(&self) -> TemplateRef {
|
fn template(&self) -> TemplateRef {
|
||||||
self.template
|
self.template
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn theme(&self) -> ThemeRef {
|
||||||
|
self.theme
|
||||||
|
}
|
||||||
|
|
||||||
fn param<T: 'static>(&self, key: &'static str) -> Result<&T, ContextError> {
|
fn param<T: 'static>(&self, key: &'static str) -> Result<&T, ContextError> {
|
||||||
let (any, type_name) = self.params.get(key).ok_or(ContextError::ParamNotFound)?;
|
let (any, type_name) = self.params.get(key).ok_or(ContextError::ParamNotFound)?;
|
||||||
any.downcast_ref::<T>()
|
any.downcast_ref::<T>()
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ use std::sync::Arc;
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// # let cx = Context::new(None);
|
/// # let cx = Context::default();
|
||||||
/// # let waypoint = Waypoint::default();
|
/// # let waypoint = Waypoint::default();
|
||||||
/// let route: Route = waypoint.append_to(cx.route("/items")).into();
|
/// let route: Route = waypoint.append_to(cx.route("/items")).into();
|
||||||
/// ```
|
/// ```
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ enum Source {
|
||||||
/// "#.to_string());
|
/// "#.to_string());
|
||||||
///
|
///
|
||||||
/// // Script embebido con handler asíncrono (`async`) que puede usar `await`.
|
/// // Script embebido con handler asíncrono (`async`) que puede usar `await`.
|
||||||
/// let mut cx = Context::new(None).with_param("user_id", 7u32);
|
/// let mut cx = Context::default().with_param("user_id", 7u32);
|
||||||
///
|
///
|
||||||
/// let js = JavaScript::on_load_async("hydrate", |cx| {
|
/// let js = JavaScript::on_load_async("hydrate", |cx| {
|
||||||
/// // Ejemplo: lectura de un parámetro del contexto para inyectarlo en el código.
|
/// // Ejemplo: lectura de un parámetro del contexto para inyectarlo en el código.
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ use crate::base::action;
|
||||||
use crate::base::component::layout;
|
use crate::base::component::layout;
|
||||||
use crate::core::component::{AssetsOp, ChildOp, ComponentRender};
|
use crate::core::component::{AssetsOp, ChildOp, ComponentRender};
|
||||||
use crate::core::component::{Context, ContextError, Contextual};
|
use crate::core::component::{Context, ContextError, Contextual};
|
||||||
use crate::core::theme::{CoreRegion, CoreTemplate, RegionName, RegionRef, TemplateRef, ThemeRef};
|
use crate::core::theme::{CoreRegion, RegionName, RegionRef, TemplateRef, ThemeRef};
|
||||||
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
||||||
use crate::html::{Attr, Props, PropsOp};
|
use crate::html::{Attr, Props, PropsOp};
|
||||||
use crate::html::{DOCTYPE, Markup, html};
|
use crate::html::{DOCTYPE, Markup, html};
|
||||||
|
|
@ -104,7 +104,7 @@ impl Page {
|
||||||
/// usuario actual desde el momento en que se crea la página, sin llamadas adicionales.
|
/// usuario actual desde el momento en que se crea la página, sin llamadas adicionales.
|
||||||
pub fn new(request: HttpRequest) -> Self {
|
pub fn new(request: HttpRequest) -> Self {
|
||||||
Page {
|
Page {
|
||||||
context: Context::new(Some(request)),
|
context: Context::new(request),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -114,9 +114,11 @@ impl Page {
|
||||||
/// Cada tema puede maquetarla de forma distinta capturando
|
/// Cada tema puede maquetarla de forma distinta capturando
|
||||||
/// [`Template`](crate::base::component::layout::Template) en `handle_component()`, pero la
|
/// [`Template`](crate::base::component::layout::Template) en `handle_component()`, pero la
|
||||||
/// plantilla en sí es la misma constante para cualquier tema.
|
/// plantilla en sí es la misma constante para cualquier tema.
|
||||||
|
///
|
||||||
|
/// [`CoreTemplate::Admin`]: crate::core::theme::CoreTemplate::Admin
|
||||||
pub fn admin(request: HttpRequest) -> Self {
|
pub fn admin(request: HttpRequest) -> Self {
|
||||||
Page {
|
Page {
|
||||||
context: Context::new(Some(request)).with_template(&CoreTemplate::Admin),
|
context: Context::admin(request),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -154,22 +156,22 @@ impl Page {
|
||||||
// **< Page GETTERS >***************************************************************************
|
// **< Page GETTERS >***************************************************************************
|
||||||
|
|
||||||
/// Devuelve el título traducido para el idioma de la página, si existe.
|
/// Devuelve el título traducido para el idioma de la página, si existe.
|
||||||
pub fn title(&mut self) -> Option<String> {
|
pub fn title(&self) -> Option<String> {
|
||||||
self.title.lookup(&self.context)
|
self.title.lookup(&self.context)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve la descripción traducida para el idioma de la página, si existe.
|
/// Devuelve la descripción traducida para el idioma de la página, si existe.
|
||||||
pub fn description(&mut self) -> Option<String> {
|
pub fn description(&self) -> Option<String> {
|
||||||
self.description.lookup(&self.context)
|
self.description.lookup(&self.context)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve la lista de metadatos `<meta name=...>`.
|
/// Devuelve la lista de metadatos `<meta name=...>`.
|
||||||
pub fn metadata(&self) -> &Vec<(&str, &str)> {
|
pub fn metadata(&self) -> &Vec<(&'static str, &'static str)> {
|
||||||
&self.metadata
|
&self.metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve la lista de propiedades `<meta property=...>`.
|
/// Devuelve la lista de propiedades `<meta property=...>`.
|
||||||
pub fn properties(&self) -> &Vec<(&str, &str)> {
|
pub fn properties(&self) -> &Vec<(&'static str, &'static str)> {
|
||||||
&self.properties
|
&self.properties
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,14 +283,14 @@ impl Contextual for Page {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_theme(mut self, theme: ThemeRef) -> Self {
|
fn with_template(mut self, template: TemplateRef) -> Self {
|
||||||
self.context.alter_theme(theme);
|
self.context.alter_template(template);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_template(mut self, template: TemplateRef) -> Self {
|
fn with_theme(mut self, theme: ThemeRef) -> Self {
|
||||||
self.context.alter_template(template);
|
self.context.alter_theme(theme);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -332,14 +334,14 @@ impl Contextual for Page {
|
||||||
self.context.current_user()
|
self.context.current_user()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn theme(&self) -> ThemeRef {
|
|
||||||
self.context.theme()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn template(&self) -> TemplateRef {
|
fn template(&self) -> TemplateRef {
|
||||||
self.context.template()
|
self.context.template()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn theme(&self) -> ThemeRef {
|
||||||
|
self.context.theme()
|
||||||
|
}
|
||||||
|
|
||||||
fn param<T: 'static>(&self, key: &'static str) -> Result<&T, ContextError> {
|
fn param<T: 'static>(&self, key: &'static str) -> Result<&T, ContextError> {
|
||||||
self.context.param(key)
|
self.context.param(key)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,22 +16,41 @@ use std::any::Any;
|
||||||
/// Página de error asociada a un código de estado HTTP.
|
/// Página de error asociada a un código de estado HTTP.
|
||||||
///
|
///
|
||||||
/// Este enumerado agrupa tipos esenciales de error que pueden devolverse como página HTML completa.
|
/// Este enumerado agrupa tipos esenciales de error que pueden devolverse como página HTML completa.
|
||||||
/// Cada variante encapsula la solicitud original ([`HttpRequest`]) y se corresponde con un código
|
/// Cada variante encapsula la petición original si está disponible ([`HttpRequest`]), y se asocia a
|
||||||
/// de estado concreto.
|
/// un código de estado concreto.
|
||||||
///
|
///
|
||||||
/// Para cada error se construye una [`Page`] usando el tema activo, lo que permite personalizar la
|
/// Para cada error se construye una [`Page`] usando el tema activo, lo que permite personalizar la
|
||||||
/// plantilla y el contenido del mensaje mediante los métodos específicos del tema (como
|
/// plantilla y el contenido del mensaje con los métodos específicos del tema, como
|
||||||
/// [`Theme::error_403()`](crate::core::theme::Theme::error_403),
|
/// [`Theme::error_403()`], [`Theme::error_404()`] o [`Theme::error_fatal()`].
|
||||||
/// [`Theme::error_404()`](crate::core::theme::Theme::error_404) o
|
///
|
||||||
/// [`Theme::error_fatal()`](crate::core::theme::Theme::error_fatal)).
|
/// Sin `request` (`None`), la página se renderiza igualmente, pero sin el idioma negociado ni el
|
||||||
|
/// usuario actual, que dependen de la petición original.
|
||||||
|
///
|
||||||
|
/// [`Theme::error_403()`]: crate::core::theme::Theme::error_403
|
||||||
|
/// [`Theme::error_404()`]: crate::core::theme::Theme::error_404
|
||||||
|
/// [`Theme::error_fatal()`]: crate::core::theme::Theme::error_fatal
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum ErrorPage {
|
pub enum ErrorPage {
|
||||||
BadRequest(HttpRequest),
|
/// Petición incorrecta (400). El servidor no puede procesar la petición tal y como está
|
||||||
AccessDenied(HttpRequest),
|
/// formulada (datos malformados, parámetros inválidos, etc.).
|
||||||
NotFound(HttpRequest),
|
BadRequest(Option<HttpRequest>),
|
||||||
InternalError(HttpRequest),
|
/// Acceso denegado (403). El usuario actual no tiene permiso para acceder al recurso.
|
||||||
ServiceUnavailable(HttpRequest),
|
///
|
||||||
GatewayTimeout(HttpRequest),
|
/// Se renderiza con [`Theme::error_403()`](crate::core::theme::Theme::error_403).
|
||||||
|
AccessDenied(Option<HttpRequest>),
|
||||||
|
/// Recurso no encontrado (404). La ruta solicitada no existe o no coincide con ningún handler.
|
||||||
|
///
|
||||||
|
/// Se renderiza con [`Theme::error_404()`](crate::core::theme::Theme::error_404).
|
||||||
|
NotFound(Option<HttpRequest>),
|
||||||
|
/// Error interno del servidor (500). Un fallo controlado (no un `panic!`) impide completar la
|
||||||
|
/// petición.
|
||||||
|
InternalError(Option<HttpRequest>),
|
||||||
|
/// Servicio no disponible (503). El servidor no puede atender la petición temporalmente
|
||||||
|
/// (mantenimiento, sobrecarga, etc.).
|
||||||
|
ServiceUnavailable(Option<HttpRequest>),
|
||||||
|
/// Tiempo de espera agotado (504). Una dependencia externa (proxy, servicio remoto) no ha
|
||||||
|
/// respondido a tiempo.
|
||||||
|
GatewayTimeout(Option<HttpRequest>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ErrorPage {
|
impl ErrorPage {
|
||||||
|
|
@ -53,12 +72,12 @@ impl ErrorPage {
|
||||||
let status = self.status_code();
|
let status = self.status_code();
|
||||||
let mut page = match self {
|
let mut page = match self {
|
||||||
Self::AccessDenied(request) => {
|
Self::AccessDenied(request) => {
|
||||||
let mut page = Page::new(request);
|
let mut page = Page::default().with_request(request);
|
||||||
page.theme().error_403(&mut page);
|
page.theme().error_403(&mut page);
|
||||||
page
|
page
|
||||||
}
|
}
|
||||||
Self::NotFound(request) => {
|
Self::NotFound(request) => {
|
||||||
let mut page = Page::new(request);
|
let mut page = Page::default().with_request(request);
|
||||||
page.theme().error_404(&mut page);
|
page.theme().error_404(&mut page);
|
||||||
page
|
page
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +85,7 @@ impl ErrorPage {
|
||||||
| Self::InternalError(request)
|
| Self::InternalError(request)
|
||||||
| Self::ServiceUnavailable(request)
|
| Self::ServiceUnavailable(request)
|
||||||
| Self::GatewayTimeout(request) => {
|
| Self::GatewayTimeout(request) => {
|
||||||
let mut page = Page::new(request);
|
let mut page = Page::default().with_request(request);
|
||||||
page.theme().error_fatal(
|
page.theme().error_fatal(
|
||||||
&mut page,
|
&mut page,
|
||||||
status,
|
status,
|
||||||
|
|
@ -84,6 +103,9 @@ impl ErrorPage {
|
||||||
rendered.into_string(),
|
rendered.into_string(),
|
||||||
)
|
)
|
||||||
.into_response(),
|
.into_response(),
|
||||||
|
// Si renderizar la propia página de error falla, se descarta el `ErrorPage` resultante
|
||||||
|
// en vez de intentar renderizarlo de nuevo, para no arriesgarse a una recursión si el
|
||||||
|
// fallo persiste.
|
||||||
Err(_) => status.into_response(),
|
Err(_) => status.into_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -104,7 +126,7 @@ impl IntoResponse for ErrorPage {
|
||||||
//
|
//
|
||||||
// Se registra como `.fallback()` del router principal desde [`Application`](crate::Application).
|
// Se registra como `.fallback()` del router principal desde [`Application`](crate::Application).
|
||||||
pub(crate) async fn route_not_found(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
pub(crate) async fn route_not_found(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
Err(ErrorPage::NotFound(request))
|
Err(ErrorPage::NotFound(Some(request)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intercepta respuestas con un [`ErrorPage`] pendiente y las convierte en páginas HTML.
|
// Intercepta respuestas con un [`ErrorPage`] pendiente y las convierte en páginas HTML.
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ async fn current_user_propagates_from_request_extensions() {
|
||||||
display_name: "Bob".to_owned(),
|
display_name: "Bob".to_owned(),
|
||||||
})
|
})
|
||||||
.to_http_request();
|
.to_http_request();
|
||||||
let cx = Context::new(Some(req));
|
let cx = Context::new(req);
|
||||||
let user = cx.current_user();
|
let user = cx.current_user();
|
||||||
assert!(user.is_authenticated());
|
assert!(user.is_authenticated());
|
||||||
assert_eq!(user.id(), Some(7));
|
assert_eq!(user.id(), Some(7));
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ async fn component_html_can_access_request_path() {
|
||||||
let req = web::test::TestRequest::get()
|
let req = web::test::TestRequest::get()
|
||||||
.uri("/hello/world")
|
.uri("/hello/world")
|
||||||
.to_http_request();
|
.to_http_request();
|
||||||
let mut cx = Context::new(Some(req));
|
let mut cx = Context::new(req);
|
||||||
|
|
||||||
let mut component = Html::with(|cx| {
|
let mut component = Html::with(|cx| {
|
||||||
let path = cx
|
let path = cx
|
||||||
|
|
|
||||||
|
|
@ -46,10 +46,10 @@ impl Theme for MarkerTheme {
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn default_template_identity_is_independent_of_theme() {
|
async fn default_template_identity_is_independent_of_theme() {
|
||||||
let cx = Context::new(None);
|
let cx = Context::default();
|
||||||
assert_eq!(cx.template().name(), "standard");
|
assert_eq!(cx.template().name(), "standard");
|
||||||
|
|
||||||
let cx = Context::new(None).with_theme(&MarkerTheme);
|
let cx = Context::default().with_theme(&MarkerTheme);
|
||||||
assert_eq!(cx.template().name(), "standard");
|
assert_eq!(cx.template().name(), "standard");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@ async fn admin_template_identity_is_independent_of_theme() {
|
||||||
async fn explicit_template_is_not_overridden_by_a_later_with_theme() {
|
async fn explicit_template_is_not_overridden_by_a_later_with_theme() {
|
||||||
// A template explicitly set with `with_template()` prevails even if `with_theme()` is called
|
// A template explicitly set with `with_template()` prevails even if `with_theme()` is called
|
||||||
// afterwards.
|
// afterwards.
|
||||||
let cx = Context::new(None)
|
let cx = Context::default()
|
||||||
.with_template(&CoreTemplate::Admin)
|
.with_template(&CoreTemplate::Admin)
|
||||||
.with_theme(&pagetop::base::theme::Basic);
|
.with_theme(&pagetop::base::theme::Basic);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use pagetop::prelude::*;
|
||||||
// Forces an effective language different from the default negotiated one (en-US, with no `?lang` in
|
// Forces an effective language different from the default negotiated one (en-US, with no `?lang` in
|
||||||
// the request), so that `Context::route()` decides to propagate `?lang=...` in local routes.
|
// the request), so that `Context::route()` decides to propagate `?lang=...` in local routes.
|
||||||
fn cx_with_lang(lang: &str) -> Context {
|
fn cx_with_lang(lang: &str) -> Context {
|
||||||
Context::new(None).with_langid(&Locale::resolve(lang))
|
Context::default().with_langid(&Locale::resolve(lang))
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< Route - automatic detection of external URLs >***********************************************
|
// **< Route - automatic detection of external URLs >***********************************************
|
||||||
|
|
@ -107,7 +107,7 @@ async fn route_with_captures_dynamic_values_from_the_environment() {
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn route_default_resolves_to_an_empty_path() {
|
async fn route_default_resolves_to_an_empty_path() {
|
||||||
let cx = Context::new(None);
|
let cx = Context::default();
|
||||||
assert_eq!(Route::default().resolve(&cx).to_string(), "");
|
assert_eq!(Route::default().resolve(&cx).to_string(), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue