systemprompt_cloud/api_client/
client.rs1use anyhow::{anyhow, Context, Result};
2use chrono::Utc;
3use reqwest::{Client, StatusCode};
4use serde::de::DeserializeOwned;
5use serde::Serialize;
6use systemprompt_models::modules::ApiPaths;
7
8use super::types::{
9 ActivityRequest, ApiError, CheckoutRequest, CheckoutResponse, ListResponse, Plan, Tenant,
10 UserMeResponse,
11};
12
13#[derive(Debug)]
14pub struct CloudApiClient {
15 pub(super) client: Client,
16 pub(super) api_url: String,
17 pub(super) token: String,
18}
19
20impl CloudApiClient {
21 #[must_use]
22 pub fn new(api_url: &str, token: &str) -> Self {
23 Self {
24 client: Client::new(),
25 api_url: api_url.to_string(),
26 token: token.to_string(),
27 }
28 }
29
30 #[must_use]
31 pub fn api_url(&self) -> &str {
32 &self.api_url
33 }
34
35 #[must_use]
36 pub fn token(&self) -> &str {
37 &self.token
38 }
39
40 pub(super) async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
41 let url = format!("{}{}", self.api_url, path);
42 let response = self
43 .client
44 .get(&url)
45 .header("Authorization", format!("Bearer {}", self.token))
46 .send()
47 .await
48 .context("Failed to connect to API")?;
49
50 self.handle_response(response).await
51 }
52
53 pub(super) async fn post<T: DeserializeOwned, B: Serialize + Sync>(
54 &self,
55 path: &str,
56 body: &B,
57 ) -> Result<T> {
58 let url = format!("{}{}", self.api_url, path);
59 let response = self
60 .client
61 .post(&url)
62 .header("Authorization", format!("Bearer {}", self.token))
63 .json(body)
64 .send()
65 .await
66 .context("Failed to connect to API")?;
67
68 self.handle_response(response).await
69 }
70
71 pub(super) async fn post_no_response<B: Serialize + Sync>(
72 &self,
73 path: &str,
74 body: &B,
75 ) -> Result<()> {
76 let url = format!("{}{}", self.api_url, path);
77 let response = self
78 .client
79 .post(&url)
80 .header("Authorization", format!("Bearer {}", self.token))
81 .json(body)
82 .send()
83 .await
84 .context("Failed to connect to API")?;
85
86 let status = response.status();
87 if status == StatusCode::UNAUTHORIZED {
88 return Err(anyhow!(
89 "Authentication failed. Please run 'systemprompt cloud login' again."
90 ));
91 }
92 if status == StatusCode::NO_CONTENT || status.is_success() {
93 return Ok(());
94 }
95
96 let error_text = response
97 .text()
98 .await
99 .unwrap_or_else(|_| String::from("<failed to read response body>"));
100
101 serde_json::from_str::<ApiError>(&error_text).map_or_else(
102 |_| {
103 Err(anyhow!(
104 "Request failed with status {}: {}",
105 status,
106 error_text.chars().take(500).collect::<String>()
107 ))
108 },
109 |parsed| Err(anyhow!("{}: {}", parsed.error.code, parsed.error.message)),
110 )
111 }
112
113 pub(super) async fn put<T: DeserializeOwned, B: Serialize + Sync>(
114 &self,
115 path: &str,
116 body: &B,
117 ) -> Result<T> {
118 let url = format!("{}{}", self.api_url, path);
119 let response = self
120 .client
121 .put(&url)
122 .header("Authorization", format!("Bearer {}", self.token))
123 .json(body)
124 .send()
125 .await
126 .context("Failed to connect to API")?;
127
128 self.handle_response(response).await
129 }
130
131 pub(super) async fn put_no_content<B: Serialize + Sync>(
132 &self,
133 path: &str,
134 body: &B,
135 ) -> Result<()> {
136 let url = format!("{}{}", self.api_url, path);
137 let response = self
138 .client
139 .put(&url)
140 .header("Authorization", format!("Bearer {}", self.token))
141 .json(body)
142 .send()
143 .await
144 .context("Failed to connect to API")?;
145
146 let status = response.status();
147 if status == StatusCode::UNAUTHORIZED {
148 return Err(anyhow!(
149 "Authentication failed. Please run 'systemprompt cloud login' again."
150 ));
151 }
152 if status == StatusCode::NO_CONTENT || status.is_success() {
153 return Ok(());
154 }
155
156 let error_text = response
157 .text()
158 .await
159 .unwrap_or_else(|_| String::from("<failed to read response body>"));
160
161 serde_json::from_str::<ApiError>(&error_text).map_or_else(
162 |_| {
163 Err(anyhow!(
164 "Request failed with status {}: {}",
165 status,
166 error_text.chars().take(500).collect::<String>()
167 ))
168 },
169 |parsed| Err(anyhow!("{}: {}", parsed.error.code, parsed.error.message)),
170 )
171 }
172
173 pub(super) async fn delete(&self, path: &str) -> Result<()> {
174 let url = format!("{}{}", self.api_url, path);
175 let response = self
176 .client
177 .delete(&url)
178 .header("Authorization", format!("Bearer {}", self.token))
179 .send()
180 .await
181 .context("Failed to connect to API")?;
182
183 let status = response.status();
184
185 if status == StatusCode::UNAUTHORIZED {
186 return Err(anyhow!(
187 "Authentication failed. Please run 'systemprompt cloud login' again."
188 ));
189 }
190
191 if status == StatusCode::NO_CONTENT {
192 return Ok(());
193 }
194
195 if !status.is_success() {
196 let error_text = response
197 .text()
198 .await
199 .unwrap_or_else(|_| String::from("<failed to read response body>"));
200
201 return serde_json::from_str::<ApiError>(&error_text).map_or_else(
202 |_| {
203 Err(anyhow!(
204 "Request failed with status {}: {}",
205 status,
206 error_text.chars().take(500).collect::<String>()
207 ))
208 },
209 |parsed| Err(anyhow!("{}: {}", parsed.error.code, parsed.error.message)),
210 );
211 }
212
213 Ok(())
214 }
215
216 pub(super) async fn post_empty<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
217 let url = format!("{}{}", self.api_url, path);
218 let response = self
219 .client
220 .post(&url)
221 .header("Authorization", format!("Bearer {}", self.token))
222 .send()
223 .await
224 .context("Failed to connect to API")?;
225
226 self.handle_response(response).await
227 }
228
229 async fn handle_response<T: DeserializeOwned>(&self, response: reqwest::Response) -> Result<T> {
230 let status = response.status();
231
232 if status == StatusCode::UNAUTHORIZED {
233 return Err(anyhow!(
234 "Authentication failed. Please run 'systemprompt cloud login' again."
235 ));
236 }
237
238 if !status.is_success() {
239 let error_text = response
240 .text()
241 .await
242 .unwrap_or_else(|_| String::from("<failed to read response body>"));
243
244 return serde_json::from_str::<ApiError>(&error_text).map_or_else(
245 |_| {
246 Err(anyhow!(
247 "Request failed with status {}: {}",
248 status,
249 error_text.chars().take(500).collect::<String>()
250 ))
251 },
252 |parsed| Err(anyhow!("{}: {}", parsed.error.code, parsed.error.message)),
253 );
254 }
255
256 response
257 .json()
258 .await
259 .context("Failed to parse API response")
260 }
261
262 pub async fn get_user(&self) -> Result<UserMeResponse> {
263 self.get(ApiPaths::AUTH_ME).await
264 }
265
266 pub async fn list_tenants(&self) -> Result<Vec<Tenant>> {
267 let response: ListResponse<Tenant> = self.get(ApiPaths::CLOUD_TENANTS).await?;
268 Ok(response.data)
269 }
270
271 pub async fn get_plans(&self) -> Result<Vec<Plan>> {
272 let plans: Vec<Plan> = self.get(ApiPaths::CLOUD_CHECKOUT_PLANS).await?;
273 Ok(plans)
274 }
275
276 pub async fn create_checkout(
277 &self,
278 price_id: &str,
279 region: &str,
280 redirect_uri: Option<&str>,
281 ) -> Result<CheckoutResponse> {
282 let request = CheckoutRequest {
283 price_id: price_id.to_string(),
284 region: region.to_string(),
285 redirect_uri: redirect_uri.map(String::from),
286 };
287 self.post(ApiPaths::CLOUD_CHECKOUT, &request).await
288 }
289
290 pub async fn report_activity(&self, event_type: &str, user_id: &str) -> Result<()> {
291 let request = ActivityRequest {
292 event: event_type.to_string(),
293 timestamp: Utc::now().to_rfc3339(),
294 data: super::types::ActivityData {
295 user_id: user_id.to_string(),
296 },
297 };
298 self.post_no_response(ApiPaths::CLOUD_ACTIVITY, &request)
299 .await
300 }
301}