mod.rs
133 lignes · 4524 octets
//! Template utilities and macros for Rustwarden Core //! //! Provides the `impl_template_response!` macro and `LayoutContext` for Askama templates. pub mod email_templates; use serde::{Deserialize, Serialize}; /// Macro to implement Axum's IntoResponse for Askama templates #[macro_export] macro_rules! impl_template_response { ($($template:ty),+ $(,)?) => { $( impl axum::response::IntoResponse for $template { fn into_response(self) -> axum::response::Response { match self.render() { Ok(html) => axum::response::Html(html).into_response(), Err(e) => { tracing::error!("Template rendering error for {}: {}", stringify!($template), e); ( axum::http::StatusCode::INTERNAL_SERVER_ERROR, format!("Template rendering failed: {}", e), ) .into_response() } } } } )+ }; } /// Base layout context shared by all pages #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LayoutContext { /// HTML `<title>` and heading text for the page. pub page_title: String, /// CSRF token embedded in forms to prevent cross-site request forgery. pub csrf_token: String, /// Application display name (from `APP_NAME` env var). pub app_name: String, /// Application version string from `Cargo.toml`. pub app_version: String, } impl LayoutContext { /// Create a new `LayoutContext` with the given page title and CSRF token. pub fn new(page_title: &str, csrf_token: &str) -> Self { Self { page_title: page_title.to_string(), csrf_token: csrf_token.to_string(), app_name: crate::config::app::default_app_name(), app_version: env!("CARGO_PKG_VERSION").to_string(), } } /// Async constructor that populates `csrf_token` from the tower-sessions session. /// /// Replaces the manual pattern: /// ```rust,ignore /// let csrf_token = get_or_create_csrf_token(&session).await?; /// let ctx = LayoutContext::new("My Page", &csrf_token); /// ``` /// /// Falls back to an empty token (with a warning log) if session access fails, /// so template rendering never panics on a transient session backend error. pub async fn from_session(session: &tower_sessions::Session, page_title: &str) -> Self { let token = match crate::middleware::csrf::get_or_create_csrf_token(session).await { Ok(t) => t, Err(e) => { tracing::warn!( error = %e, "LayoutContext::from_session failed to fetch CSRF token, falling back to empty" ); String::new() } }; Self::new(page_title, &token) } } impl Default for LayoutContext { fn default() -> Self { Self::new("", "") } } #[cfg(test)] mod tests { use super::*; use std::sync::Arc; use tower_sessions::{MemoryStore, Session}; fn make_test_session() -> Session { let store = Arc::new(MemoryStore::default()); Session::new(None, store, None) } #[tokio::test] async fn from_session_generates_csrf_token_when_absent() { let session = make_test_session(); let ctx = LayoutContext::from_session(&session, "Home").await; assert_eq!(ctx.page_title, "Home"); assert_eq!( ctx.csrf_token.len(), 64, "csrf_token should be a 32-byte hex string (64 chars)" ); } #[tokio::test] async fn from_session_returns_existing_csrf_token() { let session = make_test_session(); let first = LayoutContext::from_session(&session, "First").await; let second = LayoutContext::from_session(&session, "Second").await; assert_eq!( first.csrf_token, second.csrf_token, "from_session must reuse the session's existing token" ); assert_eq!(second.page_title, "Second"); } #[tokio::test] async fn from_session_fills_app_metadata() { let session = make_test_session(); let ctx = LayoutContext::from_session(&session, "Any").await; assert!(!ctx.app_name.is_empty()); assert!(!ctx.app_version.is_empty()); } }
GitRust