opensession_api_client/
client.rs1use std::time::Duration;
2
3use anyhow::{bail, Result};
4use serde::Serialize;
5
6use opensession_api::*;
7
8pub struct ApiClient {
14 client: reqwest::Client,
15 base_url: String,
16 auth_token: Option<String>,
17}
18
19impl ApiClient {
20 pub fn new(base_url: &str, timeout: Duration) -> Result<Self> {
22 let client = reqwest::Client::builder().timeout(timeout).build()?;
23 Ok(Self {
24 client,
25 base_url: base_url.trim_end_matches('/').to_string(),
26 auth_token: None,
27 })
28 }
29
30 pub fn with_client(client: reqwest::Client, base_url: &str) -> Self {
32 Self {
33 client,
34 base_url: base_url.trim_end_matches('/').to_string(),
35 auth_token: None,
36 }
37 }
38
39 pub fn set_auth(&mut self, token: String) {
40 let normalized = token.trim();
41 if normalized.is_empty() {
42 self.auth_token = None;
43 return;
44 }
45 self.auth_token = Some(normalized.to_string());
46 }
47
48 pub fn auth_token(&self) -> Option<&str> {
49 self.auth_token.as_deref()
50 }
51
52 pub fn base_url(&self) -> &str {
53 &self.base_url
54 }
55
56 pub fn reqwest_client(&self) -> &reqwest::Client {
58 &self.client
59 }
60
61 fn url(&self, path: &str) -> String {
62 format!("{}/api{}", self.base_url, path)
63 }
64
65 fn token_or_bail(&self) -> Result<&str> {
66 self.auth_token
67 .as_deref()
68 .ok_or_else(|| anyhow::anyhow!("auth token not set"))
69 }
70
71 pub async fn health(&self) -> Result<HealthResponse> {
74 let resp = self.client.get(self.url("/health")).send().await?;
75 parse_response(resp).await
76 }
77
78 pub async fn login(&self, req: &LoginRequest) -> Result<AuthTokenResponse> {
81 let resp = self
82 .client
83 .post(self.url("/auth/login"))
84 .json(req)
85 .send()
86 .await?;
87 parse_response(resp).await
88 }
89
90 pub async fn register(&self, req: &AuthRegisterRequest) -> Result<AuthTokenResponse> {
91 let resp = self
92 .client
93 .post(self.url("/auth/register"))
94 .json(req)
95 .send()
96 .await?;
97 parse_response(resp).await
98 }
99
100 pub async fn verify(&self) -> Result<VerifyResponse> {
101 let token = self.token_or_bail()?;
102 let resp = self
103 .client
104 .post(self.url("/auth/verify"))
105 .bearer_auth(token)
106 .send()
107 .await?;
108 parse_response(resp).await
109 }
110
111 pub async fn me(&self) -> Result<UserSettingsResponse> {
112 let token = self.token_or_bail()?;
113 let resp = self
114 .client
115 .get(self.url("/auth/me"))
116 .bearer_auth(token)
117 .send()
118 .await?;
119 parse_response(resp).await
120 }
121
122 pub async fn refresh(&self, req: &RefreshRequest) -> Result<AuthTokenResponse> {
123 let resp = self
124 .client
125 .post(self.url("/auth/refresh"))
126 .json(req)
127 .send()
128 .await?;
129 parse_response(resp).await
130 }
131
132 pub async fn logout(&self, req: &LogoutRequest) -> Result<OkResponse> {
133 let token = self.token_or_bail()?;
134 let resp = self
135 .client
136 .post(self.url("/auth/logout"))
137 .bearer_auth(token)
138 .json(req)
139 .send()
140 .await?;
141 parse_response(resp).await
142 }
143
144 pub async fn change_password(&self, req: &ChangePasswordRequest) -> Result<OkResponse> {
145 let token = self.token_or_bail()?;
146 let resp = self
147 .client
148 .post(self.url("/auth/change-password"))
149 .bearer_auth(token)
150 .json(req)
151 .send()
152 .await?;
153 parse_response(resp).await
154 }
155
156 pub async fn issue_api_key(&self) -> Result<IssueApiKeyResponse> {
157 let token = self.token_or_bail()?;
158 let resp = self
159 .client
160 .post(self.url("/auth/api-keys/issue"))
161 .bearer_auth(token)
162 .send()
163 .await?;
164 parse_response(resp).await
165 }
166
167 pub async fn upload_session(&self, req: &UploadRequest) -> Result<UploadResponse> {
170 let token = self.token_or_bail()?;
171 let resp = self
172 .client
173 .post(self.url("/sessions"))
174 .bearer_auth(token)
175 .json(req)
176 .send()
177 .await?;
178 parse_response(resp).await
179 }
180
181 pub async fn list_sessions(&self, query: &SessionListQuery) -> Result<SessionListResponse> {
182 let token = self.token_or_bail()?;
183 let mut url = self.url("/sessions");
184
185 let mut params = Vec::new();
187 params.push(format!("page={}", query.page));
188 params.push(format!("per_page={}", query.per_page));
189 if let Some(ref s) = query.search {
190 params.push(format!("search={s}"));
191 }
192 if let Some(ref t) = query.tool {
193 params.push(format!("tool={t}"));
194 }
195 if let Some(ref s) = query.sort {
196 params.push(format!("sort={s}"));
197 }
198 if let Some(ref r) = query.time_range {
199 params.push(format!("time_range={r}"));
200 }
201 if !params.is_empty() {
202 url = format!("{}?{}", url, params.join("&"));
203 }
204
205 let resp = self.client.get(&url).bearer_auth(token).send().await?;
206 parse_response(resp).await
207 }
208
209 pub async fn get_session(&self, id: &str) -> Result<SessionDetail> {
210 let token = self.token_or_bail()?;
211 let resp = self
212 .client
213 .get(self.url(&format!("/sessions/{id}")))
214 .bearer_auth(token)
215 .send()
216 .await?;
217 parse_response(resp).await
218 }
219
220 pub async fn delete_session(&self, id: &str) -> Result<OkResponse> {
221 let token = self.token_or_bail()?;
222 let resp = self
223 .client
224 .delete(self.url(&format!("/sessions/{id}")))
225 .bearer_auth(token)
226 .send()
227 .await?;
228 parse_response(resp).await
229 }
230
231 pub async fn get_session_raw(&self, id: &str) -> Result<serde_json::Value> {
232 let token = self.token_or_bail()?;
233 let resp = self
234 .client
235 .get(self.url(&format!("/sessions/{id}/raw")))
236 .bearer_auth(token)
237 .send()
238 .await?;
239 parse_response(resp).await
240 }
241
242 pub async fn get_with_auth(&self, path: &str, token: &str) -> Result<reqwest::Response> {
246 Ok(self
247 .client
248 .get(self.url(path))
249 .bearer_auth(token)
250 .send()
251 .await?)
252 }
253
254 pub async fn post_with_auth(&self, path: &str, token: &str) -> Result<reqwest::Response> {
256 Ok(self
257 .client
258 .post(self.url(path))
259 .bearer_auth(token)
260 .send()
261 .await?)
262 }
263
264 pub async fn post_json_with_auth<T: Serialize>(
266 &self,
267 path: &str,
268 token: &str,
269 body: &T,
270 ) -> Result<reqwest::Response> {
271 Ok(self
272 .client
273 .post(self.url(path))
274 .bearer_auth(token)
275 .json(body)
276 .send()
277 .await?)
278 }
279
280 pub async fn put_json_with_auth<T: Serialize>(
282 &self,
283 path: &str,
284 token: &str,
285 body: &T,
286 ) -> Result<reqwest::Response> {
287 Ok(self
288 .client
289 .put(self.url(path))
290 .bearer_auth(token)
291 .json(body)
292 .send()
293 .await?)
294 }
295
296 pub async fn delete_with_auth(&self, path: &str, token: &str) -> Result<reqwest::Response> {
298 Ok(self
299 .client
300 .delete(self.url(path))
301 .bearer_auth(token)
302 .send()
303 .await?)
304 }
305
306 pub async fn post_json_raw<T: Serialize>(
308 &self,
309 path: &str,
310 body: &T,
311 ) -> Result<reqwest::Response> {
312 Ok(self.client.post(self.url(path)).json(body).send().await?)
313 }
314}
315
316async fn parse_response<T: serde::de::DeserializeOwned>(resp: reqwest::Response) -> Result<T> {
319 let status = resp.status();
320 if !status.is_success() {
321 let body = resp.text().await.unwrap_or_default();
322 bail!("{status}: {body}");
323 }
324 Ok(resp.json().await?)
325}
326
327#[cfg(test)]
328mod tests {
329 use super::ApiClient;
330 use std::time::Duration;
331
332 #[test]
333 fn set_auth_trims_surrounding_whitespace() {
334 let mut client = ApiClient::new("https://example.com", Duration::from_secs(1))
335 .expect("client should construct");
336
337 client.set_auth(" osk_test_token ".to_string());
338 assert_eq!(client.auth_token(), Some("osk_test_token"));
339 }
340
341 #[test]
342 fn set_auth_clears_auth_for_blank_tokens() {
343 let mut client = ApiClient::new("https://example.com", Duration::from_secs(1))
344 .expect("client should construct");
345
346 client.set_auth("osk_test_token".to_string());
347 assert_eq!(client.auth_token(), Some("osk_test_token"));
348
349 client.set_auth(" ".to_string());
350 assert_eq!(client.auth_token(), None);
351 }
352}