posemesh_domain_http/
discovery.rs1use 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 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 let mut cache = self.cache.lock().await;
198 cache.insert(domain_id.to_string(), cached.clone());
199 Ok(cached)
200 }
201}