dbrest_core/auth/
cache.rs1use std::sync::Arc;
24use std::time::Duration;
25
26use moka::future::Cache;
27
28use super::types::AuthResult;
29
30const DEFAULT_TTL: Duration = Duration::from_secs(300);
32
33const MAX_TTL: Duration = Duration::from_secs(3600);
35
36#[derive(Clone)]
41pub struct JwtCache {
42 inner: Cache<Arc<str>, Arc<AuthResult>>,
43}
44
45impl JwtCache {
46 pub fn new(max_entries: u64) -> Self {
48 let inner = Cache::builder()
49 .max_capacity(max_entries)
50 .time_to_live(MAX_TTL)
51 .build();
52 Self { inner }
53 }
54
55 pub async fn get(&self, token: &str) -> Option<Arc<AuthResult>> {
57 self.inner.get(&Arc::<str>::from(token)).await
58 }
59
60 pub async fn insert(&self, token: &str, result: AuthResult) {
62 let ttl = ttl_from_claims(&result);
63 self.inner.insert(Arc::from(token), Arc::new(result)).await;
64
65 let _ = ttl; }
73
74 pub fn invalidate_all(&self) {
76 self.inner.invalidate_all();
77 }
78
79 pub fn entry_count(&self) -> u64 {
81 self.inner.entry_count()
82 }
83}
84
85fn ttl_from_claims(result: &AuthResult) -> Duration {
87 if let Some(exp) = result.claims.get("exp").and_then(|v| v.as_i64()) {
88 let now = chrono::Utc::now().timestamp();
89 if exp > now {
90 let remaining = Duration::from_secs((exp - now) as u64);
91 return remaining.min(MAX_TTL);
92 }
93 }
94 DEFAULT_TTL
95}
96
97impl std::fmt::Debug for JwtCache {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 f.debug_struct("JwtCache")
100 .field("entry_count", &self.inner.entry_count())
101 .finish()
102 }
103}
104
105#[cfg(test)]
110mod tests {
111 use super::*;
112 use compact_str::CompactString;
113
114 fn make_result(role: &str, exp: Option<i64>) -> AuthResult {
115 let mut claims = serde_json::Map::new();
116 claims.insert(
117 "role".to_string(),
118 serde_json::Value::String(role.to_string()),
119 );
120 if let Some(e) = exp {
121 claims.insert("exp".to_string(), serde_json::json!(e));
122 }
123 AuthResult {
124 role: CompactString::from(role),
125 claims,
126 }
127 }
128
129 #[tokio::test]
130 async fn test_cache_insert_and_get() {
131 let cache = JwtCache::new(100);
132 let result = make_result("admin", Some(chrono::Utc::now().timestamp() + 3600));
133
134 cache.insert("token_abc", result.clone()).await;
135
136 let cached = cache.get("token_abc").await.unwrap();
137 assert_eq!(cached.role.as_str(), "admin");
138 }
139
140 #[tokio::test]
141 async fn test_cache_miss() {
142 let cache = JwtCache::new(100);
143 assert!(cache.get("nonexistent").await.is_none());
144 }
145
146 #[tokio::test]
147 async fn test_cache_invalidate_all() {
148 let cache = JwtCache::new(100);
149 let result = make_result("user", Some(chrono::Utc::now().timestamp() + 3600));
150
151 cache.insert("token1", result.clone()).await;
152 cache.insert("token2", result).await;
153
154 cache.invalidate_all();
155
156 assert!(cache.entry_count() <= 2);
160 }
161
162 #[tokio::test]
163 async fn test_cache_capacity() {
164 let cache = JwtCache::new(2);
165 let result = make_result("user", Some(chrono::Utc::now().timestamp() + 3600));
166
167 for i in 0..5 {
168 cache.insert(&format!("token_{i}"), result.clone()).await;
169 }
170
171 assert!(cache.entry_count() <= 5); }
175
176 #[test]
177 fn test_ttl_from_claims_with_exp() {
178 let result = make_result("user", Some(chrono::Utc::now().timestamp() + 600));
179 let ttl = ttl_from_claims(&result);
180 assert!(ttl.as_secs() >= 598 && ttl.as_secs() <= 601);
182 }
183
184 #[test]
185 fn test_ttl_from_claims_capped() {
186 let result = make_result("user", Some(chrono::Utc::now().timestamp() + 7200));
188 let ttl = ttl_from_claims(&result);
189 assert_eq!(ttl, MAX_TTL);
190 }
191
192 #[test]
193 fn test_ttl_from_claims_no_exp() {
194 let result = make_result("user", None);
195 let ttl = ttl_from_claims(&result);
196 assert_eq!(ttl, DEFAULT_TTL);
197 }
198
199 #[test]
200 fn test_ttl_from_claims_expired() {
201 let result = make_result("user", Some(chrono::Utc::now().timestamp() - 100));
202 let ttl = ttl_from_claims(&result);
203 assert_eq!(ttl, DEFAULT_TTL);
205 }
206}