cloudillo_core/
extract.rs1use async_trait::async_trait;
7use axum::extract::FromRequestParts;
8use axum::http::request::Parts;
9
10use crate::app::AppState;
11use crate::prelude::*;
12use cloudillo_types::auth_adapter;
13
14pub use cloudillo_types::extract::{IdTag, TnIdResolver};
16
17#[async_trait]
21impl TnIdResolver for AppState {
22 async fn resolve_tn_id(&self, id_tag: &str) -> Result<TnId, Error> {
23 self.auth_adapter.read_tn_id(id_tag).await.map_err(|_| Error::PermissionDenied)
24 }
25}
26
27#[derive(Debug, Clone)]
30pub struct Auth(pub auth_adapter::AuthCtx);
31
32impl<S> FromRequestParts<S> for Auth
33where
34 S: Send + Sync,
35{
36 type Rejection = Error;
37
38 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
39 if let Some(auth) = parts.extensions.get::<Auth>().cloned() {
40 Ok(auth)
41 } else {
42 Err(Error::PermissionDenied)
43 }
44 }
45}
46
47#[derive(Debug, Clone)]
51pub struct OptionalAuth(pub Option<auth_adapter::AuthCtx>);
52
53impl<S> FromRequestParts<S> for OptionalAuth
54where
55 S: Send + Sync,
56{
57 type Rejection = Error;
58
59 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
60 let auth = parts.extensions.get::<Auth>().cloned().map(|a| a.0);
61 Ok(OptionalAuth(auth))
62 }
63}
64
65#[derive(Clone, Debug)]
69pub struct RequestId(pub String);
70
71fn sanitize_external_id(s: &str) -> Option<String> {
76 let s = s.trim();
77 if s.is_empty() || s.len() > 64 {
78 return None;
79 }
80 if !s.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.')) {
81 return None;
82 }
83 Some(s.to_string())
84}
85
86fn random_short() -> String {
90 match cloudillo_types::utils::random_id() {
91 Ok(s) if !s.is_empty() => s.chars().take(8).collect(),
92 _ => {
93 use std::sync::atomic::{AtomicU64, Ordering};
94 static CTR: AtomicU64 = AtomicU64::new(0);
95 let n = CTR.fetch_add(1, Ordering::Relaxed);
96 warn!("random_id() failed; using sequence fallback");
97 format!("seq{n:05}")
98 }
99 }
100}
101
102impl RequestId {
103 pub fn from_headers_or_random(headers: &axum::http::HeaderMap) -> Self {
106 let from_header = headers
107 .get("X-Request-ID")
108 .and_then(|h| h.to_str().ok())
109 .and_then(sanitize_external_id);
110 Self(from_header.unwrap_or_else(random_short))
111 }
112
113 pub fn short(&self) -> &str {
116 let s = self.0.as_str();
117 let end = s.char_indices().nth(4).map_or(s.len(), |(i, _)| i);
118 &s[..end]
119 }
120
121 pub fn install<B>(req: &mut axum::http::Request<B>) -> tracing::Span {
130 if let Some(existing) = req.extensions().get::<RequestId>() {
131 return tracing::span!(tracing::Level::ERROR, "request", id = %existing.short());
132 }
133 let id = Self::from_headers_or_random(req.headers());
134 let span = tracing::span!(tracing::Level::ERROR, "request", id = %id.short());
135 req.extensions_mut().insert(id);
136 span
137 }
138}
139
140#[derive(Clone, Debug)]
142pub struct OptionalRequestId(pub Option<String>);
143
144impl<S> FromRequestParts<S> for OptionalRequestId
145where
146 S: Send + Sync,
147{
148 type Rejection = Error;
149
150 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
151 let req_id = parts.extensions.get::<RequestId>().map(|r| r.0.clone());
152 Ok(OptionalRequestId(req_id))
153 }
154}
155
156