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 list_sessions(&self, query: &SessionListQuery) -> Result<SessionListResponse> {
170 let token = self.token_or_bail()?;
171 let mut url = self.url("/sessions");
172
173 let mut params = Vec::new();
175 params.push(format!("page={}", query.page));
176 params.push(format!("per_page={}", query.per_page));
177 if let Some(ref s) = query.search {
178 params.push(format!("search={s}"));
179 }
180 if let Some(ref t) = query.tool {
181 params.push(format!("tool={t}"));
182 }
183 if let Some(ref s) = query.sort {
184 params.push(format!("sort={s}"));
185 }
186 if let Some(ref r) = query.time_range {
187 params.push(format!("time_range={r}"));
188 }
189 if !params.is_empty() {
190 url = format!("{}?{}", url, params.join("&"));
191 }
192
193 let resp = self.client.get(&url).bearer_auth(token).send().await?;
194 parse_response(resp).await
195 }
196
197 pub async fn get_session(&self, id: &str) -> Result<SessionDetail> {
198 let token = self.token_or_bail()?;
199 let resp = self
200 .client
201 .get(self.url(&format!("/sessions/{id}")))
202 .bearer_auth(token)
203 .send()
204 .await?;
205 parse_response(resp).await
206 }
207
208 pub async fn delete_session(&self, id: &str) -> Result<OkResponse> {
209 let token = self.token_or_bail()?;
210 let resp = self
211 .client
212 .delete(self.url(&format!("/sessions/{id}")))
213 .bearer_auth(token)
214 .send()
215 .await?;
216 parse_response(resp).await
217 }
218
219 pub async fn get_session_raw(&self, id: &str) -> Result<serde_json::Value> {
220 let token = self.token_or_bail()?;
221 let resp = self
222 .client
223 .get(self.url(&format!("/sessions/{id}/raw")))
224 .bearer_auth(token)
225 .send()
226 .await?;
227 parse_response(resp).await
228 }
229
230 pub async fn get_with_auth(&self, path: &str, token: &str) -> Result<reqwest::Response> {
234 Ok(self
235 .client
236 .get(self.url(path))
237 .bearer_auth(token)
238 .send()
239 .await?)
240 }
241
242 pub async fn post_with_auth(&self, path: &str, token: &str) -> Result<reqwest::Response> {
244 Ok(self
245 .client
246 .post(self.url(path))
247 .bearer_auth(token)
248 .send()
249 .await?)
250 }
251
252 pub async fn post_json_with_auth<T: Serialize>(
254 &self,
255 path: &str,
256 token: &str,
257 body: &T,
258 ) -> Result<reqwest::Response> {
259 Ok(self
260 .client
261 .post(self.url(path))
262 .bearer_auth(token)
263 .json(body)
264 .send()
265 .await?)
266 }
267
268 pub async fn put_json_with_auth<T: Serialize>(
270 &self,
271 path: &str,
272 token: &str,
273 body: &T,
274 ) -> Result<reqwest::Response> {
275 Ok(self
276 .client
277 .put(self.url(path))
278 .bearer_auth(token)
279 .json(body)
280 .send()
281 .await?)
282 }
283
284 pub async fn delete_with_auth(&self, path: &str, token: &str) -> Result<reqwest::Response> {
286 Ok(self
287 .client
288 .delete(self.url(path))
289 .bearer_auth(token)
290 .send()
291 .await?)
292 }
293
294 pub async fn post_json_raw<T: Serialize>(
296 &self,
297 path: &str,
298 body: &T,
299 ) -> Result<reqwest::Response> {
300 Ok(self.client.post(self.url(path)).json(body).send().await?)
301 }
302}
303
304async fn parse_response<T: serde::de::DeserializeOwned>(resp: reqwest::Response) -> Result<T> {
307 let status = resp.status();
308 if !status.is_success() {
309 let body = resp.text().await.unwrap_or_default();
310 bail!("{status}: {body}");
311 }
312 Ok(resp.json().await?)
313}
314
315#[cfg(test)]
316mod tests {
317 use super::ApiClient;
318 use std::time::Duration;
319
320 #[test]
321 fn set_auth_trims_surrounding_whitespace() {
322 let mut client = ApiClient::new("https://example.com", Duration::from_secs(1))
323 .expect("client should construct");
324
325 client.set_auth(" osk_test_token ".to_string());
326 assert_eq!(client.auth_token(), Some("osk_test_token"));
327 }
328
329 #[test]
330 fn set_auth_clears_auth_for_blank_tokens() {
331 let mut client = ApiClient::new("https://example.com", Duration::from_secs(1))
332 .expect("client should construct");
333
334 client.set_auth("osk_test_token".to_string());
335 assert_eq!(client.auth_token(), Some("osk_test_token"));
336
337 client.set_auth(" ".to_string());
338 assert_eq!(client.auth_token(), None);
339 }
340}