use std::sync::Arc;
use std::time::Duration;
use chrono::{Duration as ChronoDuration, Utc};
use crate::config::SaTokenConfig;
use crate::dao::SaTokenDao;
use crate::error::{SaTokenError, SaTokenResult};
use crate::keys::LOGIN_TYPE_DEFAULT;
use crate::token::map::{
TOKEN_MAP_BE_REPLACED, TOKEN_MAP_KICK_OUT, is_kick_out_marker, is_replaced_marker,
};
use crate::token::{TokenInfo, TokenValue};
const TOKEN_ID_SEP: char = '\u{1}';
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TokenIdMapping {
Identity {
login_type: String,
login_id: String,
},
KickedOut,
Replaced,
}
pub struct TokenRepo {
dao: Arc<SaTokenDao>,
config: Arc<SaTokenConfig>,
}
impl std::fmt::Debug for TokenRepo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("TokenRepo { .. }")
}
}
impl TokenRepo {
pub fn new(dao: Arc<SaTokenDao>, config: Arc<SaTokenConfig>) -> Self {
Self { dao, config }
}
pub fn dao(&self) -> &Arc<SaTokenDao> {
&self.dao
}
fn ttl(&self) -> Option<Duration> {
self.dao.default_ttl()
}
pub async fn save_token_info(&self, info: &TokenInfo) -> SaTokenResult<()> {
let key = self.dao.keys().token_info(info.token.as_str());
self.dao.set_object(&key, info, self.ttl()).await
}
pub async fn get_token_info(&self, token: &str) -> SaTokenResult<Option<TokenInfo>> {
let key = self.dao.keys().token_info(token);
self.dao.get_object(&key).await
}
pub async fn delete_token_info(&self, token: &str) -> SaTokenResult<()> {
self.dao.delete(&self.dao.keys().token_info(token)).await
}
pub async fn get_login_mapping(
&self,
login_type: &str,
login_id: &str,
) -> SaTokenResult<Option<String>> {
let key = self.dao.keys().login_token(login_type, login_id);
self.dao.get_string(&key).await
}
pub async fn save_login_mapping(
&self,
login_type: &str,
login_id: &str,
token: &str,
) -> SaTokenResult<()> {
let key = self.dao.keys().login_token(login_type, login_id);
self.dao.set_string(&key, token, self.ttl()).await
}
pub async fn cas_login_mapping(
&self,
login_type: &str,
login_id: &str,
expected: Option<&str>,
new_token: &str,
) -> SaTokenResult<bool> {
let key = self.dao.keys().login_token(login_type, login_id);
self.dao.cas(&key, expected, new_token, self.ttl()).await
}
pub async fn delete_login_mapping(
&self,
login_type: &str,
login_id: &str,
) -> SaTokenResult<()> {
self.dao
.delete(&self.dao.keys().login_token(login_type, login_id))
.await
}
pub async fn cas_delete_login_mapping(
&self,
login_type: &str,
login_id: &str,
expected_token: &str,
) -> SaTokenResult<bool> {
let key = self.dao.keys().login_token(login_type, login_id);
self.dao.cas_delete(&key, expected_token).await
}
fn encode_token_id_value(login_type: &str, login_id: &str) -> String {
let mut s = String::with_capacity(login_type.len() + 1 + login_id.len());
s.push_str(login_type);
s.push(TOKEN_ID_SEP);
s.push_str(login_id);
s
}
fn parse_token_id_value(raw: &str) -> TokenIdMapping {
if is_kick_out_marker(raw) {
return TokenIdMapping::KickedOut;
}
if is_replaced_marker(raw) {
return TokenIdMapping::Replaced;
}
match raw.split_once(TOKEN_ID_SEP) {
Some((lt, lid)) => TokenIdMapping::Identity {
login_type: if lt.is_empty() {
LOGIN_TYPE_DEFAULT.to_string()
} else {
lt.to_string()
},
login_id: lid.to_string(),
},
None => TokenIdMapping::Identity {
login_type: LOGIN_TYPE_DEFAULT.to_string(),
login_id: raw.to_string(),
},
}
}
pub async fn save_token_id_mapping(
&self,
token: &str,
login_type: &str,
login_id: &str,
) -> SaTokenResult<()> {
let key = self.dao.keys().token_id_mapping(token);
let value = Self::encode_token_id_value(login_type, login_id);
self.dao.set_string(&key, &value, self.ttl()).await
}
pub async fn mark_token_id(&self, token: &str, marker: &str) -> SaTokenResult<()> {
let key = self.dao.keys().token_id_mapping(token);
self.dao.set_string(&key, marker, self.ttl()).await
}
pub async fn delete_token_id_mapping(&self, token: &str) -> SaTokenResult<()> {
self.dao
.delete(&self.dao.keys().token_id_mapping(token))
.await
}
pub async fn get_token_id_mapping(&self, token: &str) -> SaTokenResult<Option<TokenIdMapping>> {
let raw = self
.dao
.get_string(&self.dao.keys().token_id_mapping(token))
.await?;
Ok(raw.as_deref().map(Self::parse_token_id_value))
}
pub async fn check_mapping_marker(&self, token: &str) -> SaTokenResult<()> {
match self.get_token_id_mapping(token).await? {
Some(TokenIdMapping::KickedOut) => Err(SaTokenError::AccountKickedOut),
Some(TokenIdMapping::Replaced) => Err(SaTokenError::AccountReplaced),
_ => Ok(()),
}
}
fn index_key(&self, login_type: &str, login_id: &str) -> String {
self.dao.keys().login_token_index(login_type, login_id)
}
pub async fn append_index(
&self,
login_type: &str,
login_id: &str,
token: &str,
) -> SaTokenResult<()> {
let key = self.index_key(login_type, login_id);
self.dao.list_push_unique(&key, token, self.ttl()).await?;
Ok(())
}
pub async fn remove_index(
&self,
login_type: &str,
login_id: &str,
token: &str,
) -> SaTokenResult<bool> {
let key = self.index_key(login_type, login_id);
self.dao.list_remove(&key, token).await
}
pub async fn replace_index(
&self,
login_type: &str,
login_id: &str,
old_token: &str,
new_token: &str,
) -> SaTokenResult<()> {
if old_token == new_token {
return Ok(());
}
let _ = self.remove_index(login_type, login_id, old_token).await?;
self.append_index(login_type, login_id, new_token).await
}
pub async fn list_tokens(
&self,
login_type: &str,
login_id: &str,
) -> SaTokenResult<Vec<String>> {
let key = self.index_key(login_type, login_id);
self.dao.list_range(&key, 0, None).await
}
pub async fn prune_index(
&self,
login_type: &str,
login_id: &str,
) -> SaTokenResult<(Vec<String>, usize)> {
let key = self.index_key(login_type, login_id);
let tokens = self.dao.list_range(&key, 0, None).await?;
let mut alive = Vec::with_capacity(tokens.len());
let mut pruned = 0usize;
for t in tokens {
match self.get_token_info(&t).await {
Ok(Some(_)) => alive.push(t),
Ok(None) => {
let _ = self.dao.list_remove(&key, &t).await;
pruned += 1;
tracing::debug!(token = %t, "pruned orphan token from login index");
}
Err(e) => {
tracing::warn!(token = %t, error = %e, "index prune probe failed, keeping entry");
alive.push(t);
}
}
}
Ok((alive, pruned))
}
pub fn should_auto_renew(&self, info: &TokenInfo) -> bool {
if !self.config.auto_renew {
return false;
}
if self.config.renew_threshold < 0 {
return true;
}
match info.expire_time {
Some(expire) => {
let remaining = expire.signed_duration_since(Utc::now()).num_seconds();
remaining <= self.config.renew_threshold
}
None => self.config.active_timeout > 0,
}
}
pub async fn apply_auto_renew(
&self,
token: &str,
mut info: TokenInfo,
) -> SaTokenResult<TokenInfo> {
info.update_active_time();
let secs = self.dao.renew_secs();
let ttl = if secs > 0 {
info.expire_time = Some(Utc::now() + ChronoDuration::seconds(secs));
Some(Duration::from_secs(secs as u64))
} else {
None
};
let key = self.dao.keys().token_info(token);
self.dao.set_object(&key, &info, ttl).await?;
Ok(info)
}
pub async fn load_token_info_no_renew(&self, token: &TokenValue) -> SaTokenResult<TokenInfo> {
self.check_mapping_marker(token.as_str()).await?;
let info = self
.get_token_info(token.as_str())
.await?
.ok_or(SaTokenError::TokenNotFound)?;
if info.is_expired() {
return Err(SaTokenError::TokenExpired);
}
if info.is_freeze(info.effective_active_timeout(&self.config)) {
return Err(SaTokenError::TokenInactive);
}
Ok(info)
}
pub async fn load_valid_token_info(&self, token: &TokenValue) -> SaTokenResult<TokenInfo> {
let info = self.load_token_info_no_renew(token).await?;
if self.should_auto_renew(&info) {
return self.apply_auto_renew(token.as_str(), info).await;
}
Ok(info)
}
pub fn kick_out_marker(&self) -> &'static str {
TOKEN_MAP_KICK_OUT
}
pub fn replaced_marker(&self) -> &'static str {
TOKEN_MAP_BE_REPLACED
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::TokenStyle;
use sa_token_storage_memory::MemoryStorage;
fn repo(auto_renew: bool, renew_threshold: i64, timeout: i64) -> TokenRepo {
let config = Arc::new(SaTokenConfig {
auto_renew,
renew_threshold,
timeout,
active_timeout: -1,
token_style: TokenStyle::Uuid,
..Default::default()
});
let dao = Arc::new(crate::dao::SaTokenDao::new(
Arc::new(MemoryStorage::new()),
config.clone(),
));
TokenRepo::new(dao, config)
}
#[test]
fn should_auto_renew_false_when_disabled() {
let r = repo(false, -1, 3600);
let mut info = TokenInfo::new(TokenValue::new("t"), "u");
info.expire_time = Some(Utc::now() + ChronoDuration::seconds(10));
assert!(!r.should_auto_renew(&info));
}
#[test]
fn should_auto_renew_always_when_threshold_negative() {
let r = repo(true, -1, 3600);
let mut info = TokenInfo::new(TokenValue::new("t"), "u");
info.expire_time = Some(Utc::now() + ChronoDuration::seconds(3500));
assert!(r.should_auto_renew(&info));
}
#[test]
fn should_auto_renew_only_when_remaining_within_threshold() {
let r = repo(true, 300, 3600);
let mut far = TokenInfo::new(TokenValue::new("t1"), "u");
far.expire_time = Some(Utc::now() + ChronoDuration::seconds(3500));
assert!(!r.should_auto_renew(&far));
let mut near = TokenInfo::new(TokenValue::new("t2"), "u");
near.expire_time = Some(Utc::now() + ChronoDuration::seconds(200));
assert!(r.should_auto_renew(&near));
}
}