use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tracing::{debug, error, info, warn};
use super::cache::MemoryTokenCache;
use super::token::{TokenInfo, TokenType};
use openlark_core::{
config::Config,
constants::{APP_ACCESS_TOKEN_URL_PATH, TENANT_ACCESS_TOKEN_URL_PATH},
error::CoreError,
SDKResult,
};
#[derive(Debug, Clone, serde::Deserialize)]
pub struct RefreshTokenResponse {
#[serde(rename = "app_access_token")]
pub app_access_token: Option<String>,
#[serde(rename = "tenant_access_token")]
pub tenant_access_token: Option<String>,
#[serde(rename = "refresh_token")]
pub refresh_token: Option<String>,
#[serde(rename = "expires_in")]
pub expires_in: u64,
#[serde(rename = "token_type")]
pub token_type: Option<String>,
}
pub struct TokenRefresher {
config: Config,
cache: Arc<MemoryTokenCache>,
refresh_config: super::token::TokenRefreshConfig,
}
impl TokenRefresher {
pub fn new(config: Config, cache: Arc<MemoryTokenCache>) -> Self {
Self {
config,
cache,
refresh_config: super::token::TokenRefreshConfig::default(),
}
}
pub fn with_refresh_config(mut self, config: super::token::TokenRefreshConfig) -> Self {
self.refresh_config = config;
self
}
pub async fn refresh_app_token(&self) -> SDKResult<TokenInfo> {
self.refresh_token_internal(TokenType::AppAccessToken, None)
.await
}
pub async fn refresh_tenant_token(&self, tenant_key: &str) -> SDKResult<TokenInfo> {
self.refresh_token_internal(TokenType::TenantAccessToken, Some(tenant_key))
.await
}
pub async fn refresh_user_token(&self, _refresh_token: &str) -> SDKResult<TokenInfo> {
self.refresh_token_internal(TokenType::UserAccessToken, None)
.await
}
pub async fn refresh_with_refresh_token(&self, refresh_token: &str) -> SDKResult<TokenInfo> {
let url = self.config.base_url().to_string() + APP_ACCESS_TOKEN_URL_PATH;
let mut params = std::collections::HashMap::new();
params.insert("grant_type".to_string(), "refresh_token".to_string());
params.insert("refresh_token".to_string(), refresh_token.to_string());
let request = self.config.http_client().post(&url);
let request = request.form(¶ms);
let response = request.send().await.map_err(CoreError::from)?;
let refresh_response: RefreshTokenResponse =
response.json().await.map_err(CoreError::from)?;
self.parse_refresh_response(refresh_response, TokenType::AppAccessToken)
}
async fn refresh_token_internal(
&self,
token_type: TokenType,
tenant_key: Option<&str>,
) -> SDKResult<TokenInfo> {
let url = self.config.base_url().to_string()
+ match token_type {
TokenType::AppAccessToken => APP_ACCESS_TOKEN_URL_PATH,
TokenType::TenantAccessToken => TENANT_ACCESS_TOKEN_URL_PATH,
TokenType::UserAccessToken => {
return Err(crate::error::validation_error(
"user_access_token",
"User token refresh not implemented",
));
}
};
debug!("Refreshing token of type: {:?}", token_type);
let mut request = self.config.http_client().post(&url);
request = request.basic_auth(self.config.app_id(), Some(self.config.app_secret()));
if let Some(tenant_key) = tenant_key {
request = request.header("x-tenant-key", tenant_key);
}
let response = request.send().await.map_err(CoreError::from)?;
let refresh_response: RefreshTokenResponse =
response.json().await.map_err(CoreError::from)?;
let token_info = self.parse_refresh_response(refresh_response, token_type)?;
let cache_key = self.generate_cache_key(token_info.token_type, tenant_key);
self.cache.put(&cache_key, token_info.clone()).await;
info!(
"Successfully refreshed {} token for tenant: {:?}",
token_type.as_str(),
tenant_key
);
Ok(token_info)
}
fn parse_refresh_response(
&self,
response: RefreshTokenResponse,
expected_type: TokenType,
) -> SDKResult<TokenInfo> {
let access_token = match expected_type {
TokenType::AppAccessToken => response.app_access_token,
TokenType::TenantAccessToken => response.tenant_access_token,
TokenType::UserAccessToken => {
return Err(crate::error::validation_error(
"user_access_token",
"User access token refresh not supported",
));
}
};
let access_token = access_token.ok_or_else(|| {
crate::error::validation_error("access_token", "Missing access_token")
})?;
let expires_in = response.expires_in;
let expires_at = SystemTime::now() + Duration::from_secs(expires_in);
let mut token_info = TokenInfo {
access_token,
refresh_token: response.refresh_token,
expires_at,
token_type: expected_type,
app_type: self.config.app_type().to_string(),
tenant_key: None,
created_at: SystemTime::now(),
last_accessed_at: SystemTime::now(),
access_count: 0,
};
if expected_type == TokenType::TenantAccessToken {
token_info.tenant_key = Some("default_tenant".to_string());
}
Ok(token_info)
}
fn generate_cache_key(&self, token_type: TokenType, tenant_key: Option<&str>) -> String {
match (token_type, tenant_key) {
(TokenType::AppAccessToken, None) => "app_access_token".to_string(),
(TokenType::TenantAccessToken, Some(tenant)) => {
format!("tenant_access_token:{}", tenant)
}
(TokenType::UserAccessToken, None) => "user_access_token".to_string(),
_ => "unknown_token".to_string(),
}
}
pub async fn should_refresh(&self, token_info: &TokenInfo) -> bool {
if token_info.is_expired() {
return true;
}
if let Some(_refresh_token) = &token_info.refresh_token {
let refresh_ahead = Duration::from_secs(self.refresh_config.refresh_ahead_seconds);
let time_until_expiry = token_info.time_until_expiry();
if let Some(remaining) = time_until_expiry {
remaining < refresh_ahead
} else {
false
}
} else {
false
}
}
pub async fn refresh_with_retry(
&self,
token_type: TokenType,
tenant_key: Option<&str>,
) -> SDKResult<TokenInfo> {
let max_attempts = self.refresh_config.max_retry_attempts;
let base_interval = Duration::from_secs(self.refresh_config.retry_interval_base);
let max_interval = Duration::from_secs(self.refresh_config.retry_interval_max);
for attempt in 1..=max_attempts {
match self.refresh_token_internal(token_type, tenant_key).await {
Ok(token) => {
if attempt > 1 {
info!("Token refresh succeeded on attempt {}", attempt);
}
return Ok(token);
}
Err(e) => {
if attempt == max_attempts {
error!(
"Token refresh failed after {} attempts: {}",
max_attempts, e
);
return Err(e);
} else {
warn!(
"Token refresh attempt {} failed, retrying...: {}",
attempt, e
);
let delay =
std::cmp::min(base_interval * 2_u32.pow(attempt - 1), max_interval);
tokio::time::sleep(delay).await;
}
}
}
}
Err(crate::error::network_error("Max retry attempts reached"))
}
pub async fn batch_refresh(
&self,
requests: Vec<(TokenType, Option<String>)>,
) -> Vec<SDKResult<TokenInfo>> {
let mut results = Vec::with_capacity(requests.len());
for (token_type, tenant_key) in requests {
let result = self
.refresh_token_internal(token_type, tenant_key.as_deref())
.await;
results.push(result);
}
results
}
pub async fn warmup_cache(&self, tenant_keys: Vec<String>) -> SDKResult<()> {
let _ = self.refresh_app_token().await;
let tenant_requests: Vec<_> = tenant_keys
.into_iter()
.map(|key| (TokenType::TenantAccessToken, Some(key)))
.collect();
let results = self.batch_refresh(tenant_requests).await;
let successful_count = results.iter().filter(|r| r.is_ok()).count();
info!(
"Cache warmup completed: {}/{} tokens refreshed successfully",
successful_count,
results.len()
);
Ok(())
}
}
#[cfg(test)]
#[allow(unused_imports)]
mod tests {
use super::*;
#[tokio::test]
async fn test_token_refresher_creation() {
}
#[tokio::test]
async fn test_should_refresh_logic() {
let expired_token = TokenInfo {
access_token: "test".to_string(),
refresh_token: Some("refresh".to_string()),
expires_at: SystemTime::now() - Duration::from_secs(1),
token_type: TokenType::AppAccessToken,
app_type: "test".to_string(),
tenant_key: None,
created_at: SystemTime::now() - Duration::from_secs(3600),
last_accessed_at: SystemTime::now(),
access_count: 10,
};
assert!(expired_token.is_expired()); }
}