forked from
smokesignal.events/smokesignal
The smokesignal.events web application
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}