use std::sync::Arc;
use chrono::{DateTime, Utc};
use sa_token_adapter::storage::SaStorage;
use crate::config::SaTokenConfig;
use crate::dao::SaTokenDao;
use crate::distributed::DistributedSessionManager;
use crate::error::{SaTokenError, SaTokenResult};
use crate::event::SaTokenEventBus;
use crate::keys::{AccountNs, LOGIN_TYPE_DEFAULT, LoginId, SaKeyLayout, SaKeys};
use crate::online::OnlineManager;
use crate::permission::PermissionMatcher;
use crate::repository::{GrantRepo, SessionRepo, TokenRepo};
use crate::service::{AuthService, AuthzService, LoginRequest};
use crate::session::SaSession;
use crate::stp_interface::StpInterface;
use crate::token::{TokenInfo, TokenValue};
#[derive(Clone)]
pub struct SaTokenManager {
pub(crate) storage: Arc<dyn SaStorage>,
pub config: Arc<SaTokenConfig>,
keys: SaKeys,
pub(crate) event_bus: SaTokenEventBus,
pub(crate) dao: Arc<SaTokenDao>,
token_repo: Arc<TokenRepo>,
session_repo: Arc<SessionRepo>,
grant_repo: Arc<GrantRepo>,
authz_service: Arc<AuthzService>,
auth_service: Arc<AuthService>,
perm_matcher: Option<Arc<dyn PermissionMatcher>>,
role_matcher: Option<Arc<dyn PermissionMatcher>>,
online_manager: Option<Arc<OnlineManager>>,
distributed_manager: Option<Arc<DistributedSessionManager>>,
pub(crate) stp_interface: Option<Arc<dyn StpInterface>>,
}
impl std::fmt::Debug for SaTokenManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("SaTokenManager { .. }")
}
}
impl SaTokenManager {
pub fn new(storage: Arc<dyn SaStorage>, config: SaTokenConfig) -> Self {
let config = Arc::new(config);
let keys = SaKeys::from_config(&config);
let dao = Arc::new(SaTokenDao::new(storage.clone(), config.clone()));
let event_bus = SaTokenEventBus::new();
let token_repo = Arc::new(TokenRepo::new(dao.clone(), config.clone()));
let session_repo = Arc::new(SessionRepo::new(dao.clone(), config.clone()));
let grant_repo = Arc::new(GrantRepo::new(dao.clone()));
let authz_service = Arc::new(AuthzService::new(
grant_repo.clone(),
&config,
event_bus.clone(),
None,
));
let auth_service = Arc::new(AuthService::new(
dao.clone(),
config.clone(),
token_repo.clone(),
session_repo.clone(),
event_bus.clone(),
None,
None,
));
Self {
storage,
config,
keys,
event_bus,
dao,
token_repo,
session_repo,
grant_repo,
authz_service,
auth_service,
perm_matcher: None,
role_matcher: None,
online_manager: None,
distributed_manager: None,
stp_interface: None,
}
}
fn rebuild_services(&mut self) {
self.grant_repo = Arc::new(GrantRepo::new(self.dao.clone()));
let mut authz = AuthzService::new(
self.grant_repo.clone(),
&self.config,
self.event_bus.clone(),
self.stp_interface.clone(),
);
if let Some(matcher) = self.perm_matcher.clone() {
authz = authz.with_permission_matcher(matcher);
}
if let Some(matcher) = self.role_matcher.clone() {
authz = authz.with_role_matcher(matcher);
}
self.authz_service = Arc::new(authz);
self.auth_service = Arc::new(AuthService::new(
self.dao.clone(),
self.config.clone(),
self.token_repo.clone(),
self.session_repo.clone(),
self.event_bus.clone(),
self.online_manager.clone(),
self.distributed_manager.clone(),
));
}
fn rebuild_config_chain(&mut self) {
self.keys = SaKeys::from_config(&self.config);
self.dao = Arc::new(SaTokenDao::new(self.storage.clone(), self.config.clone()));
self.token_repo = Arc::new(TokenRepo::new(self.dao.clone(), self.config.clone()));
self.session_repo = Arc::new(SessionRepo::new(self.dao.clone(), self.config.clone()));
self.rebuild_services();
}
pub fn with_key_layout(mut self, layout: SaKeyLayout) -> Self {
Arc::make_mut(&mut self.config).key_layout = layout;
self.rebuild_config_chain();
self
}
pub fn with_serializer(
mut self,
serializer: sa_token_adapter::serializer::SharedSerializer,
) -> Self {
Arc::make_mut(&mut self.config).serializer = serializer;
self.rebuild_config_chain();
self
}
pub fn with_stp_interface(mut self, iface: Arc<dyn StpInterface>) -> Self {
self.stp_interface = Some(iface);
self.rebuild_services();
self
}
pub fn with_permission_matcher(mut self, matcher: Arc<dyn PermissionMatcher>) -> Self {
self.perm_matcher = Some(matcher);
self.rebuild_services();
self
}
pub fn with_role_matcher(mut self, matcher: Arc<dyn PermissionMatcher>) -> Self {
self.role_matcher = Some(matcher);
self.rebuild_services();
self
}
pub fn with_online_manager(mut self, manager: Arc<OnlineManager>) -> Self {
self.online_manager = Some(manager);
self.rebuild_services();
self
}
pub fn with_distributed_manager(mut self, manager: Arc<DistributedSessionManager>) -> Self {
self.distributed_manager = Some(manager);
self.rebuild_services();
self
}
pub fn with_distributed_online(mut self) -> Self {
self.online_manager = Some(Arc::new(OnlineManager::distributed(self.dao.clone())));
self.rebuild_services();
self
}
pub fn start_background_cleanup(
&self,
config: crate::cleanup::CleanupConfig,
) -> crate::cleanup::BackgroundCleanupTask {
let nonce = Arc::new(crate::nonce::NonceManager::from_dao(
self.dao.clone(),
if self.config.nonce_timeout > 0 {
self.config.nonce_timeout
} else {
60
},
));
crate::cleanup::BackgroundCleanupTask::spawn(
config,
Some(nonce),
self.online_manager.clone(),
)
}
pub fn with_event_bus(mut self, event_bus: SaTokenEventBus) -> Self {
self.event_bus = event_bus.clone();
self.rebuild_services();
self
}
#[inline]
pub fn keys(&self) -> &SaKeys {
&self.keys
}
pub fn storage(&self) -> &Arc<dyn SaStorage> {
&self.storage
}
pub fn dao(&self) -> &Arc<SaTokenDao> {
&self.dao
}
pub fn serializer(&self) -> &sa_token_adapter::serializer::SharedSerializer {
&self.config.serializer
}
pub fn auth_service(&self) -> &Arc<AuthService> {
&self.auth_service
}
pub fn token_repo(&self) -> &Arc<TokenRepo> {
&self.token_repo
}
pub fn session_repo(&self) -> &Arc<SessionRepo> {
&self.session_repo
}
pub fn authz_service(&self) -> &Arc<AuthzService> {
&self.authz_service
}
#[deprecated(
since = "0.2.0",
note = "Use SaTokenManager::authz_service() so cache invalidation and StpInterface precedence are honoured"
)]
pub fn grant_repo(&self) -> &Arc<GrantRepo> {
&self.grant_repo
}
pub fn event_bus(&self) -> &SaTokenEventBus {
&self.event_bus
}
pub fn online_manager(&self) -> Option<&Arc<OnlineManager>> {
self.online_manager.as_ref()
}
pub fn distributed_manager(&self) -> Option<&Arc<DistributedSessionManager>> {
self.distributed_manager.as_ref()
}
pub(crate) fn account_ns(&self, login_type: &str, login_id: &str) -> AccountNs {
SaKeys::account_ns(login_type, &LoginId::new(login_id))
}
pub async fn login(&self, login_id: impl Into<String>) -> SaTokenResult<TokenValue> {
self.auth_service.login(LoginRequest::new(login_id)).await
}
pub async fn login_with_options(
&self,
login_id: impl Into<String>,
login_type: Option<String>,
device: Option<String>,
extra_data: Option<serde_json::Value>,
nonce: Option<String>,
expire_time: Option<DateTime<Utc>>,
) -> SaTokenResult<TokenValue> {
let mut req = LoginRequest::new(login_id);
if let Some(lt) = login_type {
req = req.login_type(lt);
}
if let Some(d) = device {
req = req.device(d);
}
if let Some(e) = extra_data {
req = req.extra_data(e);
}
if let Some(n) = nonce {
req = req.nonce(n);
}
if let Some(t) = expire_time {
req = req.expire_time(t);
}
self.auth_service.login(req).await
}
pub async fn login_with_token_info(&self, token_info: TokenInfo) -> SaTokenResult<TokenValue> {
let mut req = LoginRequest::new(token_info.login_id.as_ref())
.login_type(token_info.login_type.as_ref());
if let Some(d) = token_info.device.clone() {
req = req.device(d);
}
if let Some(e) = token_info.extra_data.clone() {
req = req.extra_data(e);
}
if let Some(n) = token_info.nonce.clone() {
req = req.nonce(n);
}
if let Some(t) = token_info.expire_time {
req = req.expire_time(t);
}
if !token_info.token.as_str().is_empty() {
req = req.preset_token(token_info.token.as_str());
}
self.auth_service.login(req).await
}
pub async fn logout(&self, token: &TokenValue) -> SaTokenResult<()> {
self.auth_service
.logout(token, self.config.is_logout_keep_token_session)
.await
}
pub async fn kick_out_by_token(&self, token: &TokenValue) -> SaTokenResult<()> {
self.auth_service
.kick_out_by_token(token, self.config.is_logout_keep_token_session)
.await
}
pub async fn replaced_by_token(&self, token: &TokenValue) -> SaTokenResult<()> {
self.auth_service.logout_replaced(token).await
}
pub async fn logout_by_login_id(&self, login_type: &str, login_id: &str) -> SaTokenResult<()> {
self.auth_service
.logout_by_login_id(login_type, login_id)
.await
}
pub async fn kick_out(&self, login_type: &str, login_id: &str) -> SaTokenResult<()> {
self.auth_service.kick_out(login_type, login_id).await
}
pub async fn get_token_info(&self, token: &TokenValue) -> SaTokenResult<TokenInfo> {
self.auth_service.get_token_info(token).await
}
pub async fn get_token_by_login_id(
&self,
login_type: &str,
login_id: &str,
) -> SaTokenResult<TokenValue> {
match self
.token_repo()
.get_login_mapping(login_type, login_id)
.await?
{
Some(token_str) => Ok(TokenValue::new(token_str)),
None => Err(SaTokenError::NotLogin),
}
}
pub async fn get_all_tokens_by_login_id(
&self,
login_type: &str,
login_id: &str,
) -> SaTokenResult<Vec<TokenValue>> {
let tokens = self.token_repo().list_tokens(login_type, login_id).await?;
Ok(tokens.into_iter().map(TokenValue::new).collect())
}
pub async fn update_extra_data(
&self,
token: &TokenValue,
extra_data: serde_json::Value,
) -> SaTokenResult<()> {
let mut token_info = self.get_token_info(token).await?;
token_info.extra_data = Some(extra_data);
self.token_repo().save_token_info(&token_info).await
}
pub async fn update_active_timeout(
&self,
token: &TokenValue,
seconds: i64,
) -> SaTokenResult<()> {
if !self.config.dynamic_active_timeout {
return Err(SaTokenError::ConfigError(
"dynamic_active_timeout is disabled".into(),
));
}
let mut info = self.get_token_info(token).await?;
info.active_timeout_override = Some(seconds);
self.token_repo().save_token_info(&info).await
}
pub fn stp_logic(&self, login_type: &str) -> crate::stp_logic::SaLogic {
crate::stp_logic::SaLogic::new(login_type, self.clone())
}
pub async fn is_valid(&self, token: &TokenValue) -> bool {
self.auth_service.is_valid(token).await
}
pub async fn renew_timeout(
&self,
token: &TokenValue,
timeout_seconds: i64,
) -> SaTokenResult<()> {
self.auth_service
.renew_timeout(token, timeout_seconds)
.await
}
pub async fn get_session(&self, login_id: &str) -> SaTokenResult<SaSession> {
self.session_repo
.get_account_session(LOGIN_TYPE_DEFAULT, login_id)
.await
}
pub async fn get_session_with_type(
&self,
login_type: &str,
login_id: &str,
) -> SaTokenResult<SaSession> {
self.session_repo
.get_account_session(login_type, login_id)
.await
}
pub async fn save_session(&self, session: &SaSession) -> SaTokenResult<()> {
self.session_repo.save_session_object(session).await
}
pub async fn save_session_with_type(
&self,
login_type: &str,
login_id: &str,
session: &SaSession,
) -> SaTokenResult<()> {
self.session_repo
.save_account_session(login_type, login_id, session)
.await
}
pub async fn delete_session(&self, login_id: &str) -> SaTokenResult<()> {
self.session_repo
.delete_account_session(LOGIN_TYPE_DEFAULT, login_id)
.await
}
pub async fn delete_session_with_type(
&self,
login_type: &str,
login_id: &str,
) -> SaTokenResult<()> {
self.session_repo
.delete_account_session(login_type, login_id)
.await
}
pub async fn get_terminal_list(
&self,
login_type: &str,
login_id: &str,
device_type: Option<&str>,
) -> SaTokenResult<Vec<crate::session::SaTerminalInfo>> {
let ns = self.account_ns(login_type, login_id);
self.session_repo.get_terminal_list(&ns, device_type).await
}
pub async fn get_token_value_list_by_login_id(
&self,
login_type: &str,
login_id: &str,
device_type: Option<&str>,
) -> SaTokenResult<Vec<String>> {
let ns = self.account_ns(login_type, login_id);
self.session_repo.get_token_list(&ns, device_type).await
}
pub async fn get_terminal_info_by_token(
&self,
token: &TokenValue,
) -> SaTokenResult<Option<crate::session::SaTerminalInfo>> {
let Ok(info) = self.get_token_info(token).await else {
return Ok(None);
};
let ns = self.account_ns(&info.login_type, &info.login_id);
self.session_repo.get_terminal(&ns, token.as_str()).await
}
pub async fn get_permissions_with_type(
&self,
login_type: &str,
login_id: &str,
) -> SaTokenResult<Vec<String>> {
self.authz_service
.get_permissions(login_type, login_id)
.await
}
pub async fn set_permissions_with_type(
&self,
login_type: &str,
login_id: &str,
permissions: Vec<String>,
) -> SaTokenResult<()> {
self.authz_service
.set_permissions(login_type, login_id, &permissions)
.await
}
pub async fn add_permission_with_type(
&self,
login_type: &str,
login_id: &str,
permission: String,
) -> SaTokenResult<()> {
self.authz_service
.add_permission(login_type, login_id, permission)
.await
}
pub async fn remove_permission_with_type(
&self,
login_type: &str,
login_id: &str,
permission: &str,
) -> SaTokenResult<()> {
self.authz_service
.remove_permission(login_type, login_id, permission)
.await
}
pub async fn clear_permissions_with_type(
&self,
login_type: &str,
login_id: &str,
) -> SaTokenResult<()> {
self.authz_service
.clear_permissions(login_type, login_id)
.await
}
pub async fn get_roles_with_type(
&self,
login_type: &str,
login_id: &str,
) -> SaTokenResult<Vec<String>> {
self.authz_service.get_roles(login_type, login_id).await
}
pub async fn set_roles_with_type(
&self,
login_type: &str,
login_id: &str,
roles: Vec<String>,
) -> SaTokenResult<()> {
self.authz_service
.set_roles(login_type, login_id, &roles)
.await
}
pub async fn add_role_with_type(
&self,
login_type: &str,
login_id: &str,
role: String,
) -> SaTokenResult<()> {
self.authz_service
.add_role(login_type, login_id, role)
.await
}
pub async fn remove_role_with_type(
&self,
login_type: &str,
login_id: &str,
role: &str,
) -> SaTokenResult<()> {
self.authz_service
.remove_role(login_type, login_id, role)
.await
}
pub async fn clear_roles_with_type(
&self,
login_type: &str,
login_id: &str,
) -> SaTokenResult<()> {
self.authz_service.clear_roles(login_type, login_id).await
}
pub async fn get_permissions(&self, login_id: &str) -> SaTokenResult<Vec<String>> {
self.get_permissions_with_type(LOGIN_TYPE_DEFAULT, login_id)
.await
}
pub async fn set_permissions(
&self,
login_id: &str,
permissions: Vec<String>,
) -> SaTokenResult<()> {
self.set_permissions_with_type(LOGIN_TYPE_DEFAULT, login_id, permissions)
.await
}
pub async fn add_permission(&self, login_id: &str, permission: String) -> SaTokenResult<()> {
self.add_permission_with_type(LOGIN_TYPE_DEFAULT, login_id, permission)
.await
}
pub async fn remove_permission(&self, login_id: &str, permission: &str) -> SaTokenResult<()> {
self.remove_permission_with_type(LOGIN_TYPE_DEFAULT, login_id, permission)
.await
}
pub async fn clear_permissions(&self, login_id: &str) -> SaTokenResult<()> {
self.clear_permissions_with_type(LOGIN_TYPE_DEFAULT, login_id)
.await
}
pub async fn get_roles(&self, login_id: &str) -> SaTokenResult<Vec<String>> {
self.get_roles_with_type(LOGIN_TYPE_DEFAULT, login_id).await
}
pub async fn set_roles(&self, login_id: &str, roles: Vec<String>) -> SaTokenResult<()> {
self.set_roles_with_type(LOGIN_TYPE_DEFAULT, login_id, roles)
.await
}
pub async fn add_role(&self, login_id: &str, role: String) -> SaTokenResult<()> {
self.add_role_with_type(LOGIN_TYPE_DEFAULT, login_id, role)
.await
}
pub async fn remove_role(&self, login_id: &str, role: &str) -> SaTokenResult<()> {
self.remove_role_with_type(LOGIN_TYPE_DEFAULT, login_id, role)
.await
}
pub async fn clear_roles(&self, login_id: &str) -> SaTokenResult<()> {
self.clear_roles_with_type(LOGIN_TYPE_DEFAULT, login_id)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{LogoutMode, TokenStyle};
use crate::error::SaTokenError;
use chrono::{Duration, Utc};
use sa_token_storage_memory::MemoryStorage;
fn make_manager(is_concurrent: bool, auto_renew: bool, active_timeout: i64) -> SaTokenManager {
let config = SaTokenConfig {
timeout: 3600,
token_style: TokenStyle::Uuid,
is_concurrent,
auto_renew,
active_timeout,
..Default::default()
};
SaTokenManager::new(Arc::new(MemoryStorage::new()), config)
}
#[tokio::test]
async fn test_non_concurrent_login_invalidates_previous_token() {
let mgr = make_manager(false, false, -1);
let t1 = mgr.login("user_1").await.unwrap();
assert!(mgr.is_valid(&t1).await);
let t2 = mgr.login("user_1").await.unwrap();
assert!(!mgr.is_valid(&t1).await);
assert!(mgr.is_valid(&t2).await);
}
#[tokio::test]
async fn test_logout_clears_login_token_mapping() {
let mgr = make_manager(true, false, -1);
let token = mgr.login("user_1").await.unwrap();
let map_key = mgr.keys().login_token("default", "user_1");
assert!(mgr.storage.get(&map_key).await.unwrap().is_some());
mgr.logout(&token).await.unwrap();
assert!(mgr.storage.get(&map_key).await.unwrap().is_none());
}
#[tokio::test]
async fn test_concurrent_login_appends_token_index() {
let mgr = make_manager(true, false, -1);
let t1 = mgr.login("user_1").await.unwrap();
let t2 = mgr.login("user_1").await.unwrap();
let list = mgr
.token_repo()
.list_tokens("default", "user_1")
.await
.unwrap();
assert_eq!(list.len(), 2);
assert!(list.contains(&t1.as_str().to_string()));
assert!(list.contains(&t2.as_str().to_string()));
}
#[tokio::test]
async fn test_active_timeout_freeze_returns_inactive() {
let mgr = make_manager(true, false, 1);
let token = mgr.login("user_1").await.unwrap();
let key = mgr.keys().token_info(token.as_str());
let mut info = mgr.get_token_info(&token).await.unwrap();
info.last_active_time = Utc::now() - Duration::seconds(10);
mgr.storage
.set(
&key,
&mgr.config.encode(&info).unwrap(),
mgr.config.timeout_duration(),
)
.await
.unwrap();
let result = mgr.get_token_info(&token).await;
assert!(matches!(result, Err(SaTokenError::TokenInactive)));
}
#[tokio::test]
async fn test_auto_renew_updates_last_active_time() {
let config = SaTokenConfig {
timeout: 3600,
token_style: TokenStyle::Uuid,
is_concurrent: true,
auto_renew: true,
active_timeout: 3600,
renew_threshold: -1,
..Default::default()
};
let mgr = SaTokenManager::new(Arc::new(MemoryStorage::new()), config);
let token = mgr.login("user_1").await.unwrap();
let before = mgr.get_token_info(&token).await.unwrap().last_active_time;
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
let after_info = mgr.get_token_info(&token).await.unwrap();
assert!(
after_info.last_active_time > before,
"auto_renew must advance last_active_time"
);
}
#[tokio::test]
async fn test_auto_renew_skipped_when_remaining_above_threshold() {
let config = SaTokenConfig {
timeout: 3600,
auto_renew: true,
renew_threshold: 300,
active_timeout: -1,
token_style: TokenStyle::Uuid,
..Default::default()
};
let mgr = SaTokenManager::new(Arc::new(MemoryStorage::new()), config);
let token = mgr.login("user_skip").await.unwrap();
let before = mgr.get_token_info(&token).await.unwrap().last_active_time;
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
let after = mgr.get_token_info(&token).await.unwrap().last_active_time;
assert_eq!(after, before);
}
#[tokio::test]
async fn test_login_with_nonce_when_enabled() {
let config = SaTokenConfig {
enable_nonce: true,
nonce_timeout: 60,
auto_renew: false,
..Default::default()
};
let mgr = SaTokenManager::new(Arc::new(MemoryStorage::new()), config);
let nonce_mgr = crate::nonce::NonceManager::from_dao(mgr.dao().clone(), 60);
let nonce = nonce_mgr.generate();
let token = mgr
.login_with_options("user_1", None, None, None, Some(nonce.clone()), None)
.await
.unwrap();
assert!(mgr.is_valid(&token).await);
let result = mgr
.login_with_options("user_1", None, None, None, Some(nonce), None)
.await;
assert!(matches!(result, Err(SaTokenError::NonceAlreadyUsed)));
}
#[tokio::test]
async fn test_kickout_token_returns_kicked_out() {
let mgr = make_manager(true, false, -1);
let token = mgr.login("user_kick").await.unwrap();
mgr.kick_out_by_token(&token).await.unwrap();
let err = mgr.get_token_info(&token).await.unwrap_err();
assert!(matches!(err, SaTokenError::AccountKickedOut));
}
#[tokio::test]
async fn test_replaced_token_returns_replaced() {
let mgr = make_manager(false, false, -1);
let t1 = mgr.login("user_rep").await.unwrap();
let _t2 = mgr.login("user_rep").await.unwrap();
let err = mgr.get_token_info(&t1).await.unwrap_err();
assert!(matches!(err, SaTokenError::AccountReplaced));
}
#[tokio::test]
async fn test_is_share_reuses_token() {
let config = SaTokenConfig {
is_share: true,
is_concurrent: true,
..Default::default()
};
let mgr = SaTokenManager::new(Arc::new(MemoryStorage::new()), config);
let t1 = mgr.login("user_share").await.unwrap();
let t2 = mgr.login("user_share").await.unwrap();
assert_eq!(t1.as_str(), t2.as_str());
}
#[tokio::test]
async fn test_max_login_count_overflow_kickout() {
let config = SaTokenConfig {
is_concurrent: true,
max_login_count: 2,
overflow_logout_mode: LogoutMode::KickOut,
..Default::default()
};
let mgr = SaTokenManager::new(Arc::new(MemoryStorage::new()), config);
let t1 = mgr.login("user_max").await.unwrap();
let _t2 = mgr.login("user_max").await.unwrap();
let t3 = mgr.login("user_max").await.unwrap();
assert!(matches!(
mgr.get_token_info(&t1).await,
Err(SaTokenError::AccountKickedOut)
));
assert!(mgr.is_valid(&t3).await);
}
#[test]
fn test_account_ns_default_unchanged() {
let mgr = make_manager(true, false, -1);
assert_eq!(mgr.account_ns("default", "u1").as_str(), "u1");
assert_eq!(mgr.account_ns("login", "u1").as_str(), "u1");
assert_eq!(mgr.account_ns("", "u1").as_str(), "u1");
assert_eq!(mgr.account_ns("admin", "u1").as_str(), "admin:u1");
}
#[tokio::test]
async fn test_login_writes_terminal_and_logout_removes() {
let mgr = make_manager(true, false, -1);
let token = mgr
.login_with_options("u1", None, Some("PC".to_string()), None, None, None)
.await
.unwrap();
let terminals = mgr.get_terminal_list("default", "u1", None).await.unwrap();
assert_eq!(terminals.len(), 1);
assert_eq!(terminals[0].token_value, token.as_str());
assert_eq!(terminals[0].device_type, "PC");
assert_eq!(terminals[0].index, 1);
mgr.logout(&token).await.unwrap();
let terminals = mgr.get_terminal_list("default", "u1", None).await.unwrap();
assert!(terminals.is_empty());
}
#[tokio::test]
async fn test_terminal_filter_by_device_type() {
let mgr = make_manager(true, false, -1);
mgr.login_with_options("u1", None, Some("PC".to_string()), None, None, None)
.await
.unwrap();
mgr.login_with_options("u1", None, Some("APP".to_string()), None, None, None)
.await
.unwrap();
assert_eq!(
mgr.get_terminal_list("default", "u1", Some("PC"))
.await
.unwrap()
.len(),
1
);
assert_eq!(
mgr.get_token_value_list_by_login_id("default", "u1", None)
.await
.unwrap()
.len(),
2
);
}
#[tokio::test]
async fn test_permissions_isolated_by_login_type() {
let mgr = make_manager(true, false, -1);
mgr.set_permissions_with_type("admin", "u1", vec!["a:read".to_string()])
.await
.unwrap();
mgr.set_permissions_with_type("user", "u1", vec!["u:read".to_string()])
.await
.unwrap();
let admin_perms = mgr.get_permissions_with_type("admin", "u1").await.unwrap();
let user_perms = mgr.get_permissions_with_type("user", "u1").await.unwrap();
assert_eq!(admin_perms, vec!["a:read".to_string()]);
assert_eq!(user_perms, vec!["u:read".to_string()]);
}
}