posemesh_domain_http/
discovery.rs

1use std::{collections::HashMap, sync::Arc, time::Duration};
2
3use futures::lock::Mutex;
4use reqwest::Client;
5use serde::{Deserialize, Serialize};
6
7#[cfg(not(target_family = "wasm"))]
8use tokio::spawn;
9#[cfg(target_family = "wasm")]
10use wasm_bindgen_futures::spawn_local as spawn;
11
12use posemesh_utils::now_unix_secs;
13#[cfg(target_family = "wasm")]
14use posemesh_utils::sleep;
15#[cfg(not(target_family = "wasm"))]
16use tokio::time::sleep;
17
18use crate::auth::{AuthClient, TokenCache, get_cached_or_fresh_token, parse_jwt};
19use crate::errors::{AukiErrorResponse, AuthError, DomainError};
20use crate::VERSION;
21pub const ALL_DOMAINS_ORG: &str = "all";
22pub const OWN_DOMAINS_ORG: &str = "own";
23
24#[derive(Debug, Deserialize, Clone, Serialize)]
25pub struct DomainServer {
26    pub id: String,
27    pub organization_id: String,
28    pub name: String,
29    pub url: String,
30}
31
32#[derive(Debug, Deserialize, Clone)]
33pub struct DomainWithToken {
34    #[serde(flatten)]
35    pub domain: DomainWithServer,
36    #[serde(skip)]
37    pub expires_at: u64,
38    access_token: String,
39}
40
41impl TokenCache for DomainWithToken {
42    fn get_access_token(&self) -> String {
43        self.access_token.clone()
44    }
45
46    fn get_expires_at(&self) -> u64 {
47        self.expires_at
48    }
49}
50
51#[derive(Debug, Deserialize, Clone, Serialize)]
52pub struct DomainWithServer {
53    pub id: String,
54    pub name: String,
55    pub organization_id: String,
56    pub domain_server_id: String,
57    pub redirect_url: Option<String>,
58    pub domain_server: DomainServer,
59}
60
61#[derive(Debug, Clone)]
62pub struct DiscoveryService {
63    dds_url: String,
64    client: Client,
65    cache: Arc<Mutex<HashMap<String, DomainWithToken>>>,
66    api_client: AuthClient,
67    oidc_access_token: Option<String>,
68}
69
70#[derive(Debug, Deserialize)]
71pub struct ListDomainsResponse {
72    pub domains: Vec<DomainWithServer>,
73}
74
75impl DiscoveryService {
76    pub fn new(api_url: &str, dds_url: &str, client_id: &str) -> Self {
77        let api_client = AuthClient::new(api_url, client_id);
78
79        Self {
80            dds_url: dds_url.to_string(),
81            client: Client::new(),
82            cache: Arc::new(Mutex::new(HashMap::new())),
83            api_client,
84            oidc_access_token: None,
85        }
86    }
87
88    /// List domains with domain server without issue token
89    pub async fn list_domains(
90        &self,
91        org: &str,
92    ) -> Result<ListDomainsResponse, DomainError> {
93        let access_token = self
94            .api_client
95            .get_dds_access_token(self.oidc_access_token.as_deref())
96            .await
97            .map_err(|_| DomainError::AuthError(AuthError::Unauthorized("failed to get dds access token")))?;
98        let response = self
99            .client
100            .get(format!(
101                "{}/api/v1/domains?org={}&with=domain_server",
102                self.dds_url, org
103            ))
104            .bearer_auth(access_token)
105            .header("Content-Type", "application/json")
106            .header("posemesh-client-id", self.api_client.client_id.clone())
107            .send()
108            .await?;
109
110        if response.status().is_success() {
111            let domain_servers: ListDomainsResponse = response.json().await?;
112            Ok(domain_servers)
113        } else {
114            let status = response.status();
115            let text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
116            Err(AukiErrorResponse { status, error: format!("Failed to list domains. {} - {}", text, org) }.into())
117        }
118    }
119
120    pub async fn sign_in_with_auki_account(
121        &mut self,
122        email: &str,
123        password: &str,
124        remember_password: bool,
125    ) -> Result<(), DomainError> {
126        self.cache.lock().await.clear();
127        self.oidc_access_token = None;
128        self.api_client
129            .user_login(email, password)
130            .await
131            .map_err(|_| DomainError::AuthError(AuthError::Unauthorized("failed to login")))?;
132        if remember_password {
133            let mut api_client = self.api_client.clone();
134            let email = email.to_string();
135            let password = password.to_string();
136            spawn(async move {
137                loop {
138                    let expires_at = api_client
139                        .get_expires_at()
140                        .await
141                        .inspect_err(|e| tracing::error!("Failed to get expires at: {}", e));
142                    if let Ok(expires_at) = expires_at {
143                        let expiration = {
144                            let now = now_unix_secs();
145                            let duration = expires_at - now;
146                            if duration > 600 {
147                                Some(Duration::from_secs(duration))
148                            } else {
149                                None
150                            }
151                        };
152
153                        if let Some(expiration) = expiration {
154                            tracing::info!("Refreshing token in {} seconds", expiration.as_secs());
155                            sleep(expiration).await;
156                        }
157
158                        let _ = api_client
159                            .user_login(&email, &password)
160                            .await
161                            .inspect_err(|e| tracing::error!("Failed to login: {}", e));
162                    }
163                }
164            });
165        }
166        Ok(())
167    }
168
169    pub async fn sign_in_as_auki_app(
170        &mut self,
171        app_key: &str,
172        app_secret: &str,
173    ) -> Result<(), DomainError> {
174        self.cache.lock().await.clear();
175        self.oidc_access_token = None;
176        let _ = self
177            .api_client
178            .sign_in_with_app_credentials(app_key, app_secret)
179            .await
180            .map_err(|_| DomainError::AuthError(AuthError::Unauthorized("failed to sign in with app credentials")))?;
181        Ok(())
182    }
183
184    pub fn with_oidc_access_token(&self, oidc_access_token: &str) -> Self {
185        if let Some(cached_oidc_access_token) = self.oidc_access_token.as_deref()
186            && cached_oidc_access_token == oidc_access_token
187        {
188            return self.clone();
189        }
190        Self {
191            dds_url: self.dds_url.clone(),
192            client: self.client.clone(),
193            cache: Arc::new(Mutex::new(HashMap::new())),
194            api_client: AuthClient::new(&self.api_client.api_url, &self.api_client.client_id),
195            oidc_access_token: Some(oidc_access_token.to_string()),
196        }
197    }
198
199    pub async fn auth_domain(
200        &self,
201        domain_id: &str,
202    ) -> Result<DomainWithToken, DomainError> {
203        let access_token = self
204            .api_client
205            .get_dds_access_token(self.oidc_access_token.as_deref())
206            .await
207            .map_err(|_| DomainError::AuthError(AuthError::Unauthorized("failed to get dds access token")))?;
208        // Check cache first
209        let cache = if let Some(cached_domain) = self.cache.lock().await.get(domain_id) {
210            cached_domain.clone()
211        } else {
212            DomainWithToken {
213                domain: DomainWithServer {
214                    id: domain_id.to_string(),
215                    name: "".to_string(),
216                    organization_id: "".to_string(),
217                    domain_server_id: "".to_string(),
218                    redirect_url: None,
219                    domain_server: DomainServer {
220                        id: "".to_string(),
221                        organization_id: "".to_string(),
222                        name: "".to_string(),
223                        url: "".to_string(),
224                    },
225                },
226                expires_at: 0,
227                access_token: "".to_string(),
228            }
229        };
230
231        let cached = get_cached_or_fresh_token(&cache, || {
232            let client = self.client.clone();
233            let dds_url = self.dds_url.clone();
234            let client_id = self.api_client.client_id.clone();
235            async move {
236                let response = client
237                    .post(format!("{}/api/v1/domains/{}/auth", dds_url, domain_id))
238                    .bearer_auth(access_token)
239                    .header("Content-Type", "application/json")
240                    .header("posemesh-client-id", client_id)
241                    .send()
242                    .await?;
243
244                if response.status().is_success() {
245                    let mut domain_with_token: DomainWithToken = response.json().await?;
246                    domain_with_token.expires_at =
247                        parse_jwt(&domain_with_token.get_access_token())
248                            .map_err(|_| DomainError::AuthError(AuthError::Unauthorized("invalid domain token")))?
249                            .exp;
250                    Ok(domain_with_token)
251                } else {
252                    let status = response.status();
253                    let text = response
254                        .text()
255                        .await
256                        .unwrap_or_else(|_| "Unknown error".to_string());
257                    Err(AukiErrorResponse { status, error: format!("Failed to auth domain. {}", text) }.into())
258                }
259            }
260        })
261        .await
262        .map_err(|_| DomainError::AuthError(AuthError::Unauthorized("failed to auth domain")))?;
263
264        // Cache the result
265        let mut cache = self.cache.lock().await;
266        cache.insert(domain_id.to_string(), cached.clone());
267        Ok(cached)
268    }
269    pub async fn create_domain(
270        &self,
271        name: &str,
272        domain_server_id: Option<String>,
273        domain_server_url: Option<String>,
274        redirect_url: Option<String>,
275    ) -> Result<DomainWithToken, DomainError> {
276        let domain_server_id = domain_server_id.unwrap_or_default();
277        let domain_server_url = domain_server_url.unwrap_or_default();
278        if domain_server_id.is_empty() && domain_server_url.is_empty() {
279            return Err(DomainError::InvalidRequest("domain_server_id or domain_server_url is required"));
280        }
281        let access_token: String = self
282            .api_client
283            .get_dds_access_token(self.oidc_access_token.as_deref())
284            .await
285            .map_err(|_| DomainError::AuthError(AuthError::Unauthorized("failed to get dds access token")))?;
286        let response = self
287            .client
288            .post(format!("{}/api/v1/domains?issue_token=true", self.dds_url))
289            .bearer_auth(access_token)
290            .header("Content-Type", "application/json")
291            .header("posemesh-client-id", self.api_client.client_id.clone())
292            .header("posemesh-sdk-version", VERSION)
293            .json(&CreateDomainRequest { name: name.to_string(), domain_server_id: domain_server_id.to_string(), redirect_url, domain_server_url: domain_server_url.to_string() })
294            .send()
295            .await?;
296
297        if response.status().is_success() {
298            let mut domain_with_token: DomainWithToken = response.json().await?;
299            domain_with_token.expires_at =
300                parse_jwt(&domain_with_token.get_access_token())
301                    .map_err(|_| DomainError::AuthError(AuthError::Unauthorized("invalid domain token")))? 
302                    .exp;
303            // Cache the result
304            let mut cache = self.cache.lock().await;
305            cache.insert(domain_with_token.domain.id.clone(), domain_with_token.clone());
306            Ok(domain_with_token)
307        } else {
308            let status = response.status();
309            let text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
310            Err(AukiErrorResponse { status, error: format!("Failed to create domain. {}", text) }.into())
311        }
312    }
313
314    /// List domains by portal, portal_id or portal_short_id is required
315    /// If org is not provided, it will list domains for the current authorized organization
316    /// If org is provided, it will list domains for the specified organization
317    /// Set org to `all` to list domains for all organizations
318    pub async fn list_domains_by_portal(
319        &self,
320        portal_id: Option<&str>,
321        portal_short_id: Option<&str>,
322        org: &str,
323    ) -> Result<ListDomainsResponse, DomainError> {
324        let access_token: String = self
325            .api_client
326            .get_dds_access_token(self.oidc_access_token.as_deref())
327            .await
328            .map_err(|_| DomainError::AuthError(AuthError::Unauthorized("failed to get dds access token")))?;
329        if portal_id.is_none() && portal_short_id.is_none() {
330            return Err(DomainError::InvalidRequest("portal_id or portal_short_id is required"));
331        }
332        let id = portal_id.or(portal_short_id).unwrap();
333        let response = self
334            .client
335            .get(format!("{}/api/v1/lighthouses/{}/domains?with=domain_server,lighthouse&org={}", self.dds_url, id, org))
336            .bearer_auth(access_token)
337            .header("Content-Type", "application/json")
338            .header("posemesh-client-id", self.api_client.client_id.clone())
339            .header("posemesh-sdk-version", VERSION)
340            .send()
341            .await?;
342        if response.status().is_success() {
343            let domains: ListDomainsResponse = response.json().await?;
344            Ok(domains)
345        } else {
346            let status = response.status();
347            let text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
348            Err(AukiErrorResponse { status, error: format!("Failed to list domains by portal. {}", text) }.into())
349        }
350    }
351
352    pub(crate) async fn delete_domain(
353        &self,
354        access_token: &str,
355        domain_id: &str,
356    ) -> Result<(), DomainError> {
357        let response = self
358            .client
359            .delete(format!("{}/api/v1/domains/{}", self.dds_url, domain_id))
360            .bearer_auth(access_token)
361            .header("Content-Type", "application/json")
362            .header("posemesh-client-id", self.api_client.client_id.clone())
363            .header("posemesh-sdk-version", VERSION)
364            .send()
365            .await?;
366        if response.status().is_success() {
367            Ok(())
368        } else {
369            let status = response.status();
370            let text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
371            Err(AukiErrorResponse { status, error: format!("Failed to delete domain. {}", text) }.into())
372        }
373    }
374}
375
376#[derive(Debug, Serialize)]
377struct CreateDomainRequest {
378    name: String,
379    domain_server_id: String,
380    redirect_url: Option<String>,
381    domain_server_url: String,
382}