use axum::extract::FromRef; use axum::{ extract::FromRequestParts, http::request::Parts, response::{IntoResponse, Response}, }; use axum_extra::extract::Cached; use axum_template::engine::Engine; use cookie::Key; use hickory_resolver::TokioAsyncResolver; use minijinja::context as template_context; use std::{ops::Deref, sync::Arc}; use unic_langid::LanguageIdentifier; #[cfg(feature = "reload")] use minijinja_autoreload::AutoReloader; #[cfg(feature = "reload")] pub type AppEngine = Engine; #[cfg(feature = "embed")] use minijinja::Environment; use crate::{ config::Config, http::middleware_auth::Auth, http::middleware_i18n::Language, i18n::{get_supported_languages, get_translation, Locales}, storage::handle::model::Handle, storage::{CachePool, StoragePool}, }; #[cfg(feature = "embed")] pub type AppEngine = Engine>; pub struct I18nContext { pub supported_languages: Vec, pub locales: Locales, } impl I18nContext { pub fn new() -> Self { let supported_languages = get_supported_languages(); let locales = Locales::new(supported_languages.clone()); Self { supported_languages, locales, } } /// Get a translation for the given locale and key pub fn get_translation(&self, locale: &LanguageIdentifier, key: &str) -> String { get_translation(locale, key, None) } /// Get a translation with arguments pub fn get_translation_with_args( &self, locale: &LanguageIdentifier, key: &str, args: std::collections::HashMap, fluent::FluentValue<'static>>, ) -> String { // Convert the HashMap to the expected format let converted_args: std::collections::HashMap = args .into_iter() .map(|(k, v)| { let converted_value = match v { fluent::FluentValue::String(s) => fluent_templates::fluent_bundle::FluentValue::String(s), fluent::FluentValue::Number(n) => fluent_templates::fluent_bundle::FluentValue::Number(n), fluent::FluentValue::None => fluent_templates::fluent_bundle::FluentValue::String("".into()), fluent::FluentValue::Error => fluent_templates::fluent_bundle::FluentValue::String("".into()), _ => fluent_templates::fluent_bundle::FluentValue::String("".into()), // Handle any other variants }; (k.to_string(), converted_value) }) .collect(); get_translation(locale, key, Some(converted_args)) } /// Check if a language is supported pub fn supports_language(&self, locale: &LanguageIdentifier) -> bool { self.supported_languages.contains(locale) } } pub struct InnerWebContext { pub engine: AppEngine, pub http_client: reqwest::Client, pub pool: StoragePool, pub cache_pool: CachePool, pub config: Config, pub i18n_context: I18nContext, pub dns_resolver: hickory_resolver::TokioAsyncResolver, } #[derive(Clone, FromRef)] pub struct WebContext(pub Arc); impl Deref for WebContext { type Target = InnerWebContext; fn deref(&self) -> &Self::Target { &self.0 } } impl WebContext { pub fn new( pool: StoragePool, cache_pool: CachePool, engine: AppEngine, http_client: &reqwest::Client, config: Config, i18n_context: I18nContext, dns_resolver: TokioAsyncResolver, ) -> Self { Self(Arc::new(InnerWebContext { pool, cache_pool, engine, http_client: http_client.clone(), config, i18n_context, dns_resolver, })) } } impl FromRef for Key { fn from_ref(context: &WebContext) -> Self { context.0.config.http_cookie_key.as_ref().clone() } } // New structs for reducing handler function arguments /// A context struct specifically for admin handlers pub struct AdminRequestContext { pub web_context: WebContext, pub language: Language, pub admin_handle: Handle, pub auth: Auth, } impl FromRequestParts for AdminRequestContext where S: Send + Sync, WebContext: FromRef, { type Rejection = Response; async fn from_request_parts(parts: &mut Parts, context: &S) -> Result { // Extract the needed components let web_context = WebContext::from_ref(context); let language = Language::from_request_parts(parts, context).await?; let cached_auth = Cached::::from_request_parts(parts, context).await?; // Validate user is an admin let admin_handle = match cached_auth.0.require_admin(&web_context.config) { Ok(handle) => handle, Err(err) => return Err(err.into_response()), }; Ok(Self { web_context, language, admin_handle, auth: cached_auth.0, }) } } /// Helper function to create standard template context for admin views pub fn admin_template_context( ctx: &AdminRequestContext, canonical_url: &str, ) -> minijinja::value::Value { let supported_languages: Vec = ctx.web_context.i18n_context.supported_languages .iter() .map(|lang| lang.to_string()) .collect(); template_context! { language => ctx.language.to_string(), locale => ctx.language.to_string(), current_handle => ctx.admin_handle.clone(), canonical_url => canonical_url, supported_languages => supported_languages, } } /// A context struct for regular authenticated user handlers pub struct UserRequestContext { pub web_context: WebContext, pub language: Language, pub current_handle: Option, pub auth: Auth, } impl FromRequestParts for UserRequestContext where S: Send + Sync, WebContext: FromRef, { type Rejection = Response; async fn from_request_parts(parts: &mut Parts, context: &S) -> Result { // Extract the needed components let web_context = WebContext::from_ref(context); let language = Language::from_request_parts(parts, context).await?; let cached_auth = Cached::::from_request_parts(parts, context).await?; Ok(Self { web_context, language, current_handle: cached_auth.0 .0.clone(), auth: cached_auth.0, }) } } /// Helper function to create standard template context for user views pub fn user_template_context( ctx: &UserRequestContext, canonical_url: &str, ) -> minijinja::value::Value { let supported_languages: Vec = ctx.web_context.i18n_context.supported_languages .iter() .map(|lang| lang.to_string()) .collect(); template_context! { language => ctx.language.to_string(), locale => ctx.language.to_string(), current_handle => ctx.current_handle.clone(), canonical_url => canonical_url, supported_languages => supported_languages, } } /// Helper function to create basic template context with just locale information pub fn basic_template_context( language: &Language, supported_languages: &[LanguageIdentifier], ) -> minijinja::value::Value { let supported_languages: Vec = supported_languages .iter() .map(|lang| lang.to_string()) .collect(); template_context! { language => language.to_string(), locale => language.to_string(), supported_languages => supported_languages, } }