Skip to main content

modo/auth/apikey/
extractor.rs

1use 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}