1use std::collections::HashSet;
13use std::sync::Arc;
14
15use chrono::{Duration as ChronoDuration, Utc};
16
17use crate::config::{LogoutMode, LogoutRange, ReplacedLoginExitMode, ReplacedRange, SaTokenConfig};
18use crate::dao::SaTokenDao;
19use crate::distributed::DistributedSessionManager;
20use crate::error::{SaTokenError, SaTokenResult};
21use crate::event::{SaTokenEvent, SaTokenEventBus};
22use crate::keys::{AccountNs, LoginId, SaKeys};
23use crate::nonce::NonceManager;
24use crate::online::OnlineManager;
25use crate::refresh::RefreshTokenManager;
26use crate::repository::{SessionRepo, TokenIdMapping, TokenRepo};
27use crate::service::compensate::LoginCompensator;
28use crate::service::login_request::LoginRequest;
29use crate::session::SaTerminalInfo;
30use crate::token::{TokenGenerator, TokenInfo, TokenValue};
31
32struct LogoutIdentity {
34 login_type: String,
35 login_id: String,
36 _body_existed: bool,
39}
40
41pub struct AuthService {
43 dao: Arc<SaTokenDao>,
44 token_repo: Arc<TokenRepo>,
45 session_repo: Arc<SessionRepo>,
46 config: Arc<SaTokenConfig>,
47 event_bus: SaTokenEventBus,
48 online_manager: Option<Arc<OnlineManager>>,
49 distributed: Option<Arc<DistributedSessionManager>>,
52}
53
54impl std::fmt::Debug for AuthService {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 f.write_str("AuthService { .. }")
57 }
58}
59
60impl AuthService {
61 pub fn new(
63 dao: Arc<SaTokenDao>,
64 config: Arc<SaTokenConfig>,
65 token_repo: Arc<TokenRepo>,
66 session_repo: Arc<SessionRepo>,
67 event_bus: SaTokenEventBus,
68 online_manager: Option<Arc<OnlineManager>>,
69 distributed: Option<Arc<DistributedSessionManager>>,
70 ) -> Self {
71 Self {
72 dao,
73 token_repo,
74 session_repo,
75 config,
76 event_bus,
77 online_manager,
78 distributed,
79 }
80 }
81
82 pub fn token_repo(&self) -> &Arc<TokenRepo> {
84 &self.token_repo
85 }
86
87 pub fn session_repo(&self) -> &Arc<SessionRepo> {
89 &self.session_repo
90 }
91
92 fn account_ns(login_type: &str, login_id: &str) -> SaTokenResult<AccountNs> {
94 let id =
95 LoginId::try_new(login_id).map_err(|e| SaTokenError::ConfigError(e.to_string()))?;
96 Ok(SaKeys::account_ns(login_type, &id))
97 }
98
99 pub async fn login(&self, req: LoginRequest) -> SaTokenResult<TokenValue> {
101 let login_type = req.effective_login_type().to_string();
102 let login_id = req.login_id.clone();
103 let ns = Self::account_ns(&login_type, &login_id)?;
104
105 let mut compensator = LoginCompensator::new();
106
107 {
109 use crate::disable::{DEFAULT_DISABLE_SERVICE, MIN_DISABLE_LEVEL, NOT_DISABLE_LEVEL};
110 let key = self
111 .dao
112 .keys()
113 .disable(&login_type, &login_id, DEFAULT_DISABLE_SERVICE);
114 if let Some(raw) = self.dao.get_string(&key).await? {
115 let level: i32 = raw.parse().unwrap_or(MIN_DISABLE_LEVEL);
116 if level != NOT_DISABLE_LEVEL && level >= MIN_DISABLE_LEVEL {
117 return Err(SaTokenError::AccountBanned(format!(
118 "service={DEFAULT_DISABLE_SERVICE} level={level}"
119 )));
120 }
121 }
122 }
123
124 if self.config.enable_nonce
125 && let Some(ref nonce_str) = req.nonce
126 {
127 self.consume_nonce(nonce_str, &login_id, &mut compensator)
128 .await?;
129 }
130
131 if self.config.is_share
132 && let Some(existing) = self
133 .token_repo
134 .get_login_mapping(&login_type, &login_id)
135 .await?
136 {
137 let existing_token = TokenValue::new(existing);
138 if self
139 .token_repo
140 .load_valid_token_info(&existing_token)
141 .await
142 .is_ok()
143 {
144 compensator.commit();
145 if self.config.is_log {
146 tracing::info!(login_id = %login_id, "login success");
147 }
148 return Ok(existing_token);
149 }
150 }
151
152 let mut token_info = self.build_token_info(&req, &login_type).await?;
153 let token = token_info.token.clone();
154
155 let mapping_before = self
156 .token_repo
157 .get_login_mapping(&login_type, &login_id)
158 .await?;
159
160 if !self.config.is_concurrent {
161 match self
162 .handle_replaced_on_login(&login_type, &login_id, &ns, &req, token.as_str())
163 .await
164 {
165 Ok(()) => {}
166 Err(e) => {
167 let _ = compensator.rollback(&self.dao).await;
168 return Err(e);
169 }
170 }
171 }
172
173 let refresh_mgr = if self.config.enable_refresh_token {
174 Some(RefreshTokenManager::new(
175 self.dao.clone(),
176 self.token_repo.clone(),
177 self.config.clone(),
178 ))
179 } else {
180 None
181 };
182 if let Some(ref mgr) = refresh_mgr {
183 token_info.refresh_token = Some(mgr.generate(&login_id));
184 if self.config.refresh_token_timeout > 0 {
185 token_info.refresh_token_expire_time =
186 Some(Utc::now() + ChronoDuration::seconds(self.config.refresh_token_timeout));
187 }
188 }
189
190 let write_result = self
191 .write_login_stages(
192 &login_type,
193 &login_id,
194 &ns,
195 &req,
196 &token_info,
197 mapping_before.as_deref(),
198 refresh_mgr.as_ref(),
199 &mut compensator,
200 )
201 .await;
202
203 if let Err(e) = write_result {
204 let _ = compensator.rollback(&self.dao).await;
205 return Err(e);
206 }
207
208 if let Err(e) = self.enforce_max_login_count(&login_type, &login_id).await {
209 tracing::warn!(
210 login_id = %login_id,
211 error = %e,
212 "max_login_count enforcement failed after commit, login still succeeds"
213 );
214 }
215
216 compensator.commit();
217
218 if let Some(online) = &self.online_manager {
219 let device = req.effective_device().unwrap_or("unknown");
220 let mut user = crate::online::OnlineUser::new(
221 login_id.clone(),
222 token.as_str().to_string(),
223 device.to_string(),
224 );
225 user.login_type = login_type.clone();
226 if let Err(e) = online.mark_online(user).await {
227 tracing::warn!(error = %e, login_id = %login_id, "failed to mark online after login");
228 }
229 }
230
231 if let Some(dm) = &self.distributed {
232 if let Err(e) = dm
233 .create_session(login_id.clone(), token.as_str().to_string())
234 .await
235 {
236 tracing::warn!(error = %e, login_id = %login_id, "distributed session create failed after login commit");
237 }
238 }
239
240 let event =
241 SaTokenEvent::login(login_id.clone(), token.as_str()).with_login_type(&login_type);
242 self.event_bus.publish(event).await;
243
244 if self.config.is_log {
245 tracing::info!(login_id = %login_id, "login success");
246 }
247
248 Ok(token)
249 }
250
251 async fn consume_nonce(
252 &self,
253 nonce_str: &str,
254 login_id: &str,
255 compensator: &mut LoginCompensator,
256 ) -> SaTokenResult<()> {
257 let nonce_timeout = if self.config.nonce_timeout > 0 {
258 self.config.nonce_timeout
259 } else {
260 self.config.timeout
261 };
262
263 let nonce_key = self.dao.keys().nonce(nonce_str);
264 let snapshot = self.dao.get_string(&nonce_key).await?;
265
266 let nonce_mgr = NonceManager::from_dao(self.dao.clone(), nonce_timeout);
267 nonce_mgr.validate_and_consume(nonce_str, login_id).await?;
268
269 if let Some(raw) = snapshot {
270 let ttl = if nonce_timeout > 0 {
271 Some(std::time::Duration::from_secs(nonce_timeout as u64))
272 } else {
273 None
274 };
275 compensator.on_fail_restore(nonce_key, raw, ttl);
276 }
277
278 Ok(())
279 }
280
281 async fn build_token_info(
282 &self,
283 req: &LoginRequest,
284 login_type: &str,
285 ) -> SaTokenResult<TokenInfo> {
286 let token = match req.preset_token.as_deref() {
287 Some(preset) if !preset.is_empty() => TokenValue::new(preset),
288 _ => {
289 let extra = req.extra_data.clone();
290 let login_id = req.login_id.clone();
291 let cfg = self.config.clone();
292 crate::token::generate_unique(
293 cfg.max_try_times,
294 || match extra.as_ref() {
295 Some(extra) => {
296 TokenGenerator::generate_with_login_id_and_extra(&cfg, &login_id, extra)
297 }
298 None => TokenGenerator::generate_with_login_id(&cfg, &login_id),
299 },
300 |t| {
301 let repo = self.token_repo.clone();
302 let token = t.to_string();
303 async move { Ok(repo.get_token_info(&token).await?.is_some()) }
304 },
305 )
306 .await?
307 }
308 };
309
310 let mut info = TokenInfo::new(token, req.login_id.as_str());
311 info.login_type = crate::token::intern_login_type(login_type);
312 info.device = req.device.clone();
313 info.extra_data = req.extra_data.clone();
314 info.nonce = req.nonce.clone();
315 info.update_active_time();
316
317 if let Some(expire) = req.expire_time {
318 info.expire_time = Some(expire);
319 } else if let Some(timeout) = self.config.timeout_duration() {
320 let d = ChronoDuration::from_std(timeout).map_err(|_| {
321 SaTokenError::ConfigError("timeout value is out of supported range".to_string())
322 })?;
323 info.expire_time = Some(Utc::now() + d);
324 }
325
326 Ok(info)
327 }
328
329 #[allow(clippy::too_many_arguments)]
330 async fn write_login_stages(
331 &self,
332 login_type: &str,
333 login_id: &str,
334 ns: &AccountNs,
335 req: &LoginRequest,
336 token_info: &TokenInfo,
337 mapping_before: Option<&str>,
338 refresh_mgr: Option<&RefreshTokenManager>,
339 compensator: &mut LoginCompensator,
340 ) -> SaTokenResult<()> {
341 let token = token_info.token.as_str();
342 let keys = self.dao.keys();
343
344 self.token_repo
345 .append_index(login_type, login_id, token)
346 .await?;
347 compensator.on_fail_list_remove(keys.login_token_index(login_type, login_id), token);
348
349 let session_key = keys
350 .session_by_ns(ns)
351 .map_err(|e| SaTokenError::ConfigError(e.to_string()))?;
352 match self.session_repo.snapshot_account_session(ns).await? {
353 Some(old_raw) => {
354 compensator.on_fail_restore(session_key, old_raw, self.dao.default_ttl())
355 }
356 None => compensator.on_fail_delete(session_key),
357 }
358 let mut terminal = SaTerminalInfo::new(token, req.effective_device().unwrap_or(""));
359 if let Some(extra) = req.extra_data.clone() {
360 terminal = terminal.with_extra_data(extra);
361 }
362 self.session_repo.add_terminal(ns, terminal).await?;
363
364 self.token_repo
365 .save_token_id_mapping(token, login_type, login_id)
366 .await?;
367 compensator.on_fail_delete(keys.token_id_mapping(token));
368
369 self.token_repo.save_token_info(token_info).await?;
370 compensator.on_fail_delete(keys.token_info(token));
371
372 if self.config.right_now_create_token_session {
373 self.session_repo
374 .create_token_session(&token_info.token)
375 .await?;
376 compensator.on_fail_delete(keys.token_session(token));
377 }
378
379 if let Some(mgr) = refresh_mgr
380 && let Some(ref rt) = token_info.refresh_token
381 {
382 mgr.store_with_extra(
383 rt,
384 token,
385 login_type,
386 login_id,
387 token_info.extra_data.as_ref(),
388 )
389 .await?;
390 compensator.on_fail_delete(keys.refresh(rt));
391 }
392
393 self.commit_login_mapping(login_type, login_id, token, mapping_before, compensator)
394 .await
395 }
396
397 async fn commit_login_mapping(
398 &self,
399 login_type: &str,
400 login_id: &str,
401 token: &str,
402 mapping_before: Option<&str>,
403 compensator: &mut LoginCompensator,
404 ) -> SaTokenResult<()> {
405 let key = self.dao.keys().login_token(login_type, login_id);
406
407 if self.config.is_concurrent {
408 self.token_repo
409 .save_login_mapping(login_type, login_id, token)
410 .await?;
411 } else {
412 let swapped = self
413 .token_repo
414 .cas_login_mapping(login_type, login_id, mapping_before, token)
415 .await?;
416 if !swapped {
417 let swapped_absent = self
418 .token_repo
419 .cas_login_mapping(login_type, login_id, None, token)
420 .await?;
421 if !swapped_absent {
422 tracing::warn!(
423 login_id = %login_id,
424 login_type = %login_type,
425 "concurrent login detected on commit point, rolling back this attempt"
426 );
427 return Err(SaTokenError::AccountReplaced);
428 }
429 }
430 }
431
432 compensator.on_fail_delete(key);
433 Ok(())
434 }
435
436 async fn handle_replaced_on_login(
437 &self,
438 login_type: &str,
439 login_id: &str,
440 ns: &AccountNs,
441 req: &LoginRequest,
442 new_token: &str,
443 ) -> SaTokenResult<()> {
444 let device = req.effective_device();
445 let effective_range = match (self.config.replaced_range, device) {
446 (ReplacedRange::CurrDeviceType, None) => {
447 tracing::debug!(
448 login_id = %login_id,
449 "device type absent, replaced_range degraded to AllDeviceType"
450 );
451 ReplacedRange::AllDeviceType
452 }
453 (range, _) => range,
454 };
455
456 let mut targets: HashSet<String> = HashSet::new();
457
458 match effective_range {
459 ReplacedRange::CurrDeviceType => {
460 for t in self.session_repo.get_terminal_list(ns, device).await? {
463 targets.insert(t.token_value);
464 }
465 }
466 ReplacedRange::AllDeviceType => {
467 for t in self.token_repo.list_tokens(login_type, login_id).await? {
468 targets.insert(t);
469 }
470 if let Some(old) = self
471 .token_repo
472 .get_login_mapping(login_type, login_id)
473 .await?
474 {
475 targets.insert(old);
476 }
477 }
478 }
479
480 targets.remove(new_token);
481
482 if targets.is_empty() {
483 return Ok(());
484 }
485
486 match self.config.replaced_login_exit_mode {
487 ReplacedLoginExitMode::NewDevice => Err(SaTokenError::AccountReplaced),
488 ReplacedLoginExitMode::OldDevice => {
489 for t in targets {
490 if let Err(e) = self.logout_replaced(&TokenValue::new(t.clone())).await {
491 tracing::warn!(token = %t, error = %e, "replace of stale token failed");
492 }
493 }
494 Ok(())
495 }
496 }
497 }
498
499 pub async fn logout(&self, token: &TokenValue, keep_token_session: bool) -> SaTokenResult<()> {
501 let result = match self.config.logout_range {
502 LogoutRange::Token => {
503 self.logout_internal(token, LogoutMode::Logout, keep_token_session)
504 .await
505 }
506 LogoutRange::Account => match self.resolve_logout_identity(token.as_str()).await? {
507 Some(id) => self.logout_by_login_id(&id.login_type, &id.login_id).await,
508 None => {
509 self.logout_internal(token, LogoutMode::Logout, keep_token_session)
510 .await
511 }
512 },
513 };
514 if result.is_ok() && self.config.is_log {
515 tracing::info!(token = %token.as_str(), "logout success");
516 }
517 result
518 }
519
520 pub async fn kick_out_by_token(
522 &self,
523 token: &TokenValue,
524 keep_token_session: bool,
525 ) -> SaTokenResult<()> {
526 self.logout_internal(token, LogoutMode::KickOut, keep_token_session)
527 .await
528 }
529
530 pub async fn logout_replaced(&self, token: &TokenValue) -> SaTokenResult<()> {
532 self.logout_internal(
533 token,
534 LogoutMode::Replaced,
535 self.config.is_logout_keep_token_session,
536 )
537 .await
538 }
539
540 async fn resolve_logout_identity(&self, token: &str) -> SaTokenResult<Option<LogoutIdentity>> {
541 if let Some(info) = self.token_repo.get_token_info(token).await? {
542 return Ok(Some(LogoutIdentity {
543 login_type: info.login_type.to_string(),
544 login_id: info.login_id.to_string(),
545 _body_existed: true,
546 }));
547 }
548
549 match self.token_repo.get_token_id_mapping(token).await? {
550 Some(TokenIdMapping::Identity {
551 login_type,
552 login_id,
553 }) => Ok(Some(LogoutIdentity {
554 login_type,
555 login_id,
556 _body_existed: false,
557 })),
558 _ => Ok(None),
559 }
560 }
561
562 async fn logout_internal(
563 &self,
564 token: &TokenValue,
565 mode: LogoutMode,
566 keep_token_session: bool,
567 ) -> SaTokenResult<()> {
568 let token_str = token.as_str();
569 tracing::debug!(mode = ?mode, token = %token_str, "logout_internal");
570
571 let identity = self.resolve_logout_identity(token_str).await?;
572
573 self.token_repo.delete_token_info(token_str).await?;
574
575 if !keep_token_session {
576 let _ = self.session_repo.delete_token_session(token).await;
577 }
578
579 match mode {
580 LogoutMode::Logout => self.token_repo.delete_token_id_mapping(token_str).await?,
581 LogoutMode::KickOut => {
582 self.token_repo
583 .mark_token_id(token_str, self.token_repo.kick_out_marker())
584 .await?
585 }
586 LogoutMode::Replaced => {
587 self.token_repo
588 .mark_token_id(token_str, self.token_repo.replaced_marker())
589 .await?
590 }
591 }
592
593 let Some(identity) = identity else {
594 tracing::debug!(token = %token_str, "logout target has no resolvable identity, skipping account-level cleanup");
595 return Ok(());
596 };
597
598 let lt = identity.login_type.as_str();
599 let lid = identity.login_id.as_str();
600
601 if let Err(e) = self.token_repo.remove_index(lt, lid, token_str).await {
602 tracing::warn!(token = %token_str, error = %e, "failed to remove token from login index");
603 }
604
605 if let Ok(ns) = Self::account_ns(lt, lid) {
606 let removed = self
607 .session_repo
608 .remove_terminal(&ns, token_str)
609 .await
610 .unwrap_or(false);
611
612 if removed {
613 let count = self.session_repo.terminal_count(&ns).await.unwrap_or(0);
614 if count == 0 && mode != LogoutMode::Replaced {
615 let _ = self.session_repo.delete_by_ns(&ns).await;
616 }
617 }
618 }
619
620 if mode == LogoutMode::Logout {
621 let _ = self
622 .token_repo
623 .cas_delete_login_mapping(lt, lid, token_str)
624 .await;
625 }
626
627 if let Some(dm) = &self.distributed {
628 if let Err(e) = dm.delete_sessions_by_token(lid, token_str).await {
629 tracing::warn!(error = %e, "distributed session delete failed on logout");
630 }
631 }
632
633 if let Some(online) = &self.online_manager {
634 if let Err(e) = online.mark_offline_with_type(lt, lid, token_str).await {
635 tracing::warn!(error = %e, "failed to clear online presence on logout");
636 }
637 }
638
639 let event = match mode {
640 LogoutMode::Logout => SaTokenEvent::logout(lid, token_str),
641 LogoutMode::KickOut => SaTokenEvent::kick_out(lid, token_str),
642 LogoutMode::Replaced => SaTokenEvent::replaced(lid, token_str),
643 };
644 self.event_bus.publish(event.with_login_type(lt)).await;
645
646 Ok(())
647 }
648
649 async fn collect_account_tokens(
650 &self,
651 login_type: &str,
652 login_id: &str,
653 ) -> SaTokenResult<Vec<String>> {
654 let (alive, pruned) = self.token_repo.prune_index(login_type, login_id).await?;
655 if pruned > 0 {
656 tracing::debug!(pruned, login_id = %login_id, "pruned orphan index entries");
657 }
658 if !alive.is_empty() {
659 return Ok(alive);
660 }
661
662 let mut result = Vec::new();
663 let keys = self.dao.keys();
664 let pattern = keys.token_scan_pattern(Some(login_type));
665 let mut cursor = 0u64;
666
667 loop {
668 let page = match self.dao.scan(&pattern, cursor, 100).await {
669 Ok(p) => p,
670 Err(e) => {
671 tracing::debug!(error = %e, "scan fallback unavailable");
672 break;
673 }
674 };
675
676 for key in &page.keys {
677 let Some(token) = keys.parse_token_from_key(key, Some(login_type)) else {
678 continue;
679 };
680 if let Ok(Some(info)) = self.token_repo.get_token_info(token).await
681 && info.login_id.as_ref() == login_id
682 && info.login_type.as_ref() == login_type
683 {
684 result.push(token.to_string());
685 }
686 }
687
688 if page.next_cursor == 0 {
689 break;
690 }
691 cursor = page.next_cursor;
692 }
693
694 if result.is_empty()
695 && let Some(one) = self
696 .token_repo
697 .get_login_mapping(login_type, login_id)
698 .await?
699 {
700 result.push(one);
701 }
702
703 Ok(result)
704 }
705
706 pub async fn logout_by_login_id(&self, login_type: &str, login_id: &str) -> SaTokenResult<()> {
711 let tokens = self.collect_account_tokens(login_type, login_id).await?;
712 let keep = self.config.is_logout_keep_token_session;
713 for t in tokens {
714 if let Err(e) = self
715 .logout_internal(&TokenValue::new(t.clone()), LogoutMode::Logout, keep)
716 .await
717 {
718 tracing::warn!(token = %t, error = %e, "logout of one token failed during account logout");
719 }
720 }
721 Ok(())
722 }
723
724 pub async fn kick_out(&self, login_type: &str, login_id: &str) -> SaTokenResult<()> {
726 if let Some(online) = &self.online_manager {
727 let _ = online
728 .mark_offline_all_with_type(login_type, login_id)
729 .await;
730 let _ = online
731 .kick_out_notify(login_id, "Account kicked out".to_string())
732 .await;
733 }
734
735 let tokens = self.collect_account_tokens(login_type, login_id).await?;
736 for t in tokens {
737 if let Err(e) = self
738 .kick_out_by_token(
739 &TokenValue::new(t.clone()),
740 self.config.is_logout_keep_token_session,
741 )
742 .await
743 {
744 tracing::warn!(token = %t, error = %e, "kickout of one token failed");
745 }
746 }
747
748 if let Ok(ns) = Self::account_ns(login_type, login_id) {
749 let _ = self.session_repo.delete_by_ns(&ns).await;
750 }
751 Ok(())
752 }
753
754 pub async fn get_token_info(&self, token: &TokenValue) -> SaTokenResult<TokenInfo> {
756 match self.token_repo.load_valid_token_info(token).await {
757 Ok(info) => Ok(info),
758 Err(SaTokenError::TokenExpired) => {
759 let _ = self
760 .logout(token, self.config.is_logout_keep_token_session)
761 .await;
762 Err(SaTokenError::TokenExpired)
763 }
764 Err(other) => Err(other),
765 }
766 }
767
768 pub async fn is_valid(&self, token: &TokenValue) -> bool {
770 self.get_token_info(token).await.is_ok()
771 }
772
773 pub async fn renew_timeout(
775 &self,
776 token: &TokenValue,
777 timeout_seconds: i64,
778 ) -> SaTokenResult<()> {
779 let mut info = self.token_repo.load_token_info_no_renew(token).await?;
780
781 info.update_active_time();
782 let ttl = if timeout_seconds > 0 {
783 info.expire_time = Some(Utc::now() + ChronoDuration::seconds(timeout_seconds));
784 Some(std::time::Duration::from_secs(timeout_seconds as u64))
785 } else {
786 info.expire_time = None;
787 None
788 };
789
790 let key = self.dao.keys().token_info(token.as_str());
791 self.dao.set_object(&key, &info, ttl).await?;
792
793 let event =
794 SaTokenEvent::renew_timeout(info.login_id.as_ref(), token.as_str(), timeout_seconds)
795 .with_login_type(info.login_type.as_ref());
796 self.event_bus.publish(event).await;
797
798 Ok(())
799 }
800
801 async fn enforce_max_login_count(&self, login_type: &str, login_id: &str) -> SaTokenResult<()> {
802 if self.config.max_login_count <= 0 || !self.config.is_concurrent {
803 return Ok(());
804 }
805
806 let (alive, pruned) = self.token_repo.prune_index(login_type, login_id).await?;
807 if pruned > 0 {
808 tracing::debug!(
809 pruned,
810 login_id = %login_id,
811 "pruned orphan tokens before enforcing max_login_count"
812 );
813 }
814
815 let max = self.config.max_login_count as usize;
816 if alive.len() <= max {
817 return Ok(());
818 }
819 let overflow = alive.len() - max;
820
821 for stale in alive.iter().take(overflow) {
822 let _ = self
823 .token_repo
824 .remove_index(login_type, login_id, stale)
825 .await;
826
827 let token = TokenValue::new(stale.clone());
828 let keep = self.config.is_logout_keep_token_session;
829 let outcome = match self.config.overflow_logout_mode {
830 LogoutMode::Logout => self.logout(&token, keep).await,
831 LogoutMode::KickOut => self.kick_out_by_token(&token, keep).await,
832 LogoutMode::Replaced => self.logout_replaced(&token).await,
833 };
834 if let Err(e) = outcome {
835 tracing::warn!(token = %stale, error = %e, "overflow eviction failed");
836 }
837 }
838
839 Ok(())
840 }
841}