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(
92 &self,
93 user_id: &str,
94 identifiers: Vec<Identifier>,
95 groups: Vec<Group>,
96 role: Option<&str>,
97 ) -> Result<String, VortexError> {
98 let parts: Vec<&str> = self.api_key.split('.').collect();
100 if parts.len() != 3 {
101 return Err(VortexError::InvalidApiKey(
102 "Invalid API key format".to_string(),
103 ));
104 }
105
106 let prefix = parts[0];
107 let encoded_id = parts[1];
108 let key = parts[2];
109
110 if prefix != "VRTX" {
111 return Err(VortexError::InvalidApiKey(
112 "Invalid API key prefix".to_string(),
113 ));
114 }
115
116 let id_bytes = URL_SAFE_NO_PAD
118 .decode(encoded_id)
119 .map_err(|e| VortexError::InvalidApiKey(format!("Failed to decode ID: {}", e)))?;
120
121 if id_bytes.len() != 16 {
122 return Err(VortexError::InvalidApiKey("ID must be 16 bytes".to_string()));
123 }
124
125 let uuid = Uuid::from_slice(&id_bytes)
126 .map_err(|e| VortexError::InvalidApiKey(format!("Invalid UUID: {}", e)))?;
127 let uuid_str = uuid.to_string();
128
129 let now = SystemTime::now()
130 .duration_since(UNIX_EPOCH)
131 .unwrap()
132 .as_secs();
133 let expires = now + 3600; let mut hmac = HmacSha256::new_from_slice(key.as_bytes())
137 .map_err(|e| VortexError::CryptoError(format!("HMAC error: {}", e)))?;
138 hmac.update(uuid_str.as_bytes());
139 let signing_key = hmac.finalize().into_bytes();
140
141 let header = json!({
143 "iat": now,
144 "alg": "HS256",
145 "typ": "JWT",
146 "kid": uuid_str,
147 });
148
149 let payload = json!({
150 "userId": user_id,
151 "identifiers": identifiers,
152 "groups": groups,
153 "role": role,
154 "expires": expires,
155 });
156
157 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).unwrap());
159 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&payload).unwrap());
160
161 let to_sign = format!("{}.{}", header_b64, payload_b64);
163 let mut sig_hmac = HmacSha256::new_from_slice(&signing_key)
164 .map_err(|e| VortexError::CryptoError(format!("HMAC error: {}", e)))?;
165 sig_hmac.update(to_sign.as_bytes());
166 let signature = sig_hmac.finalize().into_bytes();
167 let sig_b64 = URL_SAFE_NO_PAD.encode(&signature);
168
169 Ok(format!("{}.{}.{}", header_b64, payload_b64, sig_b64))
170 }
171
172 pub async fn get_invitations_by_target(
174 &self,
175 target_type: &str,
176 target_value: &str,
177 ) -> Result<Vec<Invitation>, VortexError> {
178 let mut params = HashMap::new();
179 params.insert("targetType", target_type);
180 params.insert("targetValue", target_value);
181
182 let response: InvitationsResponse = self
183 .api_request("GET", "/api/v1/invitations", None::<&()>, Some(params))
184 .await?;
185
186 Ok(response.invitations.unwrap_or_default())
187 }
188
189 pub async fn get_invitation(&self, invitation_id: &str) -> Result<Invitation, VortexError> {
191 self.api_request(
192 "GET",
193 &format!("/api/v1/invitations/{}", invitation_id),
194 None::<&()>,
195 None,
196 )
197 .await
198 }
199
200 pub async fn revoke_invitation(&self, invitation_id: &str) -> Result<(), VortexError> {
202 self.api_request::<(), ()>(
203 "DELETE",
204 &format!("/api/v1/invitations/{}", invitation_id),
205 None,
206 None,
207 )
208 .await?;
209 Ok(())
210 }
211
212 pub async fn accept_invitations(
214 &self,
215 invitation_ids: Vec<String>,
216 target: InvitationTarget,
217 ) -> Result<Invitation, VortexError> {
218 let body = json!({
219 "invitationIds": invitation_ids,
220 "target": target,
221 });
222
223 self.api_request("POST", "/api/v1/invitations/accept", Some(&body), None)
224 .await
225 }
226
227 pub async fn delete_invitations_by_group(
229 &self,
230 group_type: &str,
231 group_id: &str,
232 ) -> Result<(), VortexError> {
233 self.api_request::<(), ()>(
234 "DELETE",
235 &format!("/api/v1/invitations/by-group/{}/{}", group_type, group_id),
236 None,
237 None,
238 )
239 .await?;
240 Ok(())
241 }
242
243 pub async fn get_invitations_by_group(
245 &self,
246 group_type: &str,
247 group_id: &str,
248 ) -> Result<Vec<Invitation>, VortexError> {
249 let response: InvitationsResponse = self
250 .api_request(
251 "GET",
252 &format!("/api/v1/invitations/by-group/{}/{}", group_type, group_id),
253 None::<&()>,
254 None,
255 )
256 .await?;
257
258 Ok(response.invitations.unwrap_or_default())
259 }
260
261 pub async fn reinvite(&self, invitation_id: &str) -> Result<Invitation, VortexError> {
263 self.api_request(
264 "POST",
265 &format!("/api/v1/invitations/{}/reinvite", invitation_id),
266 None::<&()>,
267 None,
268 )
269 .await
270 }
271
272 async fn api_request<T, B>(
273 &self,
274 method: &str,
275 path: &str,
276 body: Option<&B>,
277 query_params: Option<HashMap<&str, &str>>,
278 ) -> Result<T, VortexError>
279 where
280 T: serde::de::DeserializeOwned,
281 B: serde::Serialize,
282 {
283 let url = format!("{}{}", self.base_url, path);
284
285 let mut request = match method {
286 "GET" => self.http_client.get(&url),
287 "POST" => self.http_client.post(&url),
288 "PUT" => self.http_client.put(&url),
289 "DELETE" => self.http_client.delete(&url),
290 _ => return Err(VortexError::InvalidRequest("Invalid HTTP method".to_string())),
291 };
292
293 request = request
295 .header("Content-Type", "application/json")
296 .header("x-api-key", &self.api_key)
297 .header("User-Agent", "vortex-rust-sdk/1.0.0");
298
299 if let Some(params) = query_params {
301 request = request.query(¶ms);
302 }
303
304 if let Some(b) = body {
306 request = request.json(b);
307 }
308
309 let response = request
310 .send()
311 .await
312 .map_err(|e| VortexError::HttpError(e.to_string()))?;
313
314 if !response.status().is_success() {
315 let status = response.status();
316 let error_text = response
317 .text()
318 .await
319 .unwrap_or_else(|_| "Unknown error".to_string());
320 return Err(VortexError::ApiError(format!(
321 "API request failed: {} - {}",
322 status, error_text
323 )));
324 }
325
326 let text = response
327 .text()
328 .await
329 .map_err(|e| VortexError::HttpError(e.to_string()))?;
330
331 if text.is_empty() {
333 return serde_json::from_str("{}")
334 .map_err(|e| VortexError::SerializationError(e.to_string()));
335 }
336
337 serde_json::from_str(&text)
338 .map_err(|e| VortexError::SerializationError(e.to_string()))
339 }
340}