use std::time::Duration;
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
use tokio::sync::Mutex;
use crate::config::QPayConfig;
use crate::error::{ApiErrorBody, QPayError};
use crate::models::TokenResponse;
const TOKEN_BUFFER_SECONDS: i64 = 30;
pub(crate) struct TokenState {
pub(crate) access_token: String,
pub(crate) refresh_token: String,
pub(crate) expires_at: i64,
pub(crate) refresh_expires_at: i64,
}
impl Default for TokenState {
fn default() -> Self {
Self {
access_token: String::new(),
refresh_token: String::new(),
expires_at: 0,
refresh_expires_at: 0,
}
}
}
pub struct QPayClient {
pub(crate) config: QPayConfig,
pub(crate) http: reqwest::Client,
pub(crate) token_state: Mutex<TokenState>,
}
impl QPayClient {
pub fn new(config: QPayConfig) -> Self {
let http = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.build()
.expect("failed to build reqwest client");
Self {
config,
http,
token_state: Mutex::new(TokenState::default()),
}
}
pub fn with_http_client(config: QPayConfig, http: reqwest::Client) -> Self {
Self {
config,
http,
token_state: Mutex::new(TokenState::default()),
}
}
pub(crate) async fn ensure_token(&self) -> Result<(), QPayError> {
let now = chrono_now();
let (needs_refresh, can_refresh, refresh_tok) = {
let state = self.token_state.lock().await;
let token_valid =
!state.access_token.is_empty() && now < state.expires_at - TOKEN_BUFFER_SECONDS;
if token_valid {
return Ok(());
}
let can_refresh = !state.refresh_token.is_empty()
&& now < state.refresh_expires_at - TOKEN_BUFFER_SECONDS;
(true, can_refresh, state.refresh_token.clone())
};
if needs_refresh && can_refresh {
if let Ok(token) = self.do_refresh_token_http(&refresh_tok).await {
let mut state = self.token_state.lock().await;
store_token(&mut state, &token);
return Ok(());
}
}
let token = self
.get_token_request()
.await
.map_err(|e| QPayError::Token(e.to_string()))?;
let mut state = self.token_state.lock().await;
store_token(&mut state, &token);
Ok(())
}
pub(crate) async fn do_refresh_token_http(
&self,
refresh_tok: &str,
) -> Result<TokenResponse, QPayError> {
let url = format!("{}/v2/auth/refresh", self.config.base_url);
let resp = self
.http
.post(&url)
.header(AUTHORIZATION, format!("Bearer {}", refresh_tok))
.send()
.await?;
let status = resp.status();
let body = resp.text().await?;
if !status.is_success() {
let api_err = serde_json::from_str::<ApiErrorBody>(&body).unwrap_or_default();
return Err(QPayError::Api {
status_code: status.as_u16(),
code: api_err.code,
message: api_err.message,
raw_body: body,
});
}
let token: TokenResponse = serde_json::from_str(&body)?;
Ok(token)
}
pub(crate) async fn get_token_request(&self) -> Result<TokenResponse, QPayError> {
let url = format!("{}/v2/auth/token", self.config.base_url);
let resp = self
.http
.post(&url)
.basic_auth(&self.config.username, Some(&self.config.password))
.send()
.await?;
let status = resp.status();
let body = resp.text().await?;
if !status.is_success() {
let api_err = serde_json::from_str::<ApiErrorBody>(&body).unwrap_or_default();
let code = if api_err.code.is_empty() {
status
.canonical_reason()
.unwrap_or("Unknown")
.to_string()
} else {
api_err.code
};
let message = if api_err.message.is_empty() {
body.clone()
} else {
api_err.message
};
return Err(QPayError::Api {
status_code: status.as_u16(),
code,
message,
raw_body: body,
});
}
let token: TokenResponse = serde_json::from_str(&body)?;
Ok(token)
}
pub(crate) async fn store_token_response(&self, token: &TokenResponse) {
let mut state = self.token_state.lock().await;
store_token(&mut state, token);
}
pub(crate) async fn do_request<B: serde::Serialize, R: serde::de::DeserializeOwned>(
&self,
method: reqwest::Method,
path: &str,
body: Option<&B>,
) -> Result<R, QPayError> {
self.ensure_token().await?;
let url = format!("{}{}", self.config.base_url, path);
let access_token = {
let state = self.token_state.lock().await;
state.access_token.clone()
};
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {}", access_token))
.map_err(|e| QPayError::Config(e.to_string()))?,
);
let mut request = self.http.request(method, &url).headers(headers);
if let Some(b) = body {
request = request.json(b);
}
let resp = request.send().await?;
let status = resp.status();
let resp_body = resp.text().await?;
if !status.is_success() {
let api_err = serde_json::from_str::<ApiErrorBody>(&resp_body).unwrap_or_default();
let code = if api_err.code.is_empty() {
status
.canonical_reason()
.unwrap_or("Unknown")
.to_string()
} else {
api_err.code
};
let message = if api_err.message.is_empty() {
resp_body.clone()
} else {
api_err.message
};
return Err(QPayError::Api {
status_code: status.as_u16(),
code,
message,
raw_body: resp_body,
});
}
let result: R = serde_json::from_str(&resp_body)?;
Ok(result)
}
pub(crate) async fn do_request_no_response<B: serde::Serialize>(
&self,
method: reqwest::Method,
path: &str,
body: Option<&B>,
) -> Result<(), QPayError> {
self.ensure_token().await?;
let url = format!("{}{}", self.config.base_url, path);
let access_token = {
let state = self.token_state.lock().await;
state.access_token.clone()
};
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {}", access_token))
.map_err(|e| QPayError::Config(e.to_string()))?,
);
let mut request = self.http.request(method, &url).headers(headers);
if let Some(b) = body {
request = request.json(b);
}
let resp = request.send().await?;
let status = resp.status();
let resp_body = resp.text().await?;
if !status.is_success() {
let api_err = serde_json::from_str::<ApiErrorBody>(&resp_body).unwrap_or_default();
let code = if api_err.code.is_empty() {
status
.canonical_reason()
.unwrap_or("Unknown")
.to_string()
} else {
api_err.code
};
let message = if api_err.message.is_empty() {
resp_body.clone()
} else {
api_err.message
};
return Err(QPayError::Api {
status_code: status.as_u16(),
code,
message,
raw_body: resp_body,
});
}
Ok(())
}
}
fn store_token(state: &mut TokenState, token: &TokenResponse) {
state.access_token = token.access_token.clone();
state.refresh_token = token.refresh_token.clone();
state.expires_at = token.expires_in;
state.refresh_expires_at = token.refresh_expires_in;
}
fn chrono_now() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64
}