use std::sync::Arc;
use std::time::{Duration, Instant};
use reqwest::Client as HttpClient;
use serde::Serialize;
use tokio::sync::RwLock;
use crate::error::{Error, ErrorCode, Result};
use crate::models::{ApiResponse, TokenResponse};
pub const TOKEN_EXPIRY_SECONDS: u64 = 3600;
pub const TOKEN_EXPIRY_BUFFER: u64 = 60;
pub const AUTH_ENDPOINT: &str = "/dceapi/cms/auth/accessToken";
#[derive(Debug, Default)]
struct TokenState {
token: String,
expires_at: Option<Instant>,
}
#[derive(Debug, Serialize)]
struct AuthRequest {
secret: String,
}
#[derive(Debug)]
pub struct TokenManager {
api_key: String,
secret: String,
base_url: String,
http_client: HttpClient,
state: Arc<RwLock<TokenState>>,
}
impl TokenManager {
pub fn new(
api_key: impl Into<String>,
secret: impl Into<String>,
base_url: impl Into<String>,
http_client: HttpClient,
) -> Self {
TokenManager {
api_key: api_key.into(),
secret: secret.into(),
base_url: base_url.into(),
http_client,
state: Arc::new(RwLock::new(TokenState::default())),
}
}
pub async fn token(&self) -> Result<String> {
{
let state = self.state.read().await;
if !state.token.is_empty() && !self.is_expired_locked(&state) {
return Ok(state.token.clone());
}
}
self.refresh_and_get_token().await
}
pub async fn refresh(&self) -> Result<()> {
let mut state = self.state.write().await;
self.refresh_locked(&mut state).await
}
async fn refresh_and_get_token(&self) -> Result<String> {
let mut state = self.state.write().await;
if !state.token.is_empty() && !self.is_expired_locked(&state) {
return Ok(state.token.clone());
}
self.refresh_locked(&mut state).await?;
Ok(state.token.clone())
}
async fn refresh_locked(&self, state: &mut TokenState) -> Result<()> {
let auth_url = format!("{}{}", self.base_url, AUTH_ENDPOINT);
let req_body = AuthRequest {
secret: self.secret.clone(),
};
let response = self
.http_client
.post(&auth_url)
.header("Content-Type", "application/json")
.header("apikey", &self.api_key)
.json(&req_body)
.send()
.await
.map_err(|e| Error::auth(format!("failed to send auth request: {}", e)))?;
let resp_text = response
.text()
.await
.map_err(|e| Error::auth(format!("failed to read auth response: {}", e)))?;
let api_resp: ApiResponse = serde_json::from_str(&resp_text)
.map_err(|e| Error::auth(format!("failed to parse auth response: {}, body: {}", e, resp_text)))?;
if api_resp.code != ErrorCode::Success as i32 {
return Err(self.handle_auth_error(api_resp.code, &api_resp.msg));
}
let token_resp: TokenResponse = serde_json::from_value(api_resp.data)
.map_err(|e| Error::auth(format!("failed to parse token data: {}", e)))?;
if token_resp.access_token.is_empty() {
return Err(Error::auth("received empty access token"));
}
state.token = token_resp.access_token;
let expires_in = if token_resp.expires_in > 0 {
token_resp.expires_in as u64
} else {
TOKEN_EXPIRY_SECONDS
};
let effective_expiry = expires_in.saturating_sub(TOKEN_EXPIRY_BUFFER);
state.expires_at = Some(Instant::now() + Duration::from_secs(effective_expiry));
Ok(())
}
fn handle_auth_error(&self, code: i32, message: &str) -> Error {
match ErrorCode::from_code(code) {
Some(ErrorCode::ParamError) => Error::auth(format!("invalid parameters: {}", message)),
Some(ErrorCode::NoPermission) => Error::auth(format!("permission denied: {}", message)),
Some(ErrorCode::ServerError) => Error::auth(format!("server error: {}", message)),
Some(ErrorCode::RateLimit) => Error::auth(format!("rate limited: {}", message)),
_ => Error::auth(format!("authentication failed (code {}): {}", code, message)),
}
}
fn is_expired_locked(&self, state: &TokenState) -> bool {
if state.token.is_empty() {
return true;
}
match state.expires_at {
Some(expires_at) => Instant::now() >= expires_at,
None => true,
}
}
pub async fn is_expired(&self) -> bool {
let state = self.state.read().await;
self.is_expired_locked(&state)
}
pub async fn clear_token(&self) {
let mut state = self.state.write().await;
state.token.clear();
state.expires_at = None;
}
pub async fn get_cached_token(&self) -> String {
let state = self.state.read().await;
state.token.clone()
}
}