✨ (auth): Añade CurrentUser y sistema de permisos
- 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<T>()`. - `Extension` incluye nuevo método `configure_middleware`. - `try_dispatch_actions` para despachar acciones con control del flujo.
This commit is contained in:
parent
3120fd9a6f
commit
0e96bcce64
22 changed files with 544 additions and 84 deletions
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
170
src/auth.rs
Normal file
170
src/auth.rs
Normal file
|
|
@ -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<i32> {
|
||||
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<ActionBox> {
|
||||
/// 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::<Self>(), 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<Markup, ErrorPage> {
|
||||
/// 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)
|
||||
}
|
||||
|
|
@ -10,19 +10,16 @@ pub struct AfterRender<C: Component> {
|
|||
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<C: Component> ActionDispatcher for AfterRender<C> {
|
||||
/// Devuelve el identificador de tipo ([`UniqueId`]) del componente `C`.
|
||||
fn referer_type_id(&self) -> Option<UniqueId> {
|
||||
self.referer_type_id
|
||||
}
|
||||
|
||||
/// Devuelve el identificador del componente.
|
||||
fn referer_id(&self) -> Option<String> {
|
||||
self.referer_id.clone()
|
||||
}
|
||||
|
||||
/// Devuelve el peso para definir el orden de ejecución.
|
||||
fn weight(&self) -> Weight {
|
||||
self.weight
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,19 +10,16 @@ pub struct BeforeRender<C: Component> {
|
|||
weight: Weight,
|
||||
}
|
||||
|
||||
/// Filtro para despachar [`FnActionWithComponent`] antes de renderizar un componente `C`.
|
||||
// Filtro para despachar `FnActionWithComponent` antes de renderizar un componente `C`.
|
||||
impl<C: Component> ActionDispatcher for BeforeRender<C> {
|
||||
/// Devuelve el identificador de tipo ([`UniqueId`]) del componente `C`.
|
||||
fn referer_type_id(&self) -> Option<UniqueId> {
|
||||
self.referer_type_id
|
||||
}
|
||||
|
||||
/// Devuelve el identificador del componente.
|
||||
fn referer_id(&self) -> Option<String> {
|
||||
self.referer_id.clone()
|
||||
}
|
||||
|
||||
/// Devuelve el peso para definir el orden de ejecución.
|
||||
fn weight(&self) -> Weight {
|
||||
self.weight
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,19 +10,16 @@ pub struct TransformMarkup<C: Component> {
|
|||
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<C: Component> ActionDispatcher for TransformMarkup<C> {
|
||||
/// Devuelve el identificador de tipo ([`UniqueId`]) del componente `C`.
|
||||
fn referer_type_id(&self) -> Option<UniqueId> {
|
||||
self.referer_type_id
|
||||
}
|
||||
|
||||
/// Devuelve el identificador del componente.
|
||||
fn referer_id(&self) -> Option<String> {
|
||||
self.referer_id.clone()
|
||||
}
|
||||
|
||||
/// Devuelve el peso para definir el orden de ejecución.
|
||||
fn weight(&self) -> Weight {
|
||||
self.weight
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::<Self>(), None, None),
|
||||
|
|
|
|||
|
|
@ -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::<Self>(), None, None),
|
||||
|
|
|
|||
|
|
@ -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! >***********************************************************************************
|
||||
|
||||
|
|
|
|||
|
|
@ -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<A, B, F>(key: &ActionKey, f: F)
|
||||
pub fn dispatch_actions<A, F>(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::<Self>(), 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<A, F>(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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<UniqueId> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Identificador del objeto referido. En este caso devuelve `None`.
|
||||
/// Devuelve el identificador del objeto referido.
|
||||
fn referer_id(&self) -> Option<String> {
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,24 +19,35 @@ impl ActionsList {
|
|||
list.sort_by_key(|a| a.weight());
|
||||
}
|
||||
|
||||
pub fn iter_map<A, B, F>(&self, mut f: F)
|
||||
pub fn for_each<A, F>(&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::<A>() {
|
||||
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::<A>() {
|
||||
f(action);
|
||||
} else {
|
||||
trace::error!("Failed to downcast action of type {}", (**a).type_name());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_for_each<A, F>(&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::<A>() {
|
||||
if f(action).is_break() {
|
||||
break;
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
} else {
|
||||
trace::error!("Failed to downcast action of type {}", (**a).type_name());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Markup, ErrorPage> {
|
||||
/// 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<HttpRequest>, // 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>, // Favicon, si se ha definido.
|
||||
stylesheets: Assets<StyleSheet>, // Hojas de estilo CSS.
|
||||
javascripts: Assets<JavaScript>, // Scripts JavaScript.
|
||||
body_props : Props, // Identificador, clases CSS y atributos del <body>.
|
||||
regions : ChildrenInRegions, // Regiones de componentes para renderizar.
|
||||
params : HashMap<&'static str, (Box<dyn Any>, &'static str)>, // Parámetros en ejecución.
|
||||
id_counters: RefCell<HashMap<TypeId, usize>>, // RefCell permite mutar desde build_id(&self).
|
||||
messages : Vec<StatusMessage>, // Mensajes de usuario acumulados.
|
||||
request : Option<HttpRequest>, // 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>, // Favicon, si se ha definido.
|
||||
stylesheets : Assets<StyleSheet>, // Hojas de estilo CSS.
|
||||
javascripts : Assets<JavaScript>, // Scripts JavaScript.
|
||||
body_props : Props, // Identificador, clases CSS y atributos del <body>.
|
||||
regions : ChildrenInRegions, // Regiones de componentes para renderizar.
|
||||
params : HashMap<&'static str, (Box<dyn Any>, &'static str)>, // Parámetros en ejecución.
|
||||
id_counters : RefCell<HashMap<TypeId, usize>>, // RefCell permite mutar desde build_id(&self).
|
||||
messages : Vec<StatusMessage>, // Mensajes de usuario acumulados.
|
||||
}
|
||||
|
||||
impl Default for Context {
|
||||
|
|
@ -312,9 +334,11 @@ impl Context {
|
|||
#[rustfmt::skip]
|
||||
pub fn new(request: Option<HttpRequest>) -> 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::<CurrentUser>())
|
||||
.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<HttpRequest>) -> 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 `<script>`.
|
||||
/// - [`OnLoad`] - Inserta el código JavaScript y lo ejecuta tras el evento `DOMContentLoaded`.
|
||||
/// - [`OnLoadAsync`] - Igual que [`OnLoad`], pero con manejador asíncrono (`async`), útil si dentro
|
||||
/// - [`OnLoadAsync`] - Igual que [`OnLoad`], pero con *handler* asíncrono (`async`), útil si dentro
|
||||
/// del código JavaScript se utiliza `await`.
|
||||
#[derive(AutoDefault)]
|
||||
enum Source {
|
||||
|
|
@ -27,7 +27,7 @@ enum Source {
|
|||
Inline(CowStr, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
|
||||
/// `name`, `closure(&mut Context) -> String` (se ejecuta tras `DOMContentLoaded`).
|
||||
OnLoad(CowStr, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
|
||||
/// `name`, `closure(&mut Context) -> String` (manejador `async` tras `DOMContentLoaded`).
|
||||
/// `name`, `closure(&mut Context) -> String` (*handler* `async` tras `DOMContentLoaded`).
|
||||
OnLoadAsync(CowStr, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ enum Source {
|
|||
/// }
|
||||
/// "#.to_string());
|
||||
///
|
||||
/// // Script embebido con manejador 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 js = JavaScript::on_load_async("hydrate", |cx| {
|
||||
|
|
@ -150,7 +150,7 @@ impl JavaScript {
|
|||
}
|
||||
}
|
||||
|
||||
/// Crea un **script embebido** con un **manejador asíncrono**.
|
||||
/// Crea un **script embebido** con un ***handler* asíncrono**.
|
||||
///
|
||||
/// El código se envuelve en un `addEventListener('DOMContentLoaded',async()=>{...})`, que
|
||||
/// emplea una función `async` para que el cuerpo devuelto por la función *closure* pueda usar
|
||||
|
|
|
|||
|
|
@ -196,6 +196,8 @@ pub mod locale;
|
|||
pub mod datetime;
|
||||
// Tipos y funciones esenciales para crear acciones, componentes, extensiones y temas.
|
||||
pub mod core;
|
||||
// Identidad del usuario y sistema de autorización extensible.
|
||||
pub mod auth;
|
||||
// Respuestas a peticiones web en sus diferentes formatos.
|
||||
pub mod response;
|
||||
// Gestión del servidor y rutas web.
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ pub use crate::core::component::*;
|
|||
pub use crate::core::extension::*;
|
||||
pub use crate::core::theme::*;
|
||||
|
||||
pub use crate::auth::*;
|
||||
|
||||
pub use crate::response::{json::*, page::*, redirect::*};
|
||||
|
||||
pub use crate::base::action;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
mod error;
|
||||
pub use error::ErrorPage;
|
||||
|
||||
use crate::auth::CurrentUser;
|
||||
use crate::base::action;
|
||||
use crate::core::component::{AssetsOp, ChildOp, Context, ContextError, Contextual};
|
||||
use crate::core::theme::{DefaultRegion, Region, RegionRef, TemplateRef, ThemeRef};
|
||||
|
|
@ -95,8 +96,10 @@ pub struct Page {
|
|||
impl Page {
|
||||
/// Crea una nueva instancia de página.
|
||||
///
|
||||
/// La petición HTTP se guardará en el contexto de renderizado de la página para poder ser
|
||||
/// recuperada por los componentes si es necesario.
|
||||
/// La petición HTTP se guarda en el contexto de renderizado, que extrae automáticamente el
|
||||
/// [`CurrentUser`] inyectado por *middleware* en sus extensiones (ver
|
||||
/// [`Context::new`](crate::core::component::Context::new)). Cualquier *handler* tiene acceso al
|
||||
/// usuario actual desde el momento en que se crea la página, sin llamadas adicionales.
|
||||
#[rustfmt::skip]
|
||||
pub fn new(request: HttpRequest) -> Self {
|
||||
Page {
|
||||
|
|
@ -316,6 +319,10 @@ impl Contextual for Page {
|
|||
self.context.request()
|
||||
}
|
||||
|
||||
fn current_user(&self) -> &CurrentUser {
|
||||
self.context.current_user()
|
||||
}
|
||||
|
||||
fn theme(&self) -> ThemeRef {
|
||||
self.context.theme()
|
||||
}
|
||||
|
|
|
|||
80
src/web.rs
80
src/web.rs
|
|
@ -1,22 +1,20 @@
|
|||
//! Servidor web y rutas de la aplicación (basado en [Axum](https://docs.rs/axum)).
|
||||
//!
|
||||
//! Define rutas y manejadores: el [`Router`], las operaciones HTTP ([`get`], [`post`], [`put`],
|
||||
//! Define rutas y *handlers*: el [`Router`], las operaciones HTTP ([`get`], [`post`], [`put`],
|
||||
//! [`delete`], [`patch`]), los extractores ([`Path`], [`Query`]) e [`IntoResponse`], y re-exporta
|
||||
//! el módulo `http` para tipos de bajo nivel como `StatusCode`, `HeaderName` o `Method`. También
|
||||
//! ofrece utilidades para servir archivos estáticos, [`ServeDir`] y [`ServeEmbedded`].
|
||||
//!
|
||||
//! Los *handlers* son las funciones asíncronas que el servidor invoca al recibir una petición
|
||||
//! HTTP en una ruta concreta.
|
||||
|
||||
use crate::StaticFile;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::convert::Infallible;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::extract::FromRequestParts;
|
||||
#[doc(inline)]
|
||||
pub use axum::http;
|
||||
|
||||
// Infraestructura del router.
|
||||
pub use axum::Router;
|
||||
pub use axum::http;
|
||||
|
||||
// Extractores de petición.
|
||||
pub use axum::extract::{Path, Query};
|
||||
|
|
@ -27,24 +25,47 @@ pub use axum::response::{IntoResponse, Response};
|
|||
// Operaciones HTTP para registrar rutas.
|
||||
pub use axum::routing::{delete, get, patch, post, put};
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::extract::FromRequestParts;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::convert::Infallible;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
// **< HttpRequest >********************************************************************************
|
||||
|
||||
/// Representa una petición HTTP.
|
||||
///
|
||||
/// Almacena los datos necesarios para negociar el idioma y renderizar las páginas de error,
|
||||
/// incluyendo la URI completa y las cabeceras de la petición original.
|
||||
/// incluyendo la URI completa, las cabeceras de la petición original y las extensiones de tipo que
|
||||
/// los *middlewares* pueden inyectar antes de que el *handler* se ejecute.
|
||||
///
|
||||
/// Puede declararse directamente como parámetro en un *handler* para pasarlo al
|
||||
/// [`Context`](crate::core::component::Context) de renderizado y a las variantes de
|
||||
/// [`ErrorPage`](crate::response::page::ErrorPage):
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// async fn my_handler(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||
/// // ...
|
||||
/// # todo!()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Las extensiones inyectadas por *middleware* son accesibles vía [`extension`](Self::extension).
|
||||
/// Por ejemplo, desde un *handler*, tras añadir `MyData` en un *middleware*:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// async fn my_handler(request: HttpRequest) -> Result<Markup, ErrorPage> { ... }
|
||||
/// if let Some(data) = request.extension::<MyData>() {
|
||||
/// // ...
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct HttpRequest {
|
||||
uri: http::Uri,
|
||||
headers: http::HeaderMap,
|
||||
extensions: Arc<http::Extensions>,
|
||||
}
|
||||
|
||||
impl HttpRequest {
|
||||
|
|
@ -72,19 +93,35 @@ impl HttpRequest {
|
|||
pub fn headers(&self) -> &http::HeaderMap {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
/// Accede a un valor inyectado por *middleware* en las extensiones de la petición.
|
||||
///
|
||||
/// Devuelve una referencia al tipo `T` si algún *middleware* lo insertó antes de que el
|
||||
/// *handler* recibiera la petición, o `None` en caso contrario.
|
||||
///
|
||||
/// El tipo debe implementar `Send + Sync + 'static` (requisito de [`http::Extensions`]).
|
||||
pub fn extension<T: Send + Sync + 'static>(&self) -> Option<&T> {
|
||||
self.extensions.get::<T>()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Send + Sync> FromRequestParts<S> for HttpRequest {
|
||||
type Rejection = Infallible;
|
||||
|
||||
// Implementa el extractor de Axum para poder declarar `HttpRequest` como parámetro.
|
||||
// Extrae la petición y toma las extensiones que han sido inyectadas por *middleware*. Las
|
||||
// extensiones se mueven a un `Arc` compartido para que `HttpRequest` sea `Clone`.
|
||||
//
|
||||
// Nota: tras este extractor `parts.extensions` queda vacío; otros extractores que dependan de
|
||||
// `Extension<T>` deben registrarse antes en la cadena del *handler*.
|
||||
async fn from_request_parts(
|
||||
parts: &mut http::request::Parts,
|
||||
_state: &S,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let extensions = std::mem::take(&mut parts.extensions);
|
||||
Ok(HttpRequest {
|
||||
uri: parts.uri.clone(),
|
||||
headers: parts.headers.clone(),
|
||||
extensions: Arc::new(extensions),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -104,7 +141,7 @@ pub use tower_http::services::ServeDir;
|
|||
/// devuelve el `index.html` raíz si existe; no busca por subdirectorio.
|
||||
///
|
||||
/// Implementa [`Clone`] para clonar el servicio por petición, pero internamente comparte el mapa de
|
||||
/// recursos con un [`Arc`](std::sync::Arc) para evitar copias innecesarias.
|
||||
/// recursos con un [`Arc`] para evitar copias innecesarias.
|
||||
#[derive(Clone)]
|
||||
pub struct ServeEmbedded {
|
||||
files: std::sync::Arc<HashMap<&'static str, StaticFile>>,
|
||||
|
|
@ -300,6 +337,7 @@ pub mod test {
|
|||
pub struct TestRequest {
|
||||
method: http::Method,
|
||||
uri: String,
|
||||
extensions: http::Extensions,
|
||||
}
|
||||
|
||||
impl TestRequest {
|
||||
|
|
@ -308,6 +346,7 @@ pub mod test {
|
|||
Self {
|
||||
method: http::Method::GET,
|
||||
uri: "/".to_owned(),
|
||||
extensions: http::Extensions::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -316,6 +355,7 @@ pub mod test {
|
|||
Self {
|
||||
method: http::Method::POST,
|
||||
uri: "/".to_owned(),
|
||||
extensions: http::Extensions::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -325,13 +365,24 @@ pub mod test {
|
|||
self
|
||||
}
|
||||
|
||||
/// Inserta un valor en las extensiones de la petición.
|
||||
///
|
||||
/// Útil para simular lo que un *middleware* haría en producción antes de que el *handler*
|
||||
/// reciba la petición. El tipo debe implementar `Clone + Send + Sync + 'static`.
|
||||
pub fn with_extension<T: Clone + Send + Sync + 'static>(mut self, value: T) -> Self {
|
||||
self.extensions.insert(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Construye la petición HTTP de Axum (para enviar al router en tests de integración).
|
||||
pub fn to_request(self) -> http::Request<Body> {
|
||||
http::Request::builder()
|
||||
let mut req = http::Request::builder()
|
||||
.method(self.method)
|
||||
.uri(self.uri)
|
||||
.body(Body::empty())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
*req.extensions_mut() = self.extensions;
|
||||
req
|
||||
}
|
||||
|
||||
/// Construye un [`HttpRequest`](super::HttpRequest) listo para pasarlo a
|
||||
|
|
@ -341,6 +392,7 @@ pub mod test {
|
|||
super::HttpRequest {
|
||||
uri,
|
||||
headers: axum::http::HeaderMap::new(),
|
||||
extensions: std::sync::Arc::new(self.extensions),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
96
tests/auth.rs
Normal file
96
tests/auth.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
// **< CurrentUser >********************************************************************************
|
||||
|
||||
#[pagetop::test]
|
||||
async fn anonymous_reports_itself_correctly() {
|
||||
let user = CurrentUser::Anonymous;
|
||||
assert!(user.is_anonymous());
|
||||
assert!(!user.is_authenticated());
|
||||
assert_eq!(user.id(), None);
|
||||
assert_eq!(user.display_name(), None);
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn authenticated_reports_itself_correctly() {
|
||||
let user = CurrentUser::Authenticated {
|
||||
id: 42,
|
||||
display_name: "Alice".to_owned(),
|
||||
};
|
||||
assert!(!user.is_anonymous());
|
||||
assert!(user.is_authenticated());
|
||||
assert_eq!(user.id(), Some(42));
|
||||
assert_eq!(user.display_name(), Some("Alice"));
|
||||
}
|
||||
|
||||
// **< Context::current_user() >********************************************************************
|
||||
|
||||
#[pagetop::test]
|
||||
async fn current_user_defaults_to_anonymous() {
|
||||
let cx = Context::default();
|
||||
assert!(cx.current_user().is_anonymous());
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn current_user_propagates_from_request_extensions() {
|
||||
let req = web::test::TestRequest::get()
|
||||
.with_extension(CurrentUser::Authenticated {
|
||||
id: 7,
|
||||
display_name: "Bob".to_owned(),
|
||||
})
|
||||
.to_http_request();
|
||||
let cx = Context::new(Some(req));
|
||||
let user = cx.current_user();
|
||||
assert!(user.is_authenticated());
|
||||
assert_eq!(user.id(), Some(7));
|
||||
assert_eq!(user.display_name(), Some("Bob"));
|
||||
}
|
||||
|
||||
// **< HttpRequest::extension() >*******************************************************************
|
||||
|
||||
#[pagetop::test]
|
||||
async fn request_extension_returns_none_for_unknown_type() {
|
||||
let req = web::test::TestRequest::get().to_http_request();
|
||||
assert!(req.extension::<CurrentUser>().is_none());
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn request_extension_returns_injected_value() {
|
||||
let req = web::test::TestRequest::get()
|
||||
.with_extension(CurrentUser::Authenticated {
|
||||
id: 1,
|
||||
display_name: "Carol".to_owned(),
|
||||
})
|
||||
.to_http_request();
|
||||
|
||||
let user = req
|
||||
.extension::<CurrentUser>()
|
||||
.expect("extension should exist");
|
||||
assert!(user.is_authenticated());
|
||||
assert_eq!(user.id(), Some(1));
|
||||
assert_eq!(user.display_name(), Some("Carol"));
|
||||
}
|
||||
|
||||
// **< Page::new() >********************************************************************************
|
||||
|
||||
#[pagetop::test]
|
||||
async fn page_new_propagates_current_user_from_request_extensions() {
|
||||
let req = web::test::TestRequest::get()
|
||||
.with_extension(CurrentUser::Authenticated {
|
||||
id: 5,
|
||||
display_name: "Dave".to_owned(),
|
||||
})
|
||||
.to_http_request();
|
||||
let page = Page::new(req);
|
||||
let user = page.current_user();
|
||||
assert!(user.is_authenticated());
|
||||
assert_eq!(user.id(), Some(5));
|
||||
assert_eq!(user.display_name(), Some("Dave"));
|
||||
}
|
||||
|
||||
#[pagetop::test]
|
||||
async fn page_new_uses_anonymous_when_no_extension() {
|
||||
let req = web::test::TestRequest::get().to_http_request();
|
||||
let page = Page::new(req);
|
||||
assert!(page.current_user().is_anonymous());
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue