forked from
smokesignal.events/quickdid
QuickDID is a high-performance AT Protocol identity resolution service written in Rust. It provides handle-to-DID resolution with Redis-backed caching and queue processing.
1use crate::handle_resolver::HandleResolver;
2use crate::metrics::SharedMetricsPublisher;
3use crate::queue::{HandleResolutionWork, QueueAdapter};
4use axum::{
5 Router,
6 extract::{MatchedPath, State},
7 http::Request,
8 middleware::{self, Next},
9 response::{Json, Response},
10 routing::get,
11};
12use serde_json::json;
13use std::sync::Arc;
14use std::time::Instant;
15use tower_http::services::ServeDir;
16
17pub(crate) struct InnerAppContext {
18 pub(crate) handle_resolver: Arc<dyn HandleResolver>,
19 pub(crate) handle_queue: Arc<dyn QueueAdapter<HandleResolutionWork>>,
20 pub(crate) metrics: SharedMetricsPublisher,
21 pub(crate) etag_seed: String,
22 pub(crate) cache_control_header: Option<String>,
23 pub(crate) static_files_dir: String,
24}
25
26#[derive(Clone)]
27pub struct AppContext(pub(crate) Arc<InnerAppContext>);
28
29impl AppContext {
30 /// Create a new AppContext with the provided configuration.
31 pub fn new(
32 handle_resolver: Arc<dyn HandleResolver>,
33 handle_queue: Arc<dyn QueueAdapter<HandleResolutionWork>>,
34 metrics: SharedMetricsPublisher,
35 etag_seed: String,
36 cache_control_header: Option<String>,
37 static_files_dir: String,
38 ) -> Self {
39 Self(Arc::new(InnerAppContext {
40 handle_resolver,
41 handle_queue,
42 metrics,
43 etag_seed,
44 cache_control_header,
45 static_files_dir,
46 }))
47 }
48
49 // Internal accessor methods for handlers
50 pub(super) fn etag_seed(&self) -> &str {
51 &self.0.etag_seed
52 }
53
54 pub(super) fn cache_control_header(&self) -> Option<&str> {
55 self.0.cache_control_header.as_deref()
56 }
57
58 pub(super) fn static_files_dir(&self) -> &str {
59 &self.0.static_files_dir
60 }
61}
62
63use axum::extract::FromRef;
64
65macro_rules! impl_from_ref {
66 ($context:ty, $field:ident, $type:ty) => {
67 impl FromRef<$context> for $type {
68 fn from_ref(context: &$context) -> Self {
69 context.0.$field.clone()
70 }
71 }
72 };
73}
74
75impl_from_ref!(AppContext, handle_resolver, Arc<dyn HandleResolver>);
76impl_from_ref!(
77 AppContext,
78 handle_queue,
79 Arc<dyn QueueAdapter<HandleResolutionWork>>
80);
81impl_from_ref!(AppContext, metrics, SharedMetricsPublisher);
82
83/// Middleware to track HTTP request metrics
84async fn metrics_middleware(
85 State(metrics): State<SharedMetricsPublisher>,
86 matched_path: Option<MatchedPath>,
87 request: Request<axum::body::Body>,
88 next: Next,
89) -> Response {
90 let start = Instant::now();
91 let method = request.method().to_string();
92 let path = matched_path
93 .as_ref()
94 .map(|p| p.as_str().to_string())
95 .unwrap_or_else(|| "unknown".to_string());
96
97 // Process the request
98 let response = next.run(request).await;
99
100 // Calculate duration
101 let duration_ms = start.elapsed().as_millis() as u64;
102 let status_code = response.status().as_u16().to_string();
103
104 // Publish metrics with tags
105 metrics
106 .time_with_tags(
107 "http.request.duration_ms",
108 duration_ms,
109 &[
110 ("method", &method),
111 ("path", &path),
112 ("status", &status_code),
113 ],
114 )
115 .await;
116
117 response
118}
119
120pub fn create_router(app_context: AppContext) -> Router {
121 let static_dir = app_context.static_files_dir().to_string();
122
123 Router::new()
124 .route("/xrpc/_health", get(handle_xrpc_health))
125 .route(
126 "/xrpc/com.atproto.identity.resolveHandle",
127 get(super::handle_xrpc_resolve_handle::handle_xrpc_resolve_handle)
128 .options(super::handle_xrpc_resolve_handle::handle_xrpc_resolve_handle_options),
129 )
130 .fallback_service(ServeDir::new(static_dir))
131 .layer(middleware::from_fn_with_state(
132 app_context.0.metrics.clone(),
133 metrics_middleware,
134 ))
135 .with_state(app_context)
136}
137
138pub(super) async fn handle_xrpc_health() -> Json<serde_json::Value> {
139 Json(json!({
140 "version": "0.1.0",
141 }))
142}