use atproto_identity::resolve::IdentityResolver; use atproto_identity::traits::{DidDocumentStorage, KeyResolver}; use atproto_oauth::storage::OAuthRequestStorage; use atproto_oauth_axum::state::OAuthClientConfig; 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 minijinja::context as template_context; use std::convert::Infallible; use std::{ops::Deref, sync::Arc}; use unic_langid::LanguageIdentifier; #[cfg(all(feature = "reload", not(feature = "embed")))] use minijinja_autoreload::AutoReloader; #[cfg(all(feature = "reload", not(feature = "embed")))] pub type AppEngine = Engine; #[cfg(feature = "embed")] use minijinja::Environment; #[cfg(feature = "embed")] pub type AppEngine = Engine>; use crate::emailer::Emailer; use crate::service::{ServiceDID, ServiceDocument, ServiceKey}; use crate::storage::content::ContentStorage; use crate::{ config::Config, http::middleware_auth::Auth, http::middleware_i18n::Language, i18n::Locales, storage::identity_profile::model::IdentityProfile, storage::{CachePool, StoragePool}, }; pub(crate) struct I18nContext { pub(crate) supported_languages: Vec, pub(crate) locales: Locales, } pub struct InnerWebContext { pub engine: AppEngine, pub http_client: reqwest::Client, pub pool: StoragePool, pub cache_pool: CachePool, pub config: Config, pub(crate) i18n_context: I18nContext, pub(crate) oauth_client_config: atproto_oauth_axum::state::OAuthClientConfig, pub(crate) identity_resolver: Arc, pub(crate) key_provider: Arc, pub(crate) oauth_storage: Arc, pub(crate) document_storage: Arc, pub(crate) content_storage: Arc, pub(crate) emailer: Option>, pub(crate) service_did: ServiceDID, pub(crate) service_document: ServiceDocument, pub(crate) service_key: ServiceKey, } #[derive(Clone, FromRef)] pub struct WebContext(pub Arc); impl Deref for WebContext { type Target = InnerWebContext; fn deref(&self) -> &Self::Target { &self.0 } } impl WebContext { #[allow(clippy::too_many_arguments)] pub fn new( pool: StoragePool, cache_pool: CachePool, engine: AppEngine, http_client: &reqwest::Client, config: Config, oauth_client_config: OAuthClientConfig, identity_resolver: Arc, key_provider: Arc, oauth_storage: Arc, document_storage: Arc, supported_languages: Vec, locales: Locales, content_storage: Arc, emailer: Option>, service_did: ServiceDID, service_document: ServiceDocument, service_key: ServiceKey, ) -> Self { Self(Arc::new(InnerWebContext { pool, cache_pool, engine, http_client: http_client.clone(), config, i18n_context: I18nContext { supported_languages, locales, }, oauth_client_config, identity_resolver, key_provider, oauth_storage, document_storage, content_storage, emailer, service_did, service_document, service_key, })) } } impl FromRef for Key { fn from_ref(context: &WebContext) -> Self { context.0.config.http_cookie_key.as_ref().clone() } } impl FromRef for Arc { fn from_ref(context: &WebContext) -> Self { context.0.key_provider.clone() } } impl FromRef for OAuthClientConfig { fn from_ref(context: &WebContext) -> Self { context.0.oauth_client_config.clone() } } impl FromRef for Arc { fn from_ref(context: &WebContext) -> Self { context.0.document_storage.clone() } } impl FromRef for Arc { fn from_ref(context: &WebContext) -> Self { context.0.identity_resolver.clone() } } impl FromRef for ServiceDocument { fn from_ref(context: &WebContext) -> Self { context.0.service_document.clone() } } impl FromRef for ServiceDID { fn from_ref(context: &WebContext) -> Self { context.0.service_did.clone() } } impl FromRef for ServiceKey { fn from_ref(context: &WebContext) -> Self { context.0.service_key.clone() } } impl FromRequestParts for ServiceDocument where ServiceDocument: FromRef, S: Send + Sync, { type Rejection = Infallible; async fn from_request_parts(_parts: &mut Parts, state: &S) -> Result { let service_document = ServiceDocument::from_ref(state); Ok(service_document) } } impl FromRequestParts for ServiceDID where ServiceDID: FromRef, S: Send + Sync, { type Rejection = Infallible; async fn from_request_parts(_parts: &mut Parts, state: &S) -> Result { let service_did = ServiceDID::from_ref(state); Ok(service_did) } } impl FromRequestParts for ServiceKey where ServiceKey: FromRef, S: Send + Sync, { type Rejection = Infallible; async fn from_request_parts(_parts: &mut Parts, state: &S) -> Result { let service_key = ServiceKey::from_ref(state); Ok(service_key) } } // New structs for reducing handler function arguments /// A context struct specifically for admin handlers pub(crate) struct AdminRequestContext { pub(crate) web_context: WebContext, pub(crate) language: Language, pub(crate) admin_handle: IdentityProfile, } 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, }) } } /// Helper function to create standard template context for admin views pub(crate) fn admin_template_context( ctx: &AdminRequestContext, canonical_url: &str, active_section: &str, ) -> minijinja::value::Value { template_context! { language => ctx.language.to_string(), current_handle => ctx.admin_handle.clone(), canonical_url => canonical_url, active_section => active_section, } } /// A context struct for regular authenticated user handlers pub(crate) struct UserRequestContext { pub(crate) web_context: WebContext, pub(crate) language: Language, pub(crate) current_handle: Option, pub(crate) 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.profile().cloned(), auth: cached_auth.0, }) } }