Skip to main content

docbox_http/middleware/
action_user.rs

1//! Extractor for getting the user details from the headers set by the API
2
3use crate::error::{DynHttpError, HttpCommonError, HttpError};
4use axum::{
5    extract::FromRequestParts,
6    http::{StatusCode, request::Parts},
7};
8use docbox_core::database::{DbExecutor, models::user::User};
9use thiserror::Error;
10use utoipa::IntoParams;
11
12pub struct ActionUser(pub Option<ActionUserData>);
13
14impl ActionUser {
15    /// Stores the current user details providing back the user ID to use
16    pub async fn store_user(self, db: impl DbExecutor<'_>) -> Result<Option<User>, DynHttpError> {
17        let user_data = match self.0 {
18            Some(value) => value,
19            None => return Ok(None),
20        };
21
22        let user = match User::store(db, user_data.id, user_data.name, user_data.image_id).await {
23            Ok(value) => value,
24            Err(error) => {
25                tracing::error!(?error, "failed to store user");
26                return Err(HttpCommonError::ServerError.into());
27            }
28        };
29
30        Ok(Some(user))
31    }
32}
33
34pub struct ActionUserData {
35    pub id: String,
36    pub name: Option<String>,
37    pub image_id: Option<String>,
38}
39
40pub const USER_ID_HEADER: &str = "x-user-id";
41pub const USER_NAME_HEADER: &str = "x-user-name";
42pub const USER_IMAGE_ID_HEADER: &str = "x-user-image-id";
43
44/// OpenAPI param for optional the user identifying headers
45#[derive(IntoParams)]
46#[into_params(parameter_in = Header)]
47#[allow(unused)]
48pub struct UserParams {
49    /// Optional ID of the user if performed on behalf of a user
50    #[param(rename = "x-user-id")]
51    pub user_id: Option<String>,
52    /// Optional name of the user if performed on behalf of a user
53    #[param(rename = "x-user-name")]
54    pub user_name: Option<String>,
55    /// Optional image ID of the user if performed on behalf of a user
56    #[param(rename = "x-user-image-id")]
57    pub user_image_id: Option<String>,
58}
59
60#[derive(Debug, Error)]
61#[error("user id was not a valid utf8 string")]
62struct InvalidUserId;
63
64impl HttpError for InvalidUserId {
65    fn status(&self) -> axum::http::StatusCode {
66        StatusCode::BAD_REQUEST
67    }
68}
69
70impl<S> FromRequestParts<S> for ActionUser
71where
72    S: Send + Sync,
73{
74    type Rejection = DynHttpError;
75
76    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
77        let id = match parts.headers.get(USER_ID_HEADER) {
78            Some(value) => {
79                let value_str = value.to_str().map_err(|_| InvalidUserId)?;
80                value_str.to_string()
81            }
82
83            // Not acting on behalf of a user
84            None => return Ok(ActionUser(None)),
85        };
86
87        let name = parts
88            .headers
89            .get(USER_NAME_HEADER)
90            .and_then(|value| value.to_str().ok())
91            .map(|value| value.to_string());
92
93        let image_id = parts
94            .headers
95            .get(USER_IMAGE_ID_HEADER)
96            .and_then(|value| value.to_str().ok())
97            .map(|value| value.to_string());
98
99        Ok(ActionUser(Some(ActionUserData { id, name, image_id })))
100    }
101}