Skip to main content

haste_health/
client.rs

1use crate::CLIState;
2use haste_fhir_client::http::{FHIRHttpClient, FHIRHttpState};
3use haste_fhir_model::r4::generated::terminology::IssueType;
4use haste_fhir_operation_error::OperationOutcomeError;
5use haste_server::auth_n::oidc::routes::discovery::WellKnownDiscoveryDocument;
6use std::{borrow::Cow, sync::Arc};
7use tokio::sync::Mutex;
8
9async fn config_to_fhir_http_state(
10    state: Arc<Mutex<CLIState>>,
11) -> Result<FHIRHttpState, OperationOutcomeError> {
12    let current_state = state.lock().await;
13    let Some(active_profile) = current_state.config.current_profile().cloned() else {
14        return Err(OperationOutcomeError::error(
15            IssueType::Invalid(None),
16            "No active profile set. Please set an active profile using the config command."
17                .to_string(),
18        ));
19    };
20
21    let state = state.clone();
22    let http_state = FHIRHttpState::new(
23        &active_profile.r4_url.clone(),
24        match active_profile.auth {
25            crate::commands::config::ProfileAuth::Public {} => None,
26            crate::commands::config::ProfileAuth::ClientCredentails {
27                client_id,
28                client_secret,
29            } => {
30                Some(Arc::new(move || {
31                    let state = state.clone();
32                    let client_id = client_id.clone();
33                    let client_secret = client_secret.clone();
34                    Box::pin(async move {
35                        let mut current_state = state.lock().await;
36                        if let Some(token) = current_state.access_token.clone() {
37                            Ok(token)
38                        } else {
39                            let Some(active_profile) = current_state.config.current_profile()
40                            else {
41                                return Err(OperationOutcomeError::error(
42                            IssueType::Invalid(None),
43                            "No active profile set. Please set an active profile using the config command.".to_string(),
44                                ));
45                            };
46
47                            let well_known_document: Cow<WellKnownDiscoveryDocument> =
48                                if let Some(well_known_doc) = &current_state.well_known_document {
49                                    Cow::Borrowed(well_known_doc)
50                                } else {
51                                    let res =
52                                        reqwest::get(&active_profile.oidc_discovery_uri).await;
53                                    let res = res.map_err(|e| {
54                                        OperationOutcomeError::error(
55                                            IssueType::Exception(None),
56                                            format!(
57                                                "Failed to fetch OIDC discovery document: {}",
58                                                e
59                                            ),
60                                        )
61                                    })?;
62
63                                    let well_known_document = serde_json::from_slice::<
64                                        WellKnownDiscoveryDocument,
65                                    >(
66                                        &res.bytes().await.map_err(|e| {
67                                            OperationOutcomeError::error(
68                                                IssueType::Exception(None),
69                                                format!(
70                                                    "Failed to read OIDC discovery document: {}",
71                                                    e
72                                                ),
73                                            )
74                                        })?,
75                                    )
76                                    .map_err(|e| {
77                                        OperationOutcomeError::error(
78                                            IssueType::Exception(None),
79                                            format!(
80                                                "Failed to parse OIDC discovery document: {}",
81                                                e
82                                            ),
83                                        )
84                                    })?;
85
86                                    current_state.well_known_document =
87                                        Some(well_known_document.clone());
88
89                                    Cow::Owned(well_known_document)
90                                };
91
92                            // Post for JWT Token
93                            let params = [
94                                ("grant_type", "client_credentials"),
95                                ("client_id", &client_id),
96                                ("client_secret", &client_secret),
97                                ("scope", "openid system/*.*"),
98                            ];
99
100                            let res: reqwest::Response = reqwest::Client::new()
101                                .post(&well_known_document.token_endpoint)
102                                .form(&params)
103                                .send()
104                                .await
105                                .map_err(|e| {
106                                    OperationOutcomeError::error(
107                                        IssueType::Exception(None),
108                                        format!("Failed to fetch access token: {}", e),
109                                    )
110                                })?;
111
112                            if !res.status().is_success() {
113                                return Err(OperationOutcomeError::error(
114                                    IssueType::Forbidden(None),
115                                    format!(
116                                        "Failed to fetch access token: HTTP '{}'",
117                                        res.status(),
118                                    ),
119                                ));
120                            }
121
122                            let token_response: serde_json::Value =
123                                res.json().await.map_err(|e| {
124                                    OperationOutcomeError::error(
125                                        IssueType::Exception(None),
126                                        format!("Failed to parse access token response: {}", e),
127                                    )
128                                })?;
129
130                            let access_token = token_response
131                                .get("access_token")
132                                .and_then(|v| v.as_str())
133                                .ok_or_else(|| {
134                                    OperationOutcomeError::error(
135                                        IssueType::Exception(None),
136                                        "No access_token field in token response".to_string(),
137                                    )
138                                })?
139                                .to_string();
140
141                            current_state.access_token = Some(access_token.clone());
142
143                            Ok(access_token)
144                        }
145                    })
146                }))
147            }
148        },
149    )?;
150
151    Ok(http_state)
152}
153
154pub async fn fhir_client(
155    state: Arc<Mutex<CLIState>>,
156) -> Result<Arc<FHIRHttpClient<()>>, OperationOutcomeError> {
157    let http_state = config_to_fhir_http_state(state).await?;
158    let fhir_client = Arc::new(FHIRHttpClient::<()>::new(http_state));
159
160    Ok(fhir_client)
161}