posemesh_domain_http/
discovery.rs

1use std::{collections::HashMap, sync::Arc, time::Duration};
2
3use futures::lock::Mutex;
4use reqwest::Client;
5use serde::Deserialize;
6
7#[cfg(not(target_family = "wasm"))]
8use tokio::spawn;
9#[cfg(target_family = "wasm")]
10use wasm_bindgen_futures::spawn_local as spawn;
11
12#[cfg(target_family = "wasm")]
13use posemesh_utils::sleep;
14#[cfg(not(target_family = "wasm"))]
15use tokio::time::sleep;
16use posemesh_utils::now_unix_secs;
17
18use crate::auth::{get_cached_or_fresh_token, parse_jwt, AuthClient, TokenCache};
19
20#[derive(Debug, Deserialize, Clone)]
21pub struct Domain {
22    pub id: String,
23    pub name: String,
24}
25
26#[derive(Debug, Deserialize, Clone)]
27pub struct DomainServer {
28    pub id: String,
29    pub organization_id: String,
30    pub name: String,
31    pub url: String,
32}
33
34#[derive(Debug, Deserialize, Clone)]
35pub struct DomainWithToken {
36    #[serde(flatten)]
37    pub domain: DomainWithServer,
38    pub access_token: String,
39    #[serde(skip)]
40    pub expires_at: u64,
41}
42impl TokenCache for DomainWithToken {
43    fn get_access_token(&self) -> String {
44        self.access_token.clone()
45    }
46
47    fn get_expires_at(&self) -> u64 {
48        self.expires_at
49    }
50
51    fn set_expires_at(&mut self, expires_at: u64) {
52        self.expires_at = expires_at;
53    }
54}
55
56#[derive(Debug, Deserialize, Clone)]
57pub struct DomainWithServer {
58    #[serde(flatten)]
59    pub domain: Domain,
60    pub domain_server: DomainServer,
61}
62
63#[derive(Debug, Clone)]
64pub struct DiscoveryService {
65    dds_url: String,
66    client: Client,
67    cache: Arc<Mutex<HashMap<String, DomainWithToken>>>,
68    client_id: String,
69    api_client: AuthClient,
70}
71
72#[derive(Debug, Deserialize)]
73pub struct ListDomainsResponse {
74    pub domains: Vec<DomainWithServer>,
75}
76
77impl DiscoveryService {
78    pub fn new(api_url: &str, dds_url: &str, client_id: &str) -> Self {
79        let api_client = AuthClient::new(api_url, client_id);
80        Self {
81            dds_url: dds_url.to_string(),
82            client: Client::new(),
83            cache: Arc::new(Mutex::new(HashMap::new())),
84            client_id: client_id.to_string(),
85            api_client,
86        }
87    }
88
89    pub async fn list_domains(&self, access_token: &str) -> Result<Vec<DomainWithServer>, Box<dyn std::error::Error + Send + Sync>> {
90        let response = self.client
91            .get(&format!("{}/api/v1/domains?with=domain_server", self.dds_url))
92            .bearer_auth(access_token)
93            .header("Content-Type", "application/json")
94            .header("posemesh-client-id", self.client_id.clone())
95            .send()
96            .await?;
97
98        if response.status().is_success() {
99            let domain_servers: ListDomainsResponse = response.json().await?;
100            Ok(domain_servers.domains)
101        } else {
102            Err(format!("Failed to list domains. Status: {}", response.status()).into())
103        }
104    }
105
106    pub async fn sign_in_with_auki_account(&mut self, email: &str, password: &str, logout: bool) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
107        self.cache.lock().await.clear();
108        let _ = self.api_client.user_login(email, password).await?;
109        if !logout {
110            let mut api_client = self.api_client.clone();
111            let email = email.to_string();
112            let password = password.to_string();
113            spawn(async move {
114                loop {
115                    let expires_at = api_client.get_expires_at().await.inspect_err(|e| tracing::error!("Failed to get expires at: {}", e));
116                    if let Ok(expires_at) = expires_at {
117                        let expiration = {
118                            let now = now_unix_secs();
119                            let duration = expires_at - now;
120                            if duration > 600 {
121                                Some(Duration::from_secs(duration))
122                            } else {
123                                None
124                            }
125                        };
126                        
127                        if let Some(expiration) = expiration {
128                            tracing::info!("Refreshing token in {} seconds", expiration.as_secs());
129                            sleep(expiration).await;
130                        }
131
132                        let _ = api_client.user_login(&email, &password).await.inspect_err(|e| tracing::error!("Failed to login: {}", e));
133                    }
134                }
135            });
136        }
137        Ok(())
138    }
139
140    pub async fn sign_in_as_auki_app(&mut self, app_key: &str, app_secret: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
141        self.cache.lock().await.clear();
142        self.api_client.set_app_credentials(app_key, app_secret).await;
143        Ok(())
144    }
145
146    pub async fn auth_domain(&self, domain_id: &str) -> Result<DomainWithToken, Box<dyn std::error::Error + Send + Sync>> {
147        let access_token = self.api_client.get_dds_access_token().await?;
148        // Check cache first
149        let cache = if let Some(cached_domain) = self.cache.lock().await.get(domain_id) {
150            cached_domain.clone()
151        } else {
152            DomainWithToken {
153                domain: DomainWithServer {
154                    domain: Domain {
155                        id: domain_id.to_string(),
156                        name: "".to_string(),
157                    },
158                    domain_server: DomainServer {
159                        id: "".to_string(),
160                        organization_id: "".to_string(),
161                        name: "".to_string(),
162                        url: "".to_string(),
163                    },
164                },
165                access_token: "".to_string(),
166                expires_at: 0,
167            }
168        };
169        
170        let cached = get_cached_or_fresh_token(&cache, || {
171            let client = self.client.clone();
172            let dds_url = self.dds_url.clone();
173            let client_id = self.client_id.clone();
174            async move {
175                let response = client
176                    .post(&format!("{}/api/v1/domains/{}/auth", dds_url, domain_id))
177                    .bearer_auth(access_token)
178                    .header("Content-Type", "application/json")
179                    .header("posemesh-client-id", client_id)
180                    .send()
181                    .await?;
182
183                if response.status().is_success() {
184                    let mut domain_with_token: DomainWithToken = response.json().await?;
185                    domain_with_token.expires_at = parse_jwt(&domain_with_token.access_token)?.exp;
186                    Ok(domain_with_token)
187                } else {
188                    let status = response.status();
189                    let text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
190                    Err(format!("Failed to auth domain. Status: {} - {}", status, text).into())
191                }
192            }
193        }).await?;
194
195        
196        // Cache the result
197        let mut cache = self.cache.lock().await;
198        cache.insert(domain_id.to_string(), cached.clone());
199        Ok(cached)
200    }
201}