ayun-server 0.23.0

The RUST Framework for Web Rustceans.
Documentation
use crate::{traits::RequestTrait, Result};

#[cfg(feature = "request-validate")]
pub use validate::Validate;

#[cfg(feature = "request-validate")]
mod validate;

#[async_trait::async_trait]
impl RequestTrait for axum::extract::Request {
    fn content_type(&self) -> mime::Mime {
        if has_content_type(self.headers(), &mime::APPLICATION_WWW_FORM_URLENCODED) {
            return mime::APPLICATION_WWW_FORM_URLENCODED;
        }

        if has_content_type(self.headers(), &mime::APPLICATION_JSON) {
            return mime::APPLICATION_JSON;
        }

        if has_content_type(self.headers(), &mime::MULTIPART_FORM_DATA) {
            return mime::MULTIPART_FORM_DATA;
        }

        mime::TEXT_PLAIN
    }

    fn header<K>(&self, key: K) -> Option<&str>
    where
        K: axum::http::header::AsHeaderName,
    {
        self.headers()
            .get(key)
            .and_then(|value| value.to_str().ok())
    }

    fn user_agent(&self) -> &str {
        self.headers()
            .get(axum::http::header::USER_AGENT)
            .map_or("", |h| h.to_str().unwrap_or(""))
    }

    async fn path<T: serde::de::DeserializeOwned + Send + 'static>(
        &mut self,
    ) -> Result<T, axum::extract::rejection::PathRejection> {
        use axum::{extract::Path, RequestExt};

        let Path(path) = self.extract_parts::<Path<T>>().await?;

        Ok(path)
    }

    #[cfg(feature = "request-query")]
    async fn query<T: serde::de::DeserializeOwned + 'static>(
        &mut self,
    ) -> Result<T, axum::extract::rejection::QueryRejection> {
        use axum::{extract::Query, RequestExt};

        let Query(query) = self.extract_parts::<Query<T>>().await?;

        Ok(query)
    }

    #[cfg(feature = "request-form")]
    async fn form<T: serde::de::DeserializeOwned>(
        self,
    ) -> Result<T, axum::extract::rejection::FormRejection> {
        use axum::{extract::FromRequest, Form};
        let Form(payload) = Form::<T>::from_request(self, &()).await?;

        Ok(payload)
    }

    #[cfg(feature = "request-json")]
    async fn json<T: serde::de::DeserializeOwned>(
        self,
    ) -> Result<T, axum::extract::rejection::JsonRejection> {
        use axum::{extract::FromRequest, Json};
        let Json(payload) = Json::<T>::from_request(self, &()).await?;

        Ok(payload)
    }

    #[cfg(feature = "request-multipart")]
    async fn multipart<T: serde::de::DeserializeOwned>(self) -> Result<T, crate::Error> {
        use axum::extract::{FromRequest, Multipart};
        let mut multipart = Multipart::from_request(self, &()).await?;

        let mut data = serde_json::Map::new();

        while let Some(field) = multipart.next_field().await? {
            if let Some(name) = field.name() {
                if let Some(file_name) = field.file_name() {
                    data.insert(name.to_owned(), file_name.into());
                } else {
                    data.insert(name.to_owned(), field.text().await?.into());
                }
            }
        }

        let payload = serde_json::from_value::<T>(data.into())?;

        Ok(payload)
    }

    #[cfg(feature = "request-multipart")]
    async fn file(self, key: &str) -> Result<Option<axum::body::Bytes>, crate::Error> {
        use axum::extract::{FromRequest, Multipart};
        let mut multipart = Multipart::from_request(self, &()).await?;

        while let Some(field) = multipart.next_field().await? {
            if let Some(name) = field.name() {
                if field.file_name().is_some() && name == key {
                    return Ok(Some(field.bytes().await?));
                }
            }
        }

        Ok(None)
    }

    #[cfg(feature = "request-validate")]
    async fn validate<T: serde::de::DeserializeOwned + validator::Validate>(
        self,
    ) -> Result<T, crate::Error> {
        let payload: T = if self.content_type() == mime::APPLICATION_JSON {
            self.json::<T>().await?
        } else if self.content_type() == mime::APPLICATION_WWW_FORM_URLENCODED {
            self.form::<T>().await?
        } else if self.content_type() == mime::MULTIPART_FORM_DATA {
            self.multipart::<T>().await?
        } else {
            return Err(crate::Error::UnsupportedMediaType);
        };

        payload.validate()?;

        Ok(payload)
    }

    #[cfg(feature = "request-id")]
    fn id(&self) -> Option<&str> {
        self.header(crate::config::X_REQUEST_ID)
    }

    #[cfg(feature = "request-auth")]
    fn user(&self) -> Option<ayun_auth::jwt::UserClaims> {
        self.extensions()
            .get::<ayun_auth::jwt::UserClaims>()
            .cloned()
    }

    #[cfg(feature = "request-auth")]
    fn token(&self) -> std::result::Result<String, crate::Error> {
        let config = ayun_config::config::<ayun_auth::config::Auth>("auth").expect(
            "failed to
    get jwt config",
        );

        match config
            .jwt
            .location
            .as_ref()
            .unwrap_or(&ayun_auth::config::JwtLocation::Bearer)
        {
            ayun_auth::config::JwtLocation::Query { name } => {
                Ok(extract_token_from_query(name, self.uri())?)
            }
            ayun_auth::config::JwtLocation::Cookie { name } => Ok(extract_token_from_cookie(
                name,
                &axum_extra::extract::cookie::CookieJar::from_headers(self.headers()),
            )?),
            ayun_auth::config::JwtLocation::Bearer => Ok(extract_token_from_header(self.headers())
                .map_err(|e| ayun_core::Error::Unauthorized(e.to_string()))?),
        }
    }
}

fn has_content_type(
    headers: &axum::http::header::HeaderMap,
    expected_content_type: &mime::Mime,
) -> bool {
    let content_type = if let Some(content_type) = headers.get(axum::http::header::CONTENT_TYPE) {
        content_type
    } else {
        return false;
    };

    let content_type = if let Ok(content_type) = content_type.to_str() {
        content_type
    } else {
        return false;
    };

    content_type.starts_with(expected_content_type.as_ref())
}

#[cfg(feature = "request-auth")]
fn extract_token_from_header(
    headers: &axum::http::header::HeaderMap,
) -> std::result::Result<String, crate::Error> {
    use axum::http::header::AUTHORIZATION;

    Ok(headers
        .get(AUTHORIZATION)
        .ok_or(ayun_core::Error::Unauthorized(format!(
            "header {} token not found",
            AUTHORIZATION
        )))?
        .to_str()
        .map_err(|err| ayun_core::Error::Unauthorized(err.to_string()))?
        .strip_prefix("Bearer ")
        .ok_or(ayun_core::Error::Unauthorized(format!(
            "error strip {} value",
            AUTHORIZATION
        )))?
        .to_string())
}

#[cfg(feature = "request-auth")]
fn extract_token_from_cookie(
    name: &str,
    jar: &axum_extra::extract::cookie::CookieJar,
) -> std::result::Result<String, ayun_core::Error> {
    Ok(jar
        .get(name)
        .ok_or(ayun_core::Error::Unauthorized(
            "token is not found".to_string(),
        ))?
        .to_string()
        .strip_prefix(&format!("{name}="))
        .ok_or(ayun_core::Error::Unauthorized(
            "error strip value".to_string(),
        ))?
        .to_string())
}

#[cfg(feature = "request-auth")]
fn extract_token_from_query(
    name: &str,
    uri: &axum::http::Uri,
) -> std::result::Result<String, ayun_core::Error> {
    use axum::extract::Query;
    use std::collections::HashMap;

    let parameters: Query<HashMap<String, String>> =
        Query::try_from_uri(uri).map_err(|err| ayun_core::Error::Unauthorized(err.to_string()))?;

    parameters
        .get(name)
        .cloned()
        .ok_or(ayun_core::Error::Unauthorized(format!(
            "`{name}` query parameter not found"
        )))
}