modo/auth/apikey/
extractor.rs1use axum::extract::{FromRequestParts, OptionalFromRequestParts};
2use http::request::Parts;
3
4use crate::error::Error;
5
6use super::types::ApiKeyMeta;
7
8impl<S: Send + Sync> FromRequestParts<S> for ApiKeyMeta {
9 type Rejection = Error;
10
11 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
12 parts
13 .extensions
14 .get::<ApiKeyMeta>()
15 .cloned()
16 .ok_or_else(|| Error::unauthorized("missing API key"))
17 }
18}
19
20impl<S: Send + Sync> OptionalFromRequestParts<S> for ApiKeyMeta {
21 type Rejection = Error;
22
23 async fn from_request_parts(
24 parts: &mut Parts,
25 _state: &S,
26 ) -> Result<Option<Self>, Self::Rejection> {
27 Ok(parts.extensions.get::<ApiKeyMeta>().cloned())
28 }
29}
30
31#[cfg(test)]
32mod tests {
33 use super::*;
34
35 #[tokio::test]
36 async fn extract_from_extensions() {
37 let (mut parts, _) = http::Request::builder().body(()).unwrap().into_parts();
38 parts.extensions.insert(ApiKeyMeta {
39 id: "test".into(),
40 tenant_id: "t1".into(),
41 name: "key".into(),
42 scopes: vec!["read".into()],
43 expires_at: None,
44 last_used_at: None,
45 created_at: "2026-01-01T00:00:00.000Z".into(),
46 });
47
48 let result =
49 <ApiKeyMeta as FromRequestParts<()>>::from_request_parts(&mut parts, &()).await;
50 assert!(result.is_ok());
51 assert_eq!(result.unwrap().id, "test");
52 }
53
54 #[tokio::test]
55 async fn extract_missing_returns_unauthorized() {
56 let (mut parts, _) = http::Request::builder().body(()).unwrap().into_parts();
57
58 let result =
59 <ApiKeyMeta as FromRequestParts<()>>::from_request_parts(&mut parts, &()).await;
60 assert!(result.is_err());
61 let err = result.unwrap_err();
62 assert_eq!(err.status(), http::StatusCode::UNAUTHORIZED);
63 }
64
65 #[tokio::test]
66 async fn optional_none_when_missing() {
67 let (mut parts, _) = http::Request::builder().body(()).unwrap().into_parts();
68
69 let result =
70 <ApiKeyMeta as OptionalFromRequestParts<()>>::from_request_parts(&mut parts, &()).await;
71 assert!(result.is_ok());
72 assert!(result.unwrap().is_none());
73 }
74
75 #[tokio::test]
76 async fn optional_some_when_present() {
77 let (mut parts, _) = http::Request::builder().body(()).unwrap().into_parts();
78 parts.extensions.insert(ApiKeyMeta {
79 id: "test".into(),
80 tenant_id: "t1".into(),
81 name: "key".into(),
82 scopes: vec![],
83 expires_at: None,
84 last_used_at: None,
85 created_at: "2026-01-01T00:00:00.000Z".into(),
86 });
87
88 let result =
89 <ApiKeyMeta as OptionalFromRequestParts<()>>::from_request_parts(&mut parts, &()).await;
90 assert!(result.is_ok());
91 assert!(result.unwrap().is_some());
92 }
93}