use std::time::Duration;
use anyhow::{bail, Result};
use serde::Serialize;
use opensession_api::*;
pub struct ApiClient {
client: reqwest::Client,
base_url: String,
auth_token: Option<String>,
}
impl ApiClient {
pub fn new(base_url: &str, timeout: Duration) -> Result<Self> {
let client = reqwest::Client::builder().timeout(timeout).build()?;
Ok(Self {
client,
base_url: base_url.trim_end_matches('/').to_string(),
auth_token: None,
})
}
pub fn with_client(client: reqwest::Client, base_url: &str) -> Self {
Self {
client,
base_url: base_url.trim_end_matches('/').to_string(),
auth_token: None,
}
}
pub fn set_auth(&mut self, token: String) {
let normalized = token.trim();
if normalized.is_empty() {
self.auth_token = None;
return;
}
self.auth_token = Some(normalized.to_string());
}
pub fn auth_token(&self) -> Option<&str> {
self.auth_token.as_deref()
}
pub fn base_url(&self) -> &str {
&self.base_url
}
pub fn reqwest_client(&self) -> &reqwest::Client {
&self.client
}
fn url(&self, path: &str) -> String {
format!("{}/api{}", self.base_url, path)
}
fn token_or_bail(&self) -> Result<&str> {
self.auth_token
.as_deref()
.ok_or_else(|| anyhow::anyhow!("auth token not set"))
}
pub async fn health(&self) -> Result<HealthResponse> {
let resp = self.client.get(self.url("/health")).send().await?;
parse_response(resp).await
}
pub async fn login(&self, req: &LoginRequest) -> Result<AuthTokenResponse> {
let resp = self
.client
.post(self.url("/auth/login"))
.json(req)
.send()
.await?;
parse_response(resp).await
}
pub async fn register(&self, req: &AuthRegisterRequest) -> Result<AuthTokenResponse> {
let resp = self
.client
.post(self.url("/auth/register"))
.json(req)
.send()
.await?;
parse_response(resp).await
}
pub async fn verify(&self) -> Result<VerifyResponse> {
let token = self.token_or_bail()?;
let resp = self
.client
.post(self.url("/auth/verify"))
.bearer_auth(token)
.send()
.await?;
parse_response(resp).await
}
pub async fn me(&self) -> Result<UserSettingsResponse> {
let token = self.token_or_bail()?;
let resp = self
.client
.get(self.url("/auth/me"))
.bearer_auth(token)
.send()
.await?;
parse_response(resp).await
}
pub async fn refresh(&self, req: &RefreshRequest) -> Result<AuthTokenResponse> {
let resp = self
.client
.post(self.url("/auth/refresh"))
.json(req)
.send()
.await?;
parse_response(resp).await
}
pub async fn logout(&self, req: &LogoutRequest) -> Result<OkResponse> {
let token = self.token_or_bail()?;
let resp = self
.client
.post(self.url("/auth/logout"))
.bearer_auth(token)
.json(req)
.send()
.await?;
parse_response(resp).await
}
pub async fn change_password(&self, req: &ChangePasswordRequest) -> Result<OkResponse> {
let token = self.token_or_bail()?;
let resp = self
.client
.post(self.url("/auth/change-password"))
.bearer_auth(token)
.json(req)
.send()
.await?;
parse_response(resp).await
}
pub async fn issue_api_key(&self) -> Result<IssueApiKeyResponse> {
let token = self.token_or_bail()?;
let resp = self
.client
.post(self.url("/auth/api-keys/issue"))
.bearer_auth(token)
.send()
.await?;
parse_response(resp).await
}
pub async fn list_sessions(&self, query: &SessionListQuery) -> Result<SessionListResponse> {
let token = self.token_or_bail()?;
let mut url = self.url("/sessions");
let mut params = Vec::new();
params.push(format!("page={}", query.page));
params.push(format!("per_page={}", query.per_page));
if let Some(ref s) = query.search {
params.push(format!("search={s}"));
}
if let Some(ref t) = query.tool {
params.push(format!("tool={t}"));
}
if let Some(ref s) = query.sort {
params.push(format!("sort={s}"));
}
if let Some(ref r) = query.time_range {
params.push(format!("time_range={r}"));
}
if !params.is_empty() {
url = format!("{}?{}", url, params.join("&"));
}
let resp = self.client.get(&url).bearer_auth(token).send().await?;
parse_response(resp).await
}
pub async fn get_session(&self, id: &str) -> Result<SessionDetail> {
let token = self.token_or_bail()?;
let resp = self
.client
.get(self.url(&format!("/sessions/{id}")))
.bearer_auth(token)
.send()
.await?;
parse_response(resp).await
}
pub async fn delete_session(&self, id: &str) -> Result<OkResponse> {
let token = self.token_or_bail()?;
let resp = self
.client
.delete(self.url(&format!("/sessions/{id}")))
.bearer_auth(token)
.send()
.await?;
parse_response(resp).await
}
pub async fn get_session_raw(&self, id: &str) -> Result<serde_json::Value> {
let token = self.token_or_bail()?;
let resp = self
.client
.get(self.url(&format!("/sessions/{id}/raw")))
.bearer_auth(token)
.send()
.await?;
parse_response(resp).await
}
pub async fn get_with_auth(&self, path: &str, token: &str) -> Result<reqwest::Response> {
Ok(self
.client
.get(self.url(path))
.bearer_auth(token)
.send()
.await?)
}
pub async fn post_with_auth(&self, path: &str, token: &str) -> Result<reqwest::Response> {
Ok(self
.client
.post(self.url(path))
.bearer_auth(token)
.send()
.await?)
}
pub async fn post_json_with_auth<T: Serialize>(
&self,
path: &str,
token: &str,
body: &T,
) -> Result<reqwest::Response> {
Ok(self
.client
.post(self.url(path))
.bearer_auth(token)
.json(body)
.send()
.await?)
}
pub async fn put_json_with_auth<T: Serialize>(
&self,
path: &str,
token: &str,
body: &T,
) -> Result<reqwest::Response> {
Ok(self
.client
.put(self.url(path))
.bearer_auth(token)
.json(body)
.send()
.await?)
}
pub async fn delete_with_auth(&self, path: &str, token: &str) -> Result<reqwest::Response> {
Ok(self
.client
.delete(self.url(path))
.bearer_auth(token)
.send()
.await?)
}
pub async fn post_json_raw<T: Serialize>(
&self,
path: &str,
body: &T,
) -> Result<reqwest::Response> {
Ok(self.client.post(self.url(path)).json(body).send().await?)
}
}
async fn parse_response<T: serde::de::DeserializeOwned>(resp: reqwest::Response) -> Result<T> {
let status = resp.status();
if !status.is_success() {
let body = resp.text().await.unwrap_or_default();
bail!("{status}: {body}");
}
Ok(resp.json().await?)
}
#[cfg(test)]
mod tests {
use super::ApiClient;
use std::time::Duration;
#[test]
fn set_auth_trims_surrounding_whitespace() {
let mut client = ApiClient::new("https://example.com", Duration::from_secs(1))
.expect("client should construct");
client.set_auth(" osk_test_token ".to_string());
assert_eq!(client.auth_token(), Some("osk_test_token"));
}
#[test]
fn set_auth_clears_auth_for_blank_tokens() {
let mut client = ApiClient::new("https://example.com", Duration::from_secs(1))
.expect("client should construct");
client.set_auth("osk_test_token".to_string());
assert_eq!(client.auth_token(), Some("osk_test_token"));
client.set_auth(" ".to_string());
assert_eq!(client.auth_token(), None);
}
}