Skip to main content

opensession_api_client/
client.rs

1use std::time::Duration;
2
3use anyhow::{bail, Result};
4use serde::Serialize;
5
6use opensession_api::*;
7
8/// Typed HTTP client for the OpenSession API.
9///
10/// Provides high-level methods for each API endpoint (using the stored auth
11/// token) and low-level `*_with_auth` methods for callers that need per-request
12/// auth (e.g. E2E tests exercising multiple users).
13pub struct ApiClient {
14    client: reqwest::Client,
15    base_url: String,
16    auth_token: Option<String>,
17}
18
19impl ApiClient {
20    /// Create a new client with the given base URL and timeout.
21    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    /// Create from an existing `reqwest::Client` (e.g. shared in tests).
31    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        self.auth_token = Some(token);
41    }
42
43    pub fn auth_token(&self) -> Option<&str> {
44        self.auth_token.as_deref()
45    }
46
47    pub fn base_url(&self) -> &str {
48        &self.base_url
49    }
50
51    /// Access the underlying `reqwest::Client`.
52    pub fn reqwest_client(&self) -> &reqwest::Client {
53        &self.client
54    }
55
56    fn url(&self, path: &str) -> String {
57        format!("{}/api{}", self.base_url, path)
58    }
59
60    fn token_or_bail(&self) -> Result<&str> {
61        self.auth_token
62            .as_deref()
63            .ok_or_else(|| anyhow::anyhow!("auth token not set"))
64    }
65
66    // ── Health ────────────────────────────────────────────────────────────
67
68    pub async fn health(&self) -> Result<HealthResponse> {
69        let resp = self.client.get(self.url("/health")).send().await?;
70        parse_response(resp).await
71    }
72
73    // ── Auth ──────────────────────────────────────────────────────────────
74
75    pub async fn login(&self, req: &LoginRequest) -> Result<AuthTokenResponse> {
76        let resp = self
77            .client
78            .post(self.url("/auth/login"))
79            .json(req)
80            .send()
81            .await?;
82        parse_response(resp).await
83    }
84
85    pub async fn register(&self, req: &AuthRegisterRequest) -> Result<AuthTokenResponse> {
86        let resp = self
87            .client
88            .post(self.url("/auth/register"))
89            .json(req)
90            .send()
91            .await?;
92        parse_response(resp).await
93    }
94
95    pub async fn verify(&self) -> Result<VerifyResponse> {
96        let token = self.token_or_bail()?;
97        let resp = self
98            .client
99            .post(self.url("/auth/verify"))
100            .bearer_auth(token)
101            .send()
102            .await?;
103        parse_response(resp).await
104    }
105
106    pub async fn me(&self) -> Result<UserSettingsResponse> {
107        let token = self.token_or_bail()?;
108        let resp = self
109            .client
110            .get(self.url("/auth/me"))
111            .bearer_auth(token)
112            .send()
113            .await?;
114        parse_response(resp).await
115    }
116
117    pub async fn refresh(&self, req: &RefreshRequest) -> Result<AuthTokenResponse> {
118        let resp = self
119            .client
120            .post(self.url("/auth/refresh"))
121            .json(req)
122            .send()
123            .await?;
124        parse_response(resp).await
125    }
126
127    pub async fn logout(&self, req: &LogoutRequest) -> Result<OkResponse> {
128        let token = self.token_or_bail()?;
129        let resp = self
130            .client
131            .post(self.url("/auth/logout"))
132            .bearer_auth(token)
133            .json(req)
134            .send()
135            .await?;
136        parse_response(resp).await
137    }
138
139    pub async fn change_password(&self, req: &ChangePasswordRequest) -> Result<OkResponse> {
140        let token = self.token_or_bail()?;
141        let resp = self
142            .client
143            .post(self.url("/auth/change-password"))
144            .bearer_auth(token)
145            .json(req)
146            .send()
147            .await?;
148        parse_response(resp).await
149    }
150
151    pub async fn regenerate_key(&self) -> Result<RegenerateKeyResponse> {
152        let token = self.token_or_bail()?;
153        let resp = self
154            .client
155            .post(self.url("/auth/regenerate-key"))
156            .bearer_auth(token)
157            .send()
158            .await?;
159        parse_response(resp).await
160    }
161
162    // ── Sessions ──────────────────────────────────────────────────────────
163
164    pub async fn upload_session(&self, req: &UploadRequest) -> Result<UploadResponse> {
165        let token = self.token_or_bail()?;
166        let resp = self
167            .client
168            .post(self.url("/sessions"))
169            .bearer_auth(token)
170            .json(req)
171            .send()
172            .await?;
173        parse_response(resp).await
174    }
175
176    pub async fn list_sessions(&self, query: &SessionListQuery) -> Result<SessionListResponse> {
177        let token = self.token_or_bail()?;
178        let mut url = self.url("/sessions");
179
180        // Build query string from the struct fields
181        let mut params = Vec::new();
182        params.push(format!("page={}", query.page));
183        params.push(format!("per_page={}", query.per_page));
184        if let Some(ref s) = query.search {
185            params.push(format!("search={s}"));
186        }
187        if let Some(ref t) = query.tool {
188            params.push(format!("tool={t}"));
189        }
190        if let Some(ref s) = query.sort {
191            params.push(format!("sort={s}"));
192        }
193        if let Some(ref r) = query.time_range {
194            params.push(format!("time_range={r}"));
195        }
196        if !params.is_empty() {
197            url = format!("{}?{}", url, params.join("&"));
198        }
199
200        let resp = self.client.get(&url).bearer_auth(token).send().await?;
201        parse_response(resp).await
202    }
203
204    pub async fn get_session(&self, id: &str) -> Result<SessionDetail> {
205        let token = self.token_or_bail()?;
206        let resp = self
207            .client
208            .get(self.url(&format!("/sessions/{id}")))
209            .bearer_auth(token)
210            .send()
211            .await?;
212        parse_response(resp).await
213    }
214
215    pub async fn delete_session(&self, id: &str) -> Result<OkResponse> {
216        let token = self.token_or_bail()?;
217        let resp = self
218            .client
219            .delete(self.url(&format!("/sessions/{id}")))
220            .bearer_auth(token)
221            .send()
222            .await?;
223        parse_response(resp).await
224    }
225
226    pub async fn get_session_raw(&self, id: &str) -> Result<serde_json::Value> {
227        let token = self.token_or_bail()?;
228        let resp = self
229            .client
230            .get(self.url(&format!("/sessions/{id}/raw")))
231            .bearer_auth(token)
232            .send()
233            .await?;
234        parse_response(resp).await
235    }
236
237    // ── Raw helpers (for E2E / advanced usage) ────────────────────────────
238
239    /// Authenticated GET returning the raw response.
240    pub async fn get_with_auth(&self, path: &str, token: &str) -> Result<reqwest::Response> {
241        Ok(self
242            .client
243            .get(self.url(path))
244            .bearer_auth(token)
245            .send()
246            .await?)
247    }
248
249    /// Authenticated POST (no body) returning the raw response.
250    pub async fn post_with_auth(&self, path: &str, token: &str) -> Result<reqwest::Response> {
251        Ok(self
252            .client
253            .post(self.url(path))
254            .bearer_auth(token)
255            .send()
256            .await?)
257    }
258
259    /// Authenticated POST with JSON body returning the raw response.
260    pub async fn post_json_with_auth<T: Serialize>(
261        &self,
262        path: &str,
263        token: &str,
264        body: &T,
265    ) -> Result<reqwest::Response> {
266        Ok(self
267            .client
268            .post(self.url(path))
269            .bearer_auth(token)
270            .json(body)
271            .send()
272            .await?)
273    }
274
275    /// Authenticated PUT with JSON body returning the raw response.
276    pub async fn put_json_with_auth<T: Serialize>(
277        &self,
278        path: &str,
279        token: &str,
280        body: &T,
281    ) -> Result<reqwest::Response> {
282        Ok(self
283            .client
284            .put(self.url(path))
285            .bearer_auth(token)
286            .json(body)
287            .send()
288            .await?)
289    }
290
291    /// Authenticated DELETE returning the raw response.
292    pub async fn delete_with_auth(&self, path: &str, token: &str) -> Result<reqwest::Response> {
293        Ok(self
294            .client
295            .delete(self.url(path))
296            .bearer_auth(token)
297            .send()
298            .await?)
299    }
300
301    /// Unauthenticated POST with JSON body returning the raw response.
302    pub async fn post_json_raw<T: Serialize>(
303        &self,
304        path: &str,
305        body: &T,
306    ) -> Result<reqwest::Response> {
307        Ok(self.client.post(self.url(path)).json(body).send().await?)
308    }
309}
310
311/// Parse an HTTP response: return the deserialized body on 2xx,
312/// or an error containing the status and body text.
313async fn parse_response<T: serde::de::DeserializeOwned>(resp: reqwest::Response) -> Result<T> {
314    let status = resp.status();
315    if !status.is_success() {
316        let body = resp.text().await.unwrap_or_default();
317        bail!("{status}: {body}");
318    }
319    Ok(resp.json().await?)
320}