ayun_server/request/
mod.rs1use crate::traits::RequestTrait;
2
3#[cfg(feature = "request-validate")]
4pub use validate::Validate;
5
6#[cfg(feature = "request-validate")]
7mod validate;
8
9#[async_trait::async_trait]
10impl RequestTrait for axum::extract::Request {
11 fn content_type(&self) -> mime::Mime {
12 if has_content_type(self.headers(), &mime::APPLICATION_WWW_FORM_URLENCODED) {
13 return mime::APPLICATION_WWW_FORM_URLENCODED;
14 }
15
16 if has_content_type(self.headers(), &mime::APPLICATION_JSON) {
17 return mime::APPLICATION_JSON;
18 }
19
20 if has_content_type(self.headers(), &mime::MULTIPART_FORM_DATA) {
21 return mime::MULTIPART_FORM_DATA;
22 }
23
24 mime::TEXT_PLAIN
25 }
26
27 fn header<K>(&self, key: K) -> Option<&str>
28 where
29 K: axum::http::header::AsHeaderName,
30 {
31 self.headers()
32 .get(key)
33 .and_then(|value| value.to_str().ok())
34 }
35
36 fn user_agent(&self) -> &str {
37 self.headers()
38 .get(axum::http::header::USER_AGENT)
39 .map_or("", |h| h.to_str().unwrap_or(""))
40 }
41
42 async fn path<T: serde::de::DeserializeOwned + Send + 'static>(
43 &mut self,
44 ) -> Result<T, axum::extract::rejection::PathRejection> {
45 use axum::{extract::Path, RequestExt};
46
47 let Path(path) = self.extract_parts::<Path<T>>().await?;
48
49 Ok(path)
50 }
51
52 #[cfg(feature = "request-query")]
53 async fn query<T: serde::de::DeserializeOwned + 'static>(
54 &mut self,
55 ) -> crate::ServerResult<T, axum::extract::rejection::QueryRejection> {
56 use axum::{extract::Query, RequestExt};
57
58 let Query(query) = self.extract_parts::<Query<T>>().await?;
59
60 Ok(query)
61 }
62
63 #[cfg(feature = "request-form")]
64 async fn form<T: serde::de::DeserializeOwned>(
65 self,
66 ) -> crate::ServerResult<T, axum::extract::rejection::FormRejection> {
67 use axum::{extract::FromRequest, Form};
68 let Form(payload) = Form::<T>::from_request(self, &()).await?;
69
70 Ok(payload)
71 }
72
73 #[cfg(feature = "request-json")]
74 async fn json<T: serde::de::DeserializeOwned>(
75 self,
76 ) -> crate::ServerResult<T, axum::extract::rejection::JsonRejection> {
77 use axum::{extract::FromRequest, Json};
78 let Json(payload) = Json::<T>::from_request(self, &()).await?;
79
80 Ok(payload)
81 }
82
83 #[cfg(feature = "request-multipart")]
84 async fn multipart<T: serde::de::DeserializeOwned>(self) -> crate::ServerResult<T> {
85 use axum::extract::{FromRequest, Multipart};
86 let mut multipart = Multipart::from_request(self, &()).await?;
87
88 let mut data = serde_json::Map::new();
89
90 while let Some(field) = multipart.next_field().await? {
91 if let Some(name) = field.name() {
92 if let Some(file_name) = field.file_name() {
93 data.insert(name.to_owned(), file_name.into());
94 } else {
95 data.insert(name.to_owned(), field.text().await?.into());
96 }
97 }
98 }
99
100 let payload = serde_json::from_value::<T>(data.into())?;
101
102 Ok(payload)
103 }
104
105 #[cfg(feature = "request-multipart")]
106 async fn file(self, key: &str) -> crate::ServerResult<Option<axum::body::Bytes>> {
107 use axum::extract::{FromRequest, Multipart};
108 let mut multipart = Multipart::from_request(self, &()).await?;
109
110 while let Some(field) = multipart.next_field().await? {
111 if let Some(name) = field.name() {
112 if field.file_name().is_some() && name == key {
113 return Ok(Some(field.bytes().await?));
114 }
115 }
116 }
117
118 Ok(None)
119 }
120
121 #[cfg(feature = "request-validate")]
122 async fn validate<T: serde::de::DeserializeOwned + validator::Validate>(
123 self,
124 ) -> crate::ServerResult<T> {
125 let payload: T = if self.content_type() == mime::APPLICATION_JSON {
126 self.json::<T>().await?
127 } else if self.content_type() == mime::APPLICATION_WWW_FORM_URLENCODED {
128 self.form::<T>().await?
129 } else if self.content_type() == mime::MULTIPART_FORM_DATA {
130 self.multipart::<T>().await?
131 } else {
132 return Err(crate::error::Error::UnsupportedMediaType);
133 };
134
135 payload.validate()?;
136
137 Ok(payload)
138 }
139
140 #[cfg(feature = "request-id")]
141 fn id(&self) -> Option<&str> {
142 self.header(crate::config::X_REQUEST_ID)
143 }
144
145 #[cfg(feature = "request-auth")]
146 fn user(&self) -> Option<ayun_auth::support::UserClaims> {
147 self.extensions()
148 .get::<ayun_auth::support::UserClaims>()
149 .cloned()
150 }
151
152 #[cfg(feature = "request-auth")]
153 fn token(&self) -> crate::ServerResult<String> {
154 let config = ayun_config::support::config::<ayun_auth::config::Auth>("auth").expect(
155 "failed to
156 get jwt config",
157 );
158
159 match config
160 .jwt
161 .location
162 .as_ref()
163 .unwrap_or(&ayun_auth::config::JwtLocation::Bearer)
164 {
165 ayun_auth::config::JwtLocation::Query { name } => {
166 Ok(extract_token_from_query(name, self.uri())?)
167 }
168 ayun_auth::config::JwtLocation::Cookie { name } => Ok(extract_token_from_cookie(
169 name,
170 &axum_extra::extract::cookie::CookieJar::from_headers(self.headers()),
171 )?),
172 ayun_auth::config::JwtLocation::Bearer => Ok(extract_token_from_header(self.headers())
173 .map_err(|e| ayun_auth::error::Error::Unauthorized(e.to_string()))?),
174 }
175 }
176}
177
178fn has_content_type(
179 headers: &axum::http::header::HeaderMap,
180 expected_content_type: &mime::Mime,
181) -> bool {
182 let content_type = if let Some(content_type) = headers.get(axum::http::header::CONTENT_TYPE) {
183 content_type
184 } else {
185 return false;
186 };
187
188 let content_type = if let Ok(content_type) = content_type.to_str() {
189 content_type
190 } else {
191 return false;
192 };
193
194 content_type.starts_with(expected_content_type.as_ref())
195}
196
197#[cfg(feature = "request-auth")]
198fn extract_token_from_header(
199 headers: &axum::http::header::HeaderMap,
200) -> crate::ServerResult<String> {
201 use axum::http::header::AUTHORIZATION;
202 use ayun_auth::error::Error;
203
204 Ok(headers
205 .get(AUTHORIZATION)
206 .ok_or(Error::Unauthorized(format!(
207 "header {} token not found",
208 AUTHORIZATION
209 )))?
210 .to_str()
211 .map_err(|err| Error::Unauthorized(err.to_string()))?
212 .strip_prefix("Bearer ")
213 .ok_or(Error::Unauthorized(format!(
214 "error strip {} value",
215 AUTHORIZATION
216 )))?
217 .to_string())
218}
219
220#[cfg(feature = "request-auth")]
221fn extract_token_from_cookie(
222 name: &str,
223 jar: &axum_extra::extract::cookie::CookieJar,
224) -> crate::ServerResult<String> {
225 use ayun_auth::error::Error;
226
227 Ok(jar
228 .get(name)
229 .ok_or(Error::Unauthorized("token is not found".to_string()))?
230 .to_string()
231 .strip_prefix(&format!("{name}="))
232 .ok_or(Error::Unauthorized("error strip value".to_string()))?
233 .to_string())
234}
235
236#[cfg(feature = "request-auth")]
237fn extract_token_from_query(name: &str, uri: &axum::http::Uri) -> crate::ServerResult<String> {
238 use axum::extract::Query;
239 use ayun_auth::error::Error;
240 use std::collections::HashMap;
241
242 let parameters: Query<HashMap<String, String>> =
243 Query::try_from_uri(uri).map_err(|err| Error::Unauthorized(err.to_string()))?;
244
245 Ok(parameters
246 .get(name)
247 .cloned()
248 .ok_or(Error::Unauthorized(format!(
249 "`{name}` query parameter not found"
250 )))?)
251}