dfns_sdk_rs/utils/
user_action_fetch.rs1use 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 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 let base = Url::parse(base_url)
62 .map_err(|e| DfnsError::new(400, format!("Invalid base URL: {}", e), None))?;
63
64 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}