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::{
19 auth::{AuthClient, REFRESH_CACHE_TIME, TokenCache, get_cached_or_fresh_token, parse_jwt},
20 errors::{AukiErrorResponse, DomainError},
21};
22pub const ALL_DOMAINS_ORG: &str = "all";
23pub const OWN_DOMAINS_ORG: &str = "own";
24
25#[derive(Debug, Deserialize, Clone, Serialize)]
26pub struct DomainServer {
27 pub id: String,
28 pub organization_id: String,
29 pub name: String,
30 pub url: String,
31}
32
33#[derive(Debug, Deserialize, Clone)]
34pub struct DomainWithToken {
35 #[serde(flatten)]
36 pub domain: DomainWithServer,
37 #[serde(skip)]
38 pub expires_at: u64,
39 access_token: String,
40}
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
52#[derive(Debug, Deserialize, Clone, Serialize)]
53pub struct DomainWithServer {
54 pub id: String,
55 pub name: String,
56 pub organization_id: String,
57 pub domain_server_id: String,
58 pub redirect_url: Option<String>,
59 pub domain_server: DomainServer,
60}
61
62#[derive(Debug, Clone)]
63pub struct DiscoveryService {
64 dds_url: String,
65 client: Client,
66 cache: Arc<Mutex<HashMap<String, DomainWithToken>>>,
67 api_client: AuthClient,
68 oidc_access_token: Option<String>,
69}
70
71#[derive(Debug, Deserialize)]
72pub struct ListDomainsResponse {
73 pub domains: Vec<DomainWithServer>,
74}
75
76#[derive(Debug, Serialize)]
77pub struct CreateDomainRequest {
78 pub name: String,
79 pub domain_server_id: String,
80 pub redirect_url: Option<String>,
81 domain_server_url: String,
82}
83
84fn get_mac_address() -> Result<String, DomainError> {
87 #[cfg(not(target_family = "wasm"))]
88 {
89 match default_net::get_default_gateway() {
90 Ok(gateway) => Ok(gateway.mac_addr.to_string()),
91 Err(_) => Err(DomainError::InvalidRequest("No gateway found")),
92 }
93 }
94
95 #[cfg(target_family = "wasm")]
96 {
97 Ok(String::new())
100 }
101}
102
103impl DiscoveryService {
104 pub fn new(api_url: &str, dds_url: &str, client_id: &str) -> Self {
105 let api_client = AuthClient::new(api_url, client_id);
106
107 Self {
108 dds_url: dds_url.to_string(),
109 client: Client::new(),
110 cache: Arc::new(Mutex::new(HashMap::new())),
111 api_client,
112 oidc_access_token: None,
113 }
114 }
115
116 pub async fn list_domains(
134 &self,
135 org: &str,
136 domain_server_id: Option<&str>,
137 ) -> Result<ListDomainsResponse, DomainError> {
138 let access_token = self
139 .api_client
140 .get_dds_access_token(self.oidc_access_token.as_deref())
141 .await?;
142 let mut url = format!(
143 "{}/api/v1/domains?org={}&with=domain_server",
144 self.dds_url, org
145 );
146 if let Some(domain_server_id) = domain_server_id {
147 url.push_str(&format!("&domain_server_id={}", domain_server_id));
148 }
149 let response = self
150 .client
151 .get(&url)
152 .bearer_auth(access_token)
153 .header("Content-Type", "application/json")
154 .header("posemesh-client-id", self.api_client.client_id.clone())
155 .header("posemesh-sdk-version", crate::VERSION)
156 .header(
157 "posemesh-gateway-mac",
158 get_mac_address().unwrap_or_default(),
159 )
160 .send()
161 .await?;
162
163 if response.status().is_success() {
164 let domain_servers: ListDomainsResponse = response.json().await?;
165 Ok(domain_servers)
166 } else {
167 let status = response.status();
168 let text = response
169 .text()
170 .await
171 .unwrap_or_else(|_| "Unknown error".to_string());
172 Err(AukiErrorResponse {
173 status,
174 error: format!("Failed to list domains. {}", text),
175 }
176 .into())
177 }
178 }
179
180 pub async fn sign_in_with_auki_account(
181 &mut self,
182 email: &str,
183 password: &str,
184 remember_password: bool,
185 ) -> Result<String, DomainError> {
186 self.cache.lock().await.clear();
187 self.oidc_access_token = None;
188 let token = self.api_client.user_login(email, password).await?;
189 if remember_password {
190 let mut api_client = self.api_client.clone();
191 let email = email.to_string();
192 let password = password.to_string();
193 spawn(async move {
194 loop {
195 let expires_at = api_client
196 .get_expires_at()
197 .await
198 .inspect_err(|e| tracing::error!("Failed to get expires at: {}", e));
199 if let Ok(expires_at) = expires_at {
200 let expiration = {
201 let now = now_unix_secs();
202 let duration = expires_at - now;
203 if duration > REFRESH_CACHE_TIME {
204 Some(Duration::from_secs(duration))
205 } else {
206 None
207 }
208 };
209
210 if let Some(expiration) = expiration {
211 tracing::info!("Refreshing token in {} seconds", expiration.as_secs());
212 sleep(expiration).await;
213 }
214
215 let _ = api_client
216 .user_login(&email, &password)
217 .await
218 .inspect_err(|e| tracing::error!("Failed to relogin: {}", e));
219 }
220 }
221 });
222 }
223 Ok(token)
224 }
225
226 pub async fn sign_in_as_auki_app(
227 &mut self,
228 app_key: &str,
229 app_secret: &str,
230 ) -> Result<String, DomainError> {
231 self.cache.lock().await.clear();
232 self.oidc_access_token = None;
233 self.api_client
234 .sign_in_with_app_credentials(app_key, app_secret)
235 .await
236 }
237
238 pub fn with_oidc_access_token(&self, oidc_access_token: &str) -> Self {
239 if let Some(cached_oidc_access_token) = self.oidc_access_token.as_deref()
240 && cached_oidc_access_token == oidc_access_token
241 {
242 return self.clone();
243 }
244 Self {
245 dds_url: self.dds_url.clone(),
246 client: self.client.clone(),
247 cache: Arc::new(Mutex::new(HashMap::new())),
248 api_client: AuthClient::new(&self.api_client.api_url, &self.api_client.client_id),
249 oidc_access_token: Some(oidc_access_token.to_string()),
250 }
251 }
252
253 pub async fn auth_domain(&self, domain_id: &str) -> Result<DomainWithToken, DomainError> {
254 let access_token = self
255 .api_client
256 .get_dds_access_token(self.oidc_access_token.as_deref())
257 .await?;
258 let cache = if let Some(cached_domain) = self.cache.lock().await.get(domain_id) {
260 cached_domain.clone()
261 } else {
262 DomainWithToken {
263 domain: DomainWithServer {
264 id: domain_id.to_string(),
265 name: "".to_string(),
266 organization_id: "".to_string(),
267 domain_server_id: "".to_string(),
268 redirect_url: None,
269 domain_server: DomainServer {
270 id: "".to_string(),
271 organization_id: "".to_string(),
272 name: "".to_string(),
273 url: "".to_string(),
274 },
275 },
276 expires_at: 0,
277 access_token: "".to_string(),
278 }
279 };
280
281 let cached = get_cached_or_fresh_token(&cache, || {
282 let client = self.client.clone();
283 let dds_url = self.dds_url.clone();
284 let client_id = self.api_client.client_id.clone();
285 async move {
286 let mac_address = get_mac_address().unwrap_or_default();
287 let response = client
288 .post(format!("{}/api/v1/domains/{}/auth", dds_url, domain_id))
289 .bearer_auth(access_token)
290 .header("Content-Type", "application/json")
291 .header("posemesh-client-id", client_id)
292 .header("posemesh-sdk-version", crate::VERSION)
293 .header("posemesh-gateway-mac", mac_address)
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())?.exp;
301 Ok(domain_with_token)
302 } else {
303 let status = response.status();
304 let text = response
305 .text()
306 .await
307 .unwrap_or_else(|_| "Unknown error".to_string());
308 Err(AukiErrorResponse {
309 status,
310 error: format!("Failed to auth domain. {}", text),
311 }
312 .into())
313 }
314 }
315 })
316 .await?;
317
318 let mut cache = self.cache.lock().await;
320 cache.insert(domain_id.to_string(), cached.clone());
321 Ok(cached)
322 }
323
324 pub async fn create_domain(
325 &self,
326 name: &str,
327 domain_server_id: Option<String>,
328 domain_server_url: Option<String>,
329 redirect_url: Option<String>,
330 ) -> Result<DomainWithToken, DomainError> {
331 let domain_server_id = domain_server_id.unwrap_or_default();
332 let domain_server_url = domain_server_url.unwrap_or_default();
333 if domain_server_id.is_empty() && domain_server_url.is_empty() {
334 return Err(DomainError::InvalidRequest(
335 "domain_server_id or domain_server_url is required",
336 ));
337 }
338 let access_token: String = self
339 .api_client
340 .get_dds_access_token(self.oidc_access_token.as_deref())
341 .await?;
342 let response = self
343 .client
344 .post(format!("{}/api/v1/domains?issue_token=true", self.dds_url))
345 .bearer_auth(access_token)
346 .header("Content-Type", "application/json")
347 .header("posemesh-client-id", self.api_client.client_id.clone())
348 .header("posemesh-sdk-version", crate::VERSION)
349 .header(
350 "posemesh-gateway-mac",
351 get_mac_address().unwrap_or_default(),
352 )
353 .json(&CreateDomainRequest {
354 name: name.to_string(),
355 domain_server_id: domain_server_id.to_string(),
356 redirect_url,
357 domain_server_url: domain_server_url.to_string(),
358 })
359 .send()
360 .await?;
361
362 if response.status().is_success() {
363 let mut domain_with_token: DomainWithToken = response.json().await?;
364 domain_with_token.expires_at = parse_jwt(&domain_with_token.get_access_token())?.exp;
365 let mut cache = self.cache.lock().await;
367 cache.insert(
368 domain_with_token.domain.id.clone(),
369 domain_with_token.clone(),
370 );
371 Ok(domain_with_token)
372 } else {
373 let status = response.status();
374 let text = response
375 .text()
376 .await
377 .unwrap_or_else(|_| "Unknown error".to_string());
378 Err(AukiErrorResponse {
379 status,
380 error: format!("Failed to create domain. {}", text),
381 }
382 .into())
383 }
384 }
385
386 pub async fn list_domains_by_portal(
391 &self,
392 portal_id: Option<&str>,
393 portal_short_id: Option<&str>,
394 org: &str,
395 ) -> Result<ListDomainsResponse, DomainError> {
396 let access_token: String = self
397 .api_client
398 .get_dds_access_token(self.oidc_access_token.as_deref())
399 .await?;
400 if portal_id.is_none() && portal_short_id.is_none() {
401 return Err(DomainError::InvalidRequest(
402 "portal_id or portal_short_id is required",
403 ));
404 }
405 let id = portal_id.or(portal_short_id).unwrap();
406 let response = self
407 .client
408 .get(format!(
409 "{}/api/v1/lighthouses/{}/domains?with=domain_server,lighthouse&org={}",
410 self.dds_url, id, org
411 ))
412 .bearer_auth(access_token)
413 .header("Content-Type", "application/json")
414 .header("posemesh-client-id", self.api_client.client_id.clone())
415 .header("posemesh-sdk-version", crate::VERSION)
416 .header(
417 "posemesh-gateway-mac",
418 get_mac_address().unwrap_or_default(),
419 )
420 .send()
421 .await?;
422 if response.status().is_success() {
423 let domains: ListDomainsResponse = response.json().await?;
424 Ok(domains)
425 } else {
426 let status = response.status();
427 let text = response
428 .text()
429 .await
430 .unwrap_or_else(|_| "Unknown error".to_string());
431 Err(AukiErrorResponse {
432 status,
433 error: format!("Failed to list domains by portal. {}", text),
434 }
435 .into())
436 }
437 }
438
439 pub(crate) async fn delete_domain(
440 &self,
441 access_token: &str,
442 domain_id: &str,
443 ) -> Result<(), DomainError> {
444 let response = self
445 .client
446 .delete(format!("{}/api/v1/domains/{}", self.dds_url, domain_id))
447 .bearer_auth(access_token)
448 .header("Content-Type", "application/json")
449 .header("posemesh-client-id", self.api_client.client_id.clone())
450 .header("posemesh-sdk-version", crate::VERSION)
451 .header(
452 "posemesh-gateway-mac",
453 get_mac_address().unwrap_or_default(),
454 )
455 .send()
456 .await?;
457 if response.status().is_success() {
458 Ok(())
459 } else {
460 let status = response.status();
461 let text = response
462 .text()
463 .await
464 .unwrap_or_else(|_| "Unknown error".to_string());
465 Err(AukiErrorResponse {
466 status,
467 error: format!("Failed to delete domain. {}", text),
468 }
469 .into())
470 }
471 }
472}