dfns_sdk_rs/utils/
user_action_fetch.rs

1// @dfns-sdk-rs/src/utils/user_action_fetch.rs
2
3use crate::client::base_auth_api::{
4    BaseAuthApi, CreateUserActionChallengeRequest, SignUserActionChallengeRequest,
5};
6use crate::error::DfnsError;
7use crate::models::generic::{DfnsApiClientOptions, DfnsBaseApiOptions};
8use crate::utils::fetch::{DfnsFetch, Fetch, FetchOptions, HttpMethod};
9use reqwest::Response;
10use url::Url;
11
12#[derive(Debug, Clone, PartialEq)]
13pub struct UserActionFetch {
14    inner: DfnsFetch,
15}
16
17impl UserActionFetch {
18    pub fn new() -> Self {
19        Self {
20            inner: DfnsFetch::new(),
21        }
22    }
23}
24
25impl Fetch for UserActionFetch {
26    async fn execute(
27        &self,
28        resource: &str,
29        options: FetchOptions<DfnsBaseApiOptions>,
30    ) -> Result<Response, DfnsError> {
31        if options.method != HttpMethod::GET {
32            return Err(DfnsError::new(
33                400,
34                "A 'signer' needs to be passed to Dfns client.",
35                Some(serde_json::json!({
36                    "detail": "Most non-readonly endpoints require 'User Action Signing' flow. During that flow, the credential 'signer' that you passed will handle signing the user action challenge, using your credential."
37                })),
38            ));
39        }
40
41        self.inner.execute(resource, options).await
42    }
43}
44
45pub async fn user_action_fetch<T>(
46    resource: &str,
47    options: FetchOptions<DfnsApiClientOptions>,
48) -> Result<T, DfnsError>
49where
50    T: serde::de::DeserializeOwned,
51{
52    // First check for base URL
53    let base_url = options
54        .api_options
55        .base
56        .base_url
57        .as_deref()
58        .ok_or_else(|| DfnsError::new(400, "Base URL is required in options", None))?;
59
60    // Validate base URL
61    let base = Url::parse(base_url)
62        .map_err(|e| DfnsError::new(400, format!("Invalid base URL: {}", e), None))?;
63
64    // Try to join with resource path, but handle invalid resource paths
65    if resource.contains("://") {
66        return Err(DfnsError::new(
67            400,
68            "Invalid resource path: must be a relative path",
69            None,
70        ));
71    }
72
73    let url = base
74        .join(resource)
75        .map_err(|e| DfnsError::new(400, format!("Invalid resource path: {}", e), None))?;
76
77    let fetch = UserActionFetch::new();
78
79    if options.method != HttpMethod::GET {
80        let api_options = options.api_options;
81        let signer = api_options.signer.ok_or_else(|| DfnsError::new(
82            400,
83            "A 'signer' needs to be passed to Dfns client.",
84            Some(serde_json::json!({
85                "detail": "Most non-readonly endpoints require 'User Action Signing' flow. During that flow, the credential 'signer' that you passed will handle signing the user action challenge, using your credential."
86            }))
87        ))?;
88
89        let challenge = BaseAuthApi::create_user_action_challenge(
90            CreateUserActionChallengeRequest {
91                user_action_payload: options
92                    .body
93                    .clone()
94                    .map(|v| v.to_string())
95                    .unwrap_or_default(),
96                user_action_http_method: options.method.clone(),
97                user_action_http_path: url.path().to_string(),
98                user_action_server_kind: "Api".to_string(),
99            },
100            api_options.base.clone(),
101        )
102        .await?;
103
104        let challenge_id = challenge.challenge_identifier.clone();
105        let assertion = signer.sign(challenge).await?;
106        let user_action_response = BaseAuthApi::sign_user_action_challenge(
107            SignUserActionChallengeRequest {
108                challenge_identifier: challenge_id,
109                first_factor: assertion,
110                second_factor: None,
111            },
112            api_options.base.clone(),
113        )
114        .await?;
115
116        let mut base_options = FetchOptions {
117            method: options.method,
118            headers: options.headers,
119            body: options.body,
120            api_options: api_options.base,
121        };
122
123        let mut headers = base_options.headers.unwrap_or_default();
124        headers.insert(
125            "x-dfns-useraction".to_string(),
126            user_action_response.user_action,
127        );
128        base_options.headers = Some(headers);
129
130        let response = fetch.execute(url.as_str(), base_options).await?;
131        let status = response.status().as_u16();
132        response
133            .json::<T>()
134            .await
135            .map_err(|e| DfnsError::new(status, format!("Failed to decode response: {}", e), None))
136    } else {
137        let base_options = FetchOptions {
138            method: options.method,
139            headers: options.headers,
140            body: options.body,
141            api_options: options.api_options.base,
142        };
143        let response = fetch.execute(url.as_str(), base_options).await?;
144        let status = response.status().as_u16();
145        response
146            .json::<T>()
147            .await
148            .map_err(|e| DfnsError::new(status, format!("Failed to decode response: {}", e), None))
149    }
150}