The smokesignal.events web application
at main 288 lines 8.4 kB view raw
1use atproto_identity::resolve::IdentityResolver; 2use atproto_identity::traits::{DidDocumentStorage, KeyResolver}; 3use atproto_oauth::storage::OAuthRequestStorage; 4use atproto_oauth_axum::state::OAuthClientConfig; 5use axum::extract::FromRef; 6use axum::{ 7 extract::FromRequestParts, 8 http::request::Parts, 9 response::{IntoResponse, Response}, 10}; 11use axum_extra::extract::Cached; 12use axum_template::engine::Engine; 13use cookie::Key; 14use minijinja::context as template_context; 15use std::convert::Infallible; 16use std::{ops::Deref, sync::Arc}; 17use unic_langid::LanguageIdentifier; 18 19#[cfg(all(feature = "reload", not(feature = "embed")))] 20use minijinja_autoreload::AutoReloader; 21 22#[cfg(all(feature = "reload", not(feature = "embed")))] 23pub type AppEngine = Engine<AutoReloader>; 24 25#[cfg(feature = "embed")] 26use minijinja::Environment; 27 28#[cfg(feature = "embed")] 29pub type AppEngine = Engine<Environment<'static>>; 30 31use crate::emailer::Emailer; 32use crate::service::{ServiceDID, ServiceDocument, ServiceKey}; 33use crate::storage::content::ContentStorage; 34use crate::{ 35 config::Config, 36 http::middleware_auth::Auth, 37 http::middleware_i18n::Language, 38 i18n::Locales, 39 storage::identity_profile::model::IdentityProfile, 40 storage::{CachePool, StoragePool}, 41}; 42 43pub(crate) struct I18nContext { 44 pub(crate) supported_languages: Vec<LanguageIdentifier>, 45 pub(crate) locales: Locales, 46} 47 48pub struct InnerWebContext { 49 pub engine: AppEngine, 50 pub http_client: reqwest::Client, 51 pub pool: StoragePool, 52 pub cache_pool: CachePool, 53 pub config: Config, 54 pub(crate) i18n_context: I18nContext, 55 pub(crate) oauth_client_config: atproto_oauth_axum::state::OAuthClientConfig, 56 pub(crate) identity_resolver: Arc<dyn IdentityResolver>, 57 pub(crate) key_provider: Arc<dyn KeyResolver>, 58 pub(crate) oauth_storage: Arc<dyn OAuthRequestStorage>, 59 pub(crate) document_storage: Arc<dyn DidDocumentStorage>, 60 pub(crate) content_storage: Arc<dyn ContentStorage>, 61 pub(crate) emailer: Option<Arc<dyn Emailer>>, 62 pub(crate) service_did: ServiceDID, 63 pub(crate) service_document: ServiceDocument, 64 pub(crate) service_key: ServiceKey, 65} 66 67#[derive(Clone, FromRef)] 68pub struct WebContext(pub Arc<InnerWebContext>); 69 70impl Deref for WebContext { 71 type Target = InnerWebContext; 72 73 fn deref(&self) -> &Self::Target { 74 &self.0 75 } 76} 77 78impl WebContext { 79 #[allow(clippy::too_many_arguments)] 80 pub fn new( 81 pool: StoragePool, 82 cache_pool: CachePool, 83 engine: AppEngine, 84 http_client: &reqwest::Client, 85 config: Config, 86 oauth_client_config: OAuthClientConfig, 87 identity_resolver: Arc<dyn IdentityResolver>, 88 key_provider: Arc<dyn KeyResolver>, 89 oauth_storage: Arc<dyn OAuthRequestStorage>, 90 document_storage: Arc<dyn DidDocumentStorage>, 91 supported_languages: Vec<LanguageIdentifier>, 92 locales: Locales, 93 content_storage: Arc<dyn ContentStorage>, 94 emailer: Option<Arc<dyn Emailer>>, 95 service_did: ServiceDID, 96 service_document: ServiceDocument, 97 service_key: ServiceKey, 98 ) -> Self { 99 Self(Arc::new(InnerWebContext { 100 pool, 101 cache_pool, 102 engine, 103 http_client: http_client.clone(), 104 config, 105 i18n_context: I18nContext { 106 supported_languages, 107 locales, 108 }, 109 oauth_client_config, 110 identity_resolver, 111 key_provider, 112 oauth_storage, 113 document_storage, 114 content_storage, 115 emailer, 116 service_did, 117 service_document, 118 service_key, 119 })) 120 } 121} 122 123impl FromRef<WebContext> for Key { 124 fn from_ref(context: &WebContext) -> Self { 125 context.0.config.http_cookie_key.as_ref().clone() 126 } 127} 128 129impl FromRef<WebContext> for Arc<dyn KeyResolver> { 130 fn from_ref(context: &WebContext) -> Self { 131 context.0.key_provider.clone() 132 } 133} 134 135impl FromRef<WebContext> for OAuthClientConfig { 136 fn from_ref(context: &WebContext) -> Self { 137 context.0.oauth_client_config.clone() 138 } 139} 140 141impl FromRef<WebContext> for Arc<dyn DidDocumentStorage> { 142 fn from_ref(context: &WebContext) -> Self { 143 context.0.document_storage.clone() 144 } 145} 146 147impl FromRef<WebContext> for Arc<dyn IdentityResolver> { 148 fn from_ref(context: &WebContext) -> Self { 149 context.0.identity_resolver.clone() 150 } 151} 152 153impl FromRef<WebContext> for ServiceDocument { 154 fn from_ref(context: &WebContext) -> Self { 155 context.0.service_document.clone() 156 } 157} 158 159impl FromRef<WebContext> for ServiceDID { 160 fn from_ref(context: &WebContext) -> Self { 161 context.0.service_did.clone() 162 } 163} 164 165impl FromRef<WebContext> for ServiceKey { 166 fn from_ref(context: &WebContext) -> Self { 167 context.0.service_key.clone() 168 } 169} 170 171impl<S> FromRequestParts<S> for ServiceDocument 172where 173 ServiceDocument: FromRef<S>, 174 S: Send + Sync, 175{ 176 type Rejection = Infallible; 177 178 async fn from_request_parts(_parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> { 179 let service_document = ServiceDocument::from_ref(state); 180 Ok(service_document) 181 } 182} 183 184impl<S> FromRequestParts<S> for ServiceDID 185where 186 ServiceDID: FromRef<S>, 187 S: Send + Sync, 188{ 189 type Rejection = Infallible; 190 191 async fn from_request_parts(_parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> { 192 let service_did = ServiceDID::from_ref(state); 193 Ok(service_did) 194 } 195} 196 197impl<S> FromRequestParts<S> for ServiceKey 198where 199 ServiceKey: FromRef<S>, 200 S: Send + Sync, 201{ 202 type Rejection = Infallible; 203 204 async fn from_request_parts(_parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> { 205 let service_key = ServiceKey::from_ref(state); 206 Ok(service_key) 207 } 208} 209 210// New structs for reducing handler function arguments 211 212/// A context struct specifically for admin handlers 213pub(crate) struct AdminRequestContext { 214 pub(crate) web_context: WebContext, 215 pub(crate) language: Language, 216 pub(crate) admin_handle: IdentityProfile, 217} 218 219impl<S> FromRequestParts<S> for AdminRequestContext 220where 221 S: Send + Sync, 222 WebContext: FromRef<S>, 223{ 224 type Rejection = Response; 225 226 async fn from_request_parts(parts: &mut Parts, context: &S) -> Result<Self, Self::Rejection> { 227 // Extract the needed components 228 let web_context = WebContext::from_ref(context); 229 let language = Language::from_request_parts(parts, context).await?; 230 let cached_auth = Cached::<Auth>::from_request_parts(parts, context).await?; 231 232 // Validate user is an admin 233 let admin_handle = match cached_auth.0.require_admin(&web_context.config) { 234 Ok(handle) => handle, 235 Err(err) => return Err(err.into_response()), 236 }; 237 238 Ok(Self { 239 web_context, 240 language, 241 admin_handle, 242 }) 243 } 244} 245 246/// Helper function to create standard template context for admin views 247pub(crate) fn admin_template_context( 248 ctx: &AdminRequestContext, 249 canonical_url: &str, 250 active_section: &str, 251) -> minijinja::value::Value { 252 template_context! { 253 language => ctx.language.to_string(), 254 current_handle => ctx.admin_handle.clone(), 255 canonical_url => canonical_url, 256 active_section => active_section, 257 } 258} 259 260/// A context struct for regular authenticated user handlers 261pub(crate) struct UserRequestContext { 262 pub(crate) web_context: WebContext, 263 pub(crate) language: Language, 264 pub(crate) current_handle: Option<IdentityProfile>, 265 pub(crate) auth: Auth, 266} 267 268impl<S> FromRequestParts<S> for UserRequestContext 269where 270 S: Send + Sync, 271 WebContext: FromRef<S>, 272{ 273 type Rejection = Response; 274 275 async fn from_request_parts(parts: &mut Parts, context: &S) -> Result<Self, Self::Rejection> { 276 // Extract the needed components 277 let web_context = WebContext::from_ref(context); 278 let language = Language::from_request_parts(parts, context).await?; 279 let cached_auth = Cached::<Auth>::from_request_parts(parts, context).await?; 280 281 Ok(Self { 282 web_context, 283 language, 284 current_handle: cached_auth.0.profile().cloned(), 285 auth: cached_auth.0, 286 }) 287 } 288}