1use std::fmt::Write as _;
56use std::sync::Arc;
57
58use crate::config::SaTokenConfig;
59
60pub const LOGIN_TYPE_DEFAULT: &str = "default";
67
68pub const LOGIN_TYPE_LOGIN: &str = "login";
76
77pub const LOGIN_TYPE_SSO: &str = "sso";
79
80pub const LOGIN_TYPE_SSO_CLIENT: &str = "sso_client";
82
83const COLON_ESCAPE: &str = "%3A";
86
87const MAX_LOGIN_ID_LEN: usize = 512;
89
90#[derive(Debug, Clone, PartialEq, Eq)]
94pub enum KeyError {
95 EmptyLoginId,
97
98 LoginIdTooLong {
100 actual: usize,
102 max: usize,
104 },
105
106 NamespacedIdUnsupportedByLayout {
109 api: &'static str,
111 },
112}
113
114impl std::fmt::Display for KeyError {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 match self {
117 Self::EmptyLoginId => write!(f, "login_id must not be empty"),
118 Self::LoginIdTooLong { actual, max } => write!(
119 f,
120 "login_id is too long: {actual} bytes exceeds the maximum of {max} bytes"
121 ),
122 Self::NamespacedIdUnsupportedByLayout { api } => write!(
123 f,
124 "{api} requires SaKeyLayout::ThreeSegment; use the (login_type, login_id) variant instead"
125 ),
126 }
127 }
128}
129
130impl std::error::Error for KeyError {}
131
132#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
136#[repr(transparent)]
137pub struct LoginId(String);
138
139impl LoginId {
140 #[inline]
142 pub fn new(id: impl Into<String>) -> Self {
143 Self(id.into())
144 }
145
146 pub fn try_new(id: impl Into<String>) -> Result<Self, KeyError> {
149 let id = id.into();
150 SaKeys::validate_login_id(&id)?;
151 Ok(Self(id))
152 }
153
154 #[inline]
156 pub fn as_str(&self) -> &str {
157 &self.0
158 }
159
160 #[inline]
162 pub fn into_inner(self) -> String {
163 self.0
164 }
165}
166
167impl std::fmt::Display for LoginId {
168 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169 f.write_str(&self.0)
170 }
171}
172
173impl AsRef<str> for LoginId {
174 #[inline]
175 fn as_ref(&self) -> &str {
176 &self.0
177 }
178}
179
180impl From<&str> for LoginId {
181 #[inline]
182 fn from(value: &str) -> Self {
183 Self(value.to_string())
184 }
185}
186
187impl From<String> for LoginId {
188 #[inline]
189 fn from(value: String) -> Self {
190 Self(value)
191 }
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
196#[repr(transparent)]
197pub struct AccountNs(String);
198
199impl AccountNs {
200 #[inline]
203 pub fn from_trusted(ns: impl Into<String>) -> Self {
204 Self(ns.into())
205 }
206
207 #[inline]
209 pub fn as_str(&self) -> &str {
210 &self.0
211 }
212
213 #[inline]
215 pub fn into_inner(self) -> String {
216 self.0
217 }
218}
219
220impl std::fmt::Display for AccountNs {
221 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222 f.write_str(&self.0)
223 }
224}
225
226impl AsRef<str> for AccountNs {
227 #[inline]
228 fn as_ref(&self) -> &str {
229 &self.0
230 }
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
237pub enum SaKeyLayout {
238 #[default]
243 ThreeSegment,
244
245 JavaFourSegment,
250}
251
252#[derive(Debug, Clone)]
257pub struct SaKeys {
258 root: Arc<str>,
260 layout: SaKeyLayout,
262 token_colon: Arc<str>,
265}
266
267impl SaKeys {
268 pub fn new(prefix: impl AsRef<str>) -> Self {
271 let root: Arc<str> = Arc::from(prefix.as_ref());
272 let token_colon = Arc::from(format!("{root}token:"));
273 Self {
274 root,
275 layout: SaKeyLayout::ThreeSegment,
276 token_colon,
277 }
278 }
279
280 pub fn with_layout(root: impl AsRef<str>, layout: SaKeyLayout) -> Self {
282 let root: Arc<str> = match layout {
283 SaKeyLayout::ThreeSegment => Arc::from(root.as_ref()),
284 SaKeyLayout::JavaFourSegment => Arc::from(root.as_ref().trim_end_matches(':')),
285 };
286 let token_colon = Arc::from(format!("{root}token:"));
287 Self {
288 root,
289 layout,
290 token_colon,
291 }
292 }
293
294 pub fn from_config(config: &SaTokenConfig) -> Self {
296 match config.key_layout {
297 SaKeyLayout::ThreeSegment => {
298 Self::with_layout(&config.storage_key_prefix, SaKeyLayout::ThreeSegment)
299 }
300 SaKeyLayout::JavaFourSegment => {
301 Self::with_layout(&config.token_name, SaKeyLayout::JavaFourSegment)
302 }
303 }
304 }
305
306 #[inline]
308 pub fn prefix(&self) -> &str {
309 &self.root
310 }
311
312 #[inline]
314 pub fn layout(&self) -> SaKeyLayout {
315 self.layout
316 }
317
318 #[inline]
319 fn is_java(&self) -> bool {
320 matches!(self.layout, SaKeyLayout::JavaFourSegment)
321 }
322
323 pub fn account_ns(login_type: &str, login_id: &LoginId) -> AccountNs {
326 let id = login_id.as_str();
327
328 if Self::is_default_login_type(login_type) {
329 return AccountNs(id.to_string());
330 }
331
332 let needs_escape = id.contains(':');
333 let escaped_extra = if needs_escape {
334 id.matches(':').count() * (COLON_ESCAPE.len() - 1)
335 } else {
336 0
337 };
338
339 let mut out = String::with_capacity(login_type.len() + 1 + id.len() + escaped_extra);
340 out.push_str(login_type);
341 out.push(':');
342 if needs_escape {
343 Self::push_escaped(&mut out, id);
344 } else {
345 out.push_str(id);
346 }
347 AccountNs(out)
348 }
349
350 #[inline]
353 pub fn is_default_login_type(login_type: &str) -> bool {
354 login_type.is_empty() || login_type == LOGIN_TYPE_DEFAULT || login_type == LOGIN_TYPE_LOGIN
355 }
356
357 #[inline]
358 fn push_escaped(out: &mut String, src: &str) {
359 for ch in src.chars() {
360 if ch == ':' {
361 out.push_str(COLON_ESCAPE);
362 } else {
363 out.push(ch);
364 }
365 }
366 }
367
368 pub fn validate_login_id(login_id: &str) -> Result<(), KeyError> {
371 if login_id.is_empty() {
372 return Err(KeyError::EmptyLoginId);
373 }
374 if login_id.len() > MAX_LOGIN_ID_LEN {
375 return Err(KeyError::LoginIdTooLong {
376 actual: login_id.len(),
377 max: MAX_LOGIN_ID_LEN,
378 });
379 }
380 Ok(())
381 }
382
383 fn build_global(&self, category: &str, id: &str, login_type: Option<&str>) -> String {
384 match self.layout {
385 SaKeyLayout::ThreeSegment => {
386 let mut out =
387 String::with_capacity(self.root.len() + category.len() + 1 + id.len());
388 out.push_str(&self.root);
389 out.push_str(category);
390 out.push(':');
391 out.push_str(id);
392 out
393 }
394 SaKeyLayout::JavaFourSegment => {
395 let lt = login_type.unwrap_or(LOGIN_TYPE_LOGIN);
396 let mut out = String::with_capacity(
397 self.root.len() + 1 + lt.len() + 1 + category.len() + 1 + id.len(),
398 );
399 let _ = write!(out, "{}:{}:{}:{}", self.root, lt, category, id);
400 out
401 }
402 }
403 }
404
405 fn build_account(&self, category: &str, login_type: &str, login_id: &str) -> String {
406 match self.layout {
407 SaKeyLayout::ThreeSegment => {
408 if Self::is_default_login_type(login_type) {
409 let mut out = String::with_capacity(
410 self.root.len() + category.len() + 1 + login_id.len(),
411 );
412 out.push_str(&self.root);
413 out.push_str(category);
414 out.push(':');
415 out.push_str(login_id);
416 return out;
417 }
418
419 let escaped_extra = login_id.matches(':').count() * (COLON_ESCAPE.len() - 1);
420 let mut out = String::with_capacity(
421 self.root.len()
422 + category.len()
423 + 1
424 + login_type.len()
425 + 1
426 + login_id.len()
427 + escaped_extra,
428 );
429 out.push_str(&self.root);
430 out.push_str(category);
431 out.push(':');
432 out.push_str(login_type);
433 out.push(':');
434 Self::push_escaped(&mut out, login_id);
435 out
436 }
437 SaKeyLayout::JavaFourSegment => {
438 let lt = if login_type.is_empty() {
439 LOGIN_TYPE_LOGIN
440 } else {
441 login_type
442 };
443 let mut out = String::with_capacity(
444 self.root.len() + 1 + lt.len() + 1 + category.len() + 1 + login_id.len(),
445 );
446 let _ = write!(out, "{}:{}:{}:{}", self.root, lt, category, login_id);
447 out
448 }
449 }
450 }
451
452 fn build_from_ns(
453 &self,
454 category: &str,
455 ns: &AccountNs,
456 api: &'static str,
457 ) -> Result<String, KeyError> {
458 if self.is_java() {
459 return Err(KeyError::NamespacedIdUnsupportedByLayout { api });
460 }
461 Ok(self.build_global(category, ns.as_str(), None))
462 }
463
464 #[deprecated(
467 since = "0.1.19",
468 note = "Use a named key method (token_info / login_token / ...) so the key layout is respected"
469 )]
470 pub fn make_key(&self, suffix: &str, id: &str) -> String {
471 let mut out = String::with_capacity(self.root.len() + suffix.len() + id.len());
472 out.push_str(&self.root);
473 out.push_str(suffix);
474 out.push_str(id);
475 out
476 }
477
478 #[inline]
482 pub fn token_info(&self, token: &str) -> String {
483 self.build_global("token", token, None)
484 }
485
486 #[inline]
488 pub fn token_info_with_type(&self, login_type: &str, token: &str) -> String {
489 self.build_global("token", token, Some(login_type))
490 }
491
492 #[inline]
494 pub fn token_id_mapping(&self, token: &str) -> String {
495 self.build_global("token-id", token, None)
496 }
497
498 #[inline]
500 pub fn token_session(&self, token: &str) -> String {
501 self.build_global("token-session", token, None)
502 }
503
504 #[inline]
507 pub fn token_session_with_type(&self, login_type: &str, token: &str) -> String {
508 self.build_global("token-session", token, Some(login_type))
509 }
510
511 #[inline]
513 pub fn last_active(&self, token: &str) -> String {
514 self.build_global("last-active", token, None)
515 }
516
517 #[inline]
520 pub fn last_active_with_type(&self, login_type: &str, token: &str) -> String {
521 self.build_global("last-active", token, Some(login_type))
522 }
523
524 #[inline]
528 pub fn login_token(&self, login_type: &str, login_id: &str) -> String {
529 self.build_account("login:token", login_type, login_id)
530 }
531
532 #[inline]
534 pub fn login_token_index(&self, login_type: &str, login_id: &str) -> String {
535 self.build_account("login:tokens", login_type, login_id)
536 }
537
538 #[inline]
540 pub fn account_session(&self, login_type: &str, login_id: &str) -> String {
541 self.build_account("session", login_type, login_id)
542 }
543
544 #[inline]
547 pub fn session_by_ns(&self, ns: &AccountNs) -> Result<String, KeyError> {
548 self.build_from_ns("session", ns, "SaKeys::session_by_ns")
549 }
550
551 #[inline]
553 pub fn permission(&self, login_type: &str, login_id: &str) -> String {
554 self.build_account("permission", login_type, login_id)
555 }
556
557 #[inline]
559 pub fn role(&self, login_type: &str, login_id: &str) -> String {
560 self.build_account("role", login_type, login_id)
561 }
562
563 pub fn disable(&self, login_type: &str, login_id: &str, service: &str) -> String {
565 match self.layout {
566 SaKeyLayout::ThreeSegment => {
567 let mut out = self.build_account("disable", login_type, login_id);
568 out.push(':');
569 out.push_str(service);
570 out
571 }
572 SaKeyLayout::JavaFourSegment => {
573 let lt = if login_type.is_empty() {
574 LOGIN_TYPE_LOGIN
575 } else {
576 login_type
577 };
578 let mut out = String::with_capacity(
579 self.root.len() + 1 + lt.len() + 9 + service.len() + 1 + login_id.len(),
580 );
581 let _ = write!(out, "{}:{}:disable:{}:{}", self.root, lt, service, login_id);
582 out
583 }
584 }
585 }
586
587 pub fn disable_by_ns(&self, ns: &AccountNs, service: &str) -> Result<String, KeyError> {
589 if self.is_java() {
590 return Err(KeyError::NamespacedIdUnsupportedByLayout {
591 api: "SaKeys::disable_by_ns",
592 });
593 }
594 let mut out = self.build_global("disable", ns.as_str(), None);
595 out.push(':');
596 out.push_str(service);
597 Ok(out)
598 }
599
600 pub fn safe(&self, token: &str, service: &str) -> String {
602 self.safe_with_type(LOGIN_TYPE_LOGIN, token, service)
603 }
604
605 pub fn safe_with_type(&self, login_type: &str, token: &str, service: &str) -> String {
607 match self.layout {
608 SaKeyLayout::ThreeSegment => {
609 let mut out = self.build_global("safe", token, None);
610 out.push(':');
611 out.push_str(service);
612 out
613 }
614 SaKeyLayout::JavaFourSegment => {
615 let lt = if login_type.is_empty() {
616 LOGIN_TYPE_LOGIN
617 } else {
618 login_type
619 };
620 let mut out = String::with_capacity(
621 self.root.len() + 1 + lt.len() + 6 + service.len() + 1 + token.len(),
622 );
623 let _ = write!(out, "{}:{}:safe:{}:{}", self.root, lt, service, token);
624 out
625 }
626 }
627 }
628
629 #[inline]
633 pub fn nonce(&self, nonce_value: &str) -> String {
634 self.build_global("nonce", nonce_value, None)
635 }
636
637 #[inline]
639 pub fn refresh(&self, refresh_token: &str) -> String {
640 self.build_global("refresh", refresh_token, None)
641 }
642
643 #[inline]
645 pub fn refresh_user_index(&self, login_type: &str, login_id: &str) -> String {
646 self.build_account("refresh:user", login_type, login_id)
647 }
648
649 #[inline]
652 pub fn refresh_user_index_by_ns(&self, ns: &AccountNs) -> Result<String, KeyError> {
653 self.build_from_ns("refresh:user", ns, "SaKeys::refresh_user_index_by_ns")
654 }
655
656 #[inline]
658 pub fn oauth2_client(&self, client_id: &str) -> String {
659 self.build_global("oauth2:client", client_id, None)
660 }
661
662 #[inline]
664 pub fn oauth2_code(&self, code: &str) -> String {
665 self.build_global("oauth2:code", code, None)
666 }
667
668 #[inline]
670 pub fn oauth2_token(&self, access_token: &str) -> String {
671 self.build_global("oauth2:token", access_token, None)
672 }
673
674 #[inline]
676 pub fn oauth2_refresh(&self, refresh_token: &str) -> String {
677 self.build_global("oauth2:refresh", refresh_token, None)
678 }
679
680 #[inline]
682 pub fn sso_ticket(&self, ticket_id: &str) -> String {
683 self.build_global("sso:ticket", ticket_id, None)
684 }
685
686 #[inline]
688 pub fn sso_session(&self, login_id: &str) -> String {
689 self.build_global("sso:session", login_id, None)
690 }
691
692 #[inline]
694 pub fn sso_login_token(&self, login_type: &str, login_id: &str) -> String {
695 self.login_token(login_type, login_id)
696 }
697
698 #[inline]
700 pub fn online(&self, login_id: &str, token: &str) -> String {
701 let mut out = self.build_global("online", login_id, None);
702 out.push(':');
703 out.push_str(token);
704 out
705 }
706
707 #[inline]
710 pub fn online_with_type(&self, login_type: &str, login_id: &str, token: &str) -> String {
711 let mut out = self.build_account("online", login_type, login_id);
712 out.push(':');
713 out.push_str(token);
714 out
715 }
716
717 #[inline]
719 pub fn online_index(&self, login_id: &str) -> String {
720 self.build_global("online:index", login_id, None)
721 }
722
723 #[inline]
726 pub fn online_index_with_type(&self, login_type: &str, login_id: &str) -> String {
727 self.build_account("online:index", login_type, login_id)
728 }
729
730 #[inline]
733 pub fn online_users_set(&self) -> String {
734 self.build_global("online", "users", None)
735 }
736
737 #[inline]
739 pub fn distributed_session(&self, session_id: &str) -> String {
740 self.build_global("dsession", session_id, None)
741 }
742
743 #[inline]
745 pub fn distributed_session_index(&self, login_id: &str) -> String {
746 self.build_global("dsession:index", login_id, None)
747 }
748
749 #[inline]
751 pub fn distributed_service(&self, service_id: &str) -> String {
752 self.build_global("dservice", service_id, None)
753 }
754
755 #[inline]
758 pub fn same_token(&self) -> String {
759 self.build_global("var", "same-token", None)
760 }
761
762 #[inline]
765 pub fn same_token_past(&self) -> String {
766 self.build_global("var", "same-token-past", None)
767 }
768
769 #[inline]
772 pub fn sign_nonce(&self, nonce: &str) -> String {
773 self.build_global("sign-nonce", nonce, None)
774 }
775
776 #[inline]
779 pub fn temp_token(&self, namespace: &str, token: &str) -> String {
780 let mut cat = String::from("temp-token:");
781 cat.push_str(namespace);
782 self.build_global(&cat, token, None)
783 }
784
785 #[inline]
788 pub fn temp_index(&self, namespace: &str, value_digest: &str) -> String {
789 let mut cat = String::from("temp-index:");
790 cat.push_str(namespace);
791 self.build_global(&cat, value_digest, None)
792 }
793
794 pub fn category_prefix(&self, category: &str, login_type: Option<&str>) -> String {
799 match self.layout {
800 SaKeyLayout::ThreeSegment => {
801 let mut out = String::with_capacity(self.root.len() + category.len() + 1);
802 out.push_str(&self.root);
803 out.push_str(category);
804 out.push(':');
805 out
806 }
807 SaKeyLayout::JavaFourSegment => {
808 let lt = login_type.unwrap_or(LOGIN_TYPE_LOGIN);
809 let mut out =
810 String::with_capacity(self.root.len() + 1 + lt.len() + 1 + category.len() + 1);
811 let _ = write!(out, "{}:{}:{}:", self.root, lt, category);
812 out
813 }
814 }
815 }
816
817 #[inline]
820 pub fn token_key_prefix(&self, login_type: Option<&str>) -> String {
821 self.category_prefix("token", login_type)
822 }
823
824 pub fn token_scan_pattern(&self, login_type: Option<&str>) -> String {
826 let mut out = self.token_key_prefix(login_type);
827 out.push('*');
828 out
829 }
830
831 pub fn scan_pattern(&self, category: &str, login_type: Option<&str>) -> String {
833 let mut out = self.category_prefix(category, login_type);
834 out.push('*');
835 out
836 }
837
838 pub fn parse_token_from_key<'k>(
841 &self,
842 key: &'k str,
843 login_type: Option<&str>,
844 ) -> Option<&'k str> {
845 if matches!(self.layout, SaKeyLayout::ThreeSegment)
848 && login_type.map(Self::is_default_login_type).unwrap_or(true)
849 {
850 return key.strip_prefix(self.token_colon.as_ref());
851 }
852 let prefix = self.token_key_prefix(login_type);
853 key.strip_prefix(prefix.as_str())
854 }
855
856 pub fn parse_id_from_key<'k>(
859 &self,
860 key: &'k str,
861 category: &str,
862 login_type: Option<&str>,
863 ) -> Option<&'k str> {
864 let prefix = self.category_prefix(category, login_type);
865 key.strip_prefix(prefix.as_str())
866 }
867}
868
869impl Default for SaKeys {
870 fn default() -> Self {
871 Self::new("sa:")
872 }
873}
874
875#[cfg(test)]
876mod tests {
877 use super::*;
878
879 fn legacy_make_key(prefix: &str, suffix: &str, id: &str) -> String {
880 format!("{prefix}{suffix}{id}")
881 }
882
883 fn id(s: &str) -> LoginId {
884 LoginId::new(s)
885 }
886
887 #[test]
888 fn account_ns_default_unchanged() {
889 assert_eq!(SaKeys::account_ns("default", &id("u1")).as_str(), "u1");
890 assert_eq!(SaKeys::account_ns("login", &id("u1")).as_str(), "u1");
891 assert_eq!(SaKeys::account_ns("", &id("u1")).as_str(), "u1");
892 assert_eq!(SaKeys::account_ns("admin", &id("u1")).as_str(), "admin:u1");
893 }
894
895 #[test]
896 fn account_ns_colon_escaping() {
897 assert_eq!(SaKeys::account_ns("default", &id("a:b")).as_str(), "a:b");
898 assert_eq!(
899 SaKeys::account_ns("admin", &id("a:b")).as_str(),
900 "admin:a%3Ab"
901 );
902 assert_ne!(
903 SaKeys::account_ns("a", &id("b:c")).as_str(),
904 SaKeys::account_ns("a:b", &id("c")).as_str()
905 );
906 }
907
908 #[test]
909 fn three_segment_matches_legacy_make_key() {
910 let keys = SaKeys::new("sa:");
911 let login_id = "user_1";
912 let token = "abc123";
913
914 assert_eq!(
915 keys.token_info(token),
916 legacy_make_key("sa:", "token:", token)
917 );
918 assert_eq!(
919 keys.login_token("default", login_id),
920 legacy_make_key("sa:", "login:token:", login_id)
921 );
922 assert_eq!(
923 keys.login_token_index("default", login_id),
924 legacy_make_key("sa:", "login:tokens:", login_id)
925 );
926 assert_eq!(
927 keys.account_session("default", login_id),
928 legacy_make_key("sa:", "session:", login_id)
929 );
930 assert_eq!(
931 keys.permission("default", login_id),
932 legacy_make_key("sa:", "permission:", login_id)
933 );
934 assert_eq!(
935 keys.role("default", login_id),
936 legacy_make_key("sa:", "role:", login_id)
937 );
938 assert_eq!(
939 keys.token_id_mapping(token),
940 legacy_make_key("sa:", "token-id:", token)
941 );
942 assert_eq!(
943 keys.token_session(token),
944 legacy_make_key("sa:", "token-session:", token)
945 );
946 assert_eq!(
947 keys.disable("default", login_id, "login"),
948 legacy_make_key("sa:", "disable:", &format!("{login_id}:login"))
949 );
950 assert_eq!(
951 keys.safe(token, "pay"),
952 legacy_make_key("sa:", "safe:", &format!("{token}:pay"))
953 );
954 assert_eq!(
955 keys.nonce("nonce_1"),
956 legacy_make_key("sa:", "nonce:", "nonce_1")
957 );
958 assert_eq!(
959 keys.refresh("rt_1"),
960 legacy_make_key("sa:", "refresh:", "rt_1")
961 );
962 assert_eq!(
963 keys.refresh_user_index("default", login_id),
964 legacy_make_key("sa:", "refresh:user:", login_id)
965 );
966 }
967
968 #[test]
969 fn three_segment_admin_account_keys() {
970 let keys = SaKeys::new("sa:");
971 assert_eq!(
972 keys.login_token("admin", "10001"),
973 "sa:login:token:admin:10001"
974 );
975 assert_eq!(
976 keys.login_token_index("admin", "10001"),
977 "sa:login:tokens:admin:10001"
978 );
979 assert_eq!(
980 keys.account_session("admin", "10001"),
981 "sa:session:admin:10001"
982 );
983 }
984
985 #[test]
986 fn session_by_ns_three_segment() {
987 let keys = SaKeys::new("sa:");
988 let ns = SaKeys::account_ns("admin", &id("10001"));
989 assert_eq!(keys.session_by_ns(&ns).unwrap(), "sa:session:admin:10001");
990 }
991
992 #[test]
993 fn custom_prefix_matches_legacy_make_key() {
994 let keys = SaKeys::new("myapp:");
995 assert_eq!(keys.token_info("t1"), "myapp:token:t1");
996 assert_eq!(keys.login_token("default", "u1"), "myapp:login:token:u1");
997 }
998
999 #[test]
1000 fn java_four_segment_layout() {
1001 let keys = SaKeys::with_layout("satoken", SaKeyLayout::JavaFourSegment);
1002 assert_eq!(keys.token_info("abc"), "satoken:login:token:abc");
1003 assert_eq!(
1004 keys.token_info_with_type("admin", "abc"),
1005 "satoken:admin:token:abc"
1006 );
1007 assert_eq!(
1008 keys.login_token("admin", "u1"),
1009 "satoken:admin:login:token:u1"
1010 );
1011 assert_eq!(
1012 keys.account_session("admin", "u1"),
1013 "satoken:admin:session:u1"
1014 );
1015 assert_eq!(
1016 keys.disable("admin", "u1", "login"),
1017 "satoken:admin:disable:login:u1"
1018 );
1019 assert_eq!(keys.safe("tok", "pay"), "satoken:login:safe:pay:tok");
1020 }
1021
1022 #[test]
1023 fn scan_and_parse_token() {
1024 let keys = SaKeys::new("sa:");
1025 assert_eq!(keys.token_scan_pattern(None), "sa:token:*");
1026 assert_eq!(
1027 keys.parse_token_from_key("sa:token:abc123", None),
1028 Some("abc123")
1029 );
1030 assert_eq!(keys.parse_token_from_key("sa:session:u1", None), None);
1031
1032 let keys = SaKeys::with_layout("satoken", SaKeyLayout::JavaFourSegment);
1033 assert_eq!(
1034 keys.token_scan_pattern(Some("admin")),
1035 "satoken:admin:token:*"
1036 );
1037 assert_eq!(
1038 keys.parse_token_from_key("satoken:admin:token:xyz", Some("admin")),
1039 Some("xyz")
1040 );
1041 }
1042
1043 #[test]
1044 fn from_config_uses_storage_prefix() {
1045 let config = SaTokenConfig::builder()
1046 .storage_key_prefix("app:")
1047 .build_config();
1048 let keys = SaKeys::from_config(&config);
1049 assert_eq!(keys.token_info("x"), "app:token:x");
1050 }
1051}