1use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
2use hmac::{Hmac, Mac};
3use reqwest::Client as HttpClient;
4use serde_json::json;
5use sha2::Sha256;
6use std::collections::HashMap;
7use std::time::{SystemTime, UNIX_EPOCH};
8use uuid::Uuid;
9
10use crate::error::VortexError;
11use crate::types::*;
12
13type HmacSha256 = Hmac<Sha256>;
14
15pub struct VortexClient {
20 api_key: String,
21 base_url: String,
22 http_client: HttpClient,
23}
24
25impl VortexClient {
26 pub fn new(api_key: String) -> Self {
41 let base_url = std::env::var("VORTEX_API_BASE_URL")
42 .unwrap_or_else(|_| "https://api.vortexsoftware.com".to_string());
43
44 Self {
45 api_key,
46 base_url,
47 http_client: HttpClient::new(),
48 }
49 }
50
51 pub fn with_base_url(api_key: String, base_url: String) -> Self {
58 Self {
59 api_key,
60 base_url,
61 http_client: HttpClient::new(),
62 }
63 }
64
65 pub fn generate_jwt(
96 &self,
97 user: &User,
98 extra: Option<HashMap<String, serde_json::Value>>,
99 ) -> Result<String, VortexError> {
100 let parts: Vec<&str> = self.api_key.split('.').collect();
102 if parts.len() != 3 {
103 return Err(VortexError::InvalidApiKey(
104 "Invalid API key format".to_string(),
105 ));
106 }
107
108 let prefix = parts[0];
109 let encoded_id = parts[1];
110 let key = parts[2];
111
112 if prefix != "VRTX" {
113 return Err(VortexError::InvalidApiKey(
114 "Invalid API key prefix".to_string(),
115 ));
116 }
117
118 let id_bytes = URL_SAFE_NO_PAD
120 .decode(encoded_id)
121 .map_err(|e| VortexError::InvalidApiKey(format!("Failed to decode ID: {}", e)))?;
122
123 if id_bytes.len() != 16 {
124 return Err(VortexError::InvalidApiKey("ID must be 16 bytes".to_string()));
125 }
126
127 let uuid = Uuid::from_slice(&id_bytes)
128 .map_err(|e| VortexError::InvalidApiKey(format!("Invalid UUID: {}", e)))?;
129 let uuid_str = uuid.to_string();
130
131 let now = SystemTime::now()
132 .duration_since(UNIX_EPOCH)
133 .unwrap()
134 .as_secs();
135 let expires = now + 3600; let mut hmac = HmacSha256::new_from_slice(key.as_bytes())
139 .map_err(|e| VortexError::CryptoError(format!("HMAC error: {}", e)))?;
140 hmac.update(uuid_str.as_bytes());
141 let signing_key = hmac.finalize().into_bytes();
142
143 let header = json!({
145 "iat": now,
146 "alg": "HS256",
147 "typ": "JWT",
148 "kid": uuid_str,
149 });
150
151 let mut payload_json = json!({
153 "userId": user.id,
154 "userEmail": user.email,
155 "expires": expires,
156 });
157
158 if let Some(ref user_name) = user.user_name {
160 payload_json["userName"] = json!(user_name);
161 }
162
163 if let Some(ref user_avatar_url) = user.user_avatar_url {
165 payload_json["userAvatarUrl"] = json!(user_avatar_url);
166 }
167
168 if let Some(ref scopes) = user.admin_scopes {
170 payload_json["adminScopes"] = json!(scopes);
171 }
172
173 if let Some(extra_props) = extra {
175 for (key, value) in extra_props {
176 payload_json[key] = value;
177 }
178 }
179
180 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).unwrap());
182 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&payload_json).unwrap());
183
184 let to_sign = format!("{}.{}", header_b64, payload_b64);
186 let mut sig_hmac = HmacSha256::new_from_slice(&signing_key)
187 .map_err(|e| VortexError::CryptoError(format!("HMAC error: {}", e)))?;
188 sig_hmac.update(to_sign.as_bytes());
189 let signature = sig_hmac.finalize().into_bytes();
190 let sig_b64 = URL_SAFE_NO_PAD.encode(&signature);
191
192 Ok(format!("{}.{}.{}", header_b64, payload_b64, sig_b64))
193 }
194
195 pub async fn get_invitations_by_target(
197 &self,
198 target_type: &str,
199 target_value: &str,
200 ) -> Result<Vec<Invitation>, VortexError> {
201 let mut params = HashMap::new();
202 params.insert("targetType", target_type);
203 params.insert("targetValue", target_value);
204
205 let response: InvitationsResponse = self
206 .api_request("GET", "/api/v1/invitations", None::<&()>, Some(params))
207 .await?;
208
209 Ok(response.invitations.unwrap_or_default())
210 }
211
212 pub async fn get_invitation(&self, invitation_id: &str) -> Result<Invitation, VortexError> {
214 self.api_request(
215 "GET",
216 &format!("/api/v1/invitations/{}", invitation_id),
217 None::<&()>,
218 None,
219 )
220 .await
221 }
222
223 pub async fn revoke_invitation(&self, invitation_id: &str) -> Result<(), VortexError> {
225 self.api_request::<(), ()>(
226 "DELETE",
227 &format!("/api/v1/invitations/{}", invitation_id),
228 None,
229 None,
230 )
231 .await?;
232 Ok(())
233 }
234
235 pub async fn accept_invitations(
266 &self,
267 invitation_ids: Vec<String>,
268 param: impl Into<crate::types::AcceptInvitationParam>,
269 ) -> Result<Invitation, VortexError> {
270 use crate::types::{AcceptInvitationParam, AcceptUser};
271
272 let param = param.into();
273
274 let user = match param {
276 AcceptInvitationParam::Targets(targets) => {
277 eprintln!("[Vortex SDK] DEPRECATED: Passing a vector of targets is deprecated. Use the AcceptUser format and call once per user instead.");
278
279 if targets.is_empty() {
280 return Err(VortexError::InvalidRequest("No targets provided".to_string()));
281 }
282
283 let mut last_result = None;
284 let mut last_error = None;
285
286 for target in targets {
287 let user = match target.target_type {
289 InvitationTargetType::Email => AcceptUser::new().with_email(&target.value),
290 InvitationTargetType::Phone => AcceptUser::new().with_phone(&target.value),
291 _ => AcceptUser::new().with_email(&target.value),
292 };
293
294 match Box::pin(self.accept_invitations(invitation_ids.clone(), user)).await {
295 Ok(result) => last_result = Some(result),
296 Err(e) => last_error = Some(e),
297 }
298 }
299
300 if let Some(err) = last_error {
301 return Err(err);
302 }
303
304 return last_result.ok_or_else(|| VortexError::InvalidRequest("No results".to_string()));
305 }
306 AcceptInvitationParam::Target(target) => {
307 eprintln!("[Vortex SDK] DEPRECATED: Passing an InvitationTarget is deprecated. Use the AcceptUser format instead: AcceptUser::new().with_email(\"user@example.com\")");
308
309 match target.target_type {
311 InvitationTargetType::Email => AcceptUser::new().with_email(&target.value),
312 InvitationTargetType::Phone => AcceptUser::new().with_phone(&target.value),
313 _ => AcceptUser::new().with_email(&target.value), }
315 }
316 AcceptInvitationParam::User(user) => user,
317 };
318
319 if user.email.is_none() && user.phone.is_none() {
321 return Err(VortexError::InvalidRequest(
322 "User must have either email or phone".to_string(),
323 ));
324 }
325
326 let body = json!({
327 "invitationIds": invitation_ids,
328 "user": user,
329 });
330
331 self.api_request("POST", "/api/v1/invitations/accept", Some(&body), None)
332 .await
333 }
334
335 pub async fn delete_invitations_by_group(
337 &self,
338 group_type: &str,
339 group_id: &str,
340 ) -> Result<(), VortexError> {
341 self.api_request::<(), ()>(
342 "DELETE",
343 &format!("/api/v1/invitations/by-group/{}/{}", group_type, group_id),
344 None,
345 None,
346 )
347 .await?;
348 Ok(())
349 }
350
351 pub async fn get_invitations_by_group(
353 &self,
354 group_type: &str,
355 group_id: &str,
356 ) -> Result<Vec<Invitation>, VortexError> {
357 let response: InvitationsResponse = self
358 .api_request(
359 "GET",
360 &format!("/api/v1/invitations/by-group/{}/{}", group_type, group_id),
361 None::<&()>,
362 None,
363 )
364 .await?;
365
366 Ok(response.invitations.unwrap_or_default())
367 }
368
369 pub async fn reinvite(&self, invitation_id: &str) -> Result<Invitation, VortexError> {
371 self.api_request(
372 "POST",
373 &format!("/api/v1/invitations/{}/reinvite", invitation_id),
374 None::<&()>,
375 None,
376 )
377 .await
378 }
379
380 pub async fn create_invitation(
428 &self,
429 request: &CreateInvitationRequest,
430 ) -> Result<CreateInvitationResponse, VortexError> {
431 self.api_request("POST", "/api/v1/invitations", Some(request), None)
432 .await
433 }
434
435 async fn api_request<T, B>(
436 &self,
437 method: &str,
438 path: &str,
439 body: Option<&B>,
440 query_params: Option<HashMap<&str, &str>>,
441 ) -> Result<T, VortexError>
442 where
443 T: serde::de::DeserializeOwned,
444 B: serde::Serialize,
445 {
446 let url = format!("{}{}", self.base_url, path);
447
448 let mut request = match method {
449 "GET" => self.http_client.get(&url),
450 "POST" => self.http_client.post(&url),
451 "PUT" => self.http_client.put(&url),
452 "DELETE" => self.http_client.delete(&url),
453 _ => return Err(VortexError::InvalidRequest("Invalid HTTP method".to_string())),
454 };
455
456 request = request
458 .header("Content-Type", "application/json")
459 .header("x-api-key", &self.api_key)
460 .header("User-Agent", "vortex-rust-sdk/1.0.0");
461
462 if let Some(params) = query_params {
464 request = request.query(¶ms);
465 }
466
467 if let Some(b) = body {
469 request = request.json(b);
470 }
471
472 let response = request
473 .send()
474 .await
475 .map_err(|e| VortexError::HttpError(e.to_string()))?;
476
477 if !response.status().is_success() {
478 let status = response.status();
479 let error_text = response
480 .text()
481 .await
482 .unwrap_or_else(|_| "Unknown error".to_string());
483 return Err(VortexError::ApiError(format!(
484 "API request failed: {} - {}",
485 status, error_text
486 )));
487 }
488
489 let text = response
490 .text()
491 .await
492 .map_err(|e| VortexError::HttpError(e.to_string()))?;
493
494 if text.is_empty() {
496 return serde_json::from_str("{}")
497 .map_err(|e| VortexError::SerializationError(e.to_string()));
498 }
499
500 serde_json::from_str(&text)
501 .map_err(|e| VortexError::SerializationError(e.to_string()))
502 }
503}