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(
91 &self,
92 user: &User,
93 extra: Option<HashMap<String, serde_json::Value>>,
94 ) -> Result<String, VortexError> {
95 let parts: Vec<&str> = self.api_key.split('.').collect();
97 if parts.len() != 3 {
98 return Err(VortexError::InvalidApiKey(
99 "Invalid API key format".to_string(),
100 ));
101 }
102
103 let prefix = parts[0];
104 let encoded_id = parts[1];
105 let key = parts[2];
106
107 if prefix != "VRTX" {
108 return Err(VortexError::InvalidApiKey(
109 "Invalid API key prefix".to_string(),
110 ));
111 }
112
113 let id_bytes = URL_SAFE_NO_PAD
115 .decode(encoded_id)
116 .map_err(|e| VortexError::InvalidApiKey(format!("Failed to decode ID: {}", e)))?;
117
118 if id_bytes.len() != 16 {
119 return Err(VortexError::InvalidApiKey("ID must be 16 bytes".to_string()));
120 }
121
122 let uuid = Uuid::from_slice(&id_bytes)
123 .map_err(|e| VortexError::InvalidApiKey(format!("Invalid UUID: {}", e)))?;
124 let uuid_str = uuid.to_string();
125
126 let now = SystemTime::now()
127 .duration_since(UNIX_EPOCH)
128 .unwrap()
129 .as_secs();
130 let expires = now + 3600; let mut hmac = HmacSha256::new_from_slice(key.as_bytes())
134 .map_err(|e| VortexError::CryptoError(format!("HMAC error: {}", e)))?;
135 hmac.update(uuid_str.as_bytes());
136 let signing_key = hmac.finalize().into_bytes();
137
138 let header = json!({
140 "iat": now,
141 "alg": "HS256",
142 "typ": "JWT",
143 "kid": uuid_str,
144 });
145
146 let mut payload_json = json!({
148 "userId": user.id,
149 "userEmail": user.email,
150 "expires": expires,
151 });
152
153 if let Some(ref scopes) = user.admin_scopes {
155 payload_json["adminScopes"] = json!(scopes);
156 }
157
158 if let Some(extra_props) = extra {
160 for (key, value) in extra_props {
161 payload_json[key] = value;
162 }
163 }
164
165 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).unwrap());
167 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&payload_json).unwrap());
168
169 let to_sign = format!("{}.{}", header_b64, payload_b64);
171 let mut sig_hmac = HmacSha256::new_from_slice(&signing_key)
172 .map_err(|e| VortexError::CryptoError(format!("HMAC error: {}", e)))?;
173 sig_hmac.update(to_sign.as_bytes());
174 let signature = sig_hmac.finalize().into_bytes();
175 let sig_b64 = URL_SAFE_NO_PAD.encode(&signature);
176
177 Ok(format!("{}.{}.{}", header_b64, payload_b64, sig_b64))
178 }
179
180 pub async fn get_invitations_by_target(
182 &self,
183 target_type: &str,
184 target_value: &str,
185 ) -> Result<Vec<Invitation>, VortexError> {
186 let mut params = HashMap::new();
187 params.insert("targetType", target_type);
188 params.insert("targetValue", target_value);
189
190 let response: InvitationsResponse = self
191 .api_request("GET", "/api/v1/invitations", None::<&()>, Some(params))
192 .await?;
193
194 Ok(response.invitations.unwrap_or_default())
195 }
196
197 pub async fn get_invitation(&self, invitation_id: &str) -> Result<Invitation, VortexError> {
199 self.api_request(
200 "GET",
201 &format!("/api/v1/invitations/{}", invitation_id),
202 None::<&()>,
203 None,
204 )
205 .await
206 }
207
208 pub async fn revoke_invitation(&self, invitation_id: &str) -> Result<(), VortexError> {
210 self.api_request::<(), ()>(
211 "DELETE",
212 &format!("/api/v1/invitations/{}", invitation_id),
213 None,
214 None,
215 )
216 .await?;
217 Ok(())
218 }
219
220 pub async fn accept_invitations(
251 &self,
252 invitation_ids: Vec<String>,
253 param: impl Into<crate::types::AcceptInvitationParam>,
254 ) -> Result<Invitation, VortexError> {
255 use crate::types::{AcceptInvitationParam, AcceptUser};
256
257 let param = param.into();
258
259 let user = match param {
261 AcceptInvitationParam::Targets(targets) => {
262 eprintln!("[Vortex SDK] DEPRECATED: Passing a vector of targets is deprecated. Use the AcceptUser format and call once per user instead.");
263
264 if targets.is_empty() {
265 return Err(VortexError::InvalidRequest("No targets provided".to_string()));
266 }
267
268 let mut last_result = None;
269 let mut last_error = None;
270
271 for target in targets {
272 let user = match target.target_type.as_str() {
274 "email" => AcceptUser::new().with_email(&target.value),
275 "sms" | "phoneNumber" => AcceptUser::new().with_phone(&target.value),
276 _ => AcceptUser::new().with_email(&target.value),
277 };
278
279 match Box::pin(self.accept_invitations(invitation_ids.clone(), user)).await {
280 Ok(result) => last_result = Some(result),
281 Err(e) => last_error = Some(e),
282 }
283 }
284
285 if let Some(err) = last_error {
286 return Err(err);
287 }
288
289 return last_result.ok_or_else(|| VortexError::InvalidRequest("No results".to_string()));
290 }
291 AcceptInvitationParam::Target(target) => {
292 eprintln!("[Vortex SDK] DEPRECATED: Passing an InvitationTarget is deprecated. Use the AcceptUser format instead: AcceptUser::new().with_email(\"user@example.com\")");
293
294 match target.target_type.as_str() {
296 "email" => AcceptUser::new().with_email(&target.value),
297 "sms" | "phoneNumber" => AcceptUser::new().with_phone(&target.value),
298 _ => AcceptUser::new().with_email(&target.value), }
300 }
301 AcceptInvitationParam::User(user) => user,
302 };
303
304 if user.email.is_none() && user.phone.is_none() {
306 return Err(VortexError::InvalidRequest(
307 "User must have either email or phone".to_string(),
308 ));
309 }
310
311 let body = json!({
312 "invitationIds": invitation_ids,
313 "user": user,
314 });
315
316 self.api_request("POST", "/api/v1/invitations/accept", Some(&body), None)
317 .await
318 }
319
320 pub async fn delete_invitations_by_group(
322 &self,
323 group_type: &str,
324 group_id: &str,
325 ) -> Result<(), VortexError> {
326 self.api_request::<(), ()>(
327 "DELETE",
328 &format!("/api/v1/invitations/by-group/{}/{}", group_type, group_id),
329 None,
330 None,
331 )
332 .await?;
333 Ok(())
334 }
335
336 pub async fn get_invitations_by_group(
338 &self,
339 group_type: &str,
340 group_id: &str,
341 ) -> Result<Vec<Invitation>, VortexError> {
342 let response: InvitationsResponse = self
343 .api_request(
344 "GET",
345 &format!("/api/v1/invitations/by-group/{}/{}", group_type, group_id),
346 None::<&()>,
347 None,
348 )
349 .await?;
350
351 Ok(response.invitations.unwrap_or_default())
352 }
353
354 pub async fn reinvite(&self, invitation_id: &str) -> Result<Invitation, VortexError> {
356 self.api_request(
357 "POST",
358 &format!("/api/v1/invitations/{}/reinvite", invitation_id),
359 None::<&()>,
360 None,
361 )
362 .await
363 }
364
365 async fn api_request<T, B>(
366 &self,
367 method: &str,
368 path: &str,
369 body: Option<&B>,
370 query_params: Option<HashMap<&str, &str>>,
371 ) -> Result<T, VortexError>
372 where
373 T: serde::de::DeserializeOwned,
374 B: serde::Serialize,
375 {
376 let url = format!("{}{}", self.base_url, path);
377
378 let mut request = match method {
379 "GET" => self.http_client.get(&url),
380 "POST" => self.http_client.post(&url),
381 "PUT" => self.http_client.put(&url),
382 "DELETE" => self.http_client.delete(&url),
383 _ => return Err(VortexError::InvalidRequest("Invalid HTTP method".to_string())),
384 };
385
386 request = request
388 .header("Content-Type", "application/json")
389 .header("x-api-key", &self.api_key)
390 .header("User-Agent", "vortex-rust-sdk/1.0.0");
391
392 if let Some(params) = query_params {
394 request = request.query(¶ms);
395 }
396
397 if let Some(b) = body {
399 request = request.json(b);
400 }
401
402 let response = request
403 .send()
404 .await
405 .map_err(|e| VortexError::HttpError(e.to_string()))?;
406
407 if !response.status().is_success() {
408 let status = response.status();
409 let error_text = response
410 .text()
411 .await
412 .unwrap_or_else(|_| "Unknown error".to_string());
413 return Err(VortexError::ApiError(format!(
414 "API request failed: {} - {}",
415 status, error_text
416 )));
417 }
418
419 let text = response
420 .text()
421 .await
422 .map_err(|e| VortexError::HttpError(e.to_string()))?;
423
424 if text.is_empty() {
426 return serde_json::from_str("{}")
427 .map_err(|e| VortexError::SerializationError(e.to_string()));
428 }
429
430 serde_json::from_str(&text)
431 .map_err(|e| VortexError::SerializationError(e.to_string()))
432 }
433}