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 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 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 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 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 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}