r_token/memory.rs
1use crate::RTokenError;
2use crate::models::RTokenInfo;
3use chrono::Utc;
4use std::{
5 collections::HashMap,
6 sync::{Arc, Mutex},
7};
8
9/// Issues, stores, and revokes authentication tokens.
10///
11/// This type is designed to be stored in actix-web application state
12/// (e.g. `web::Data<RTokenManager>`). Internally it uses an `Arc<Mutex<...>>`,
13/// so `Clone` creates another handle to the same shared store.
14///
15/// Tokens are generated as UUID v4 strings. Each token is associated with:
16/// - a user id (`String`)
17/// - an expiration timestamp (Unix epoch milliseconds)
18///
19/// ## 繁體中文
20///
21/// 負責簽發、儲存與註銷 token 的管理器。
22///
23/// 一般會放在 actix-web 的 application state 中(例如 `web::Data<RTokenManager>`)。
24/// 內部以 `Arc<Mutex<...>>` 共享狀態,因此 `Clone` 只是在同一份映射表上增加一個引用。
25///
26/// token 以 UUID v4 字串產生,並會綁定:
27/// - 使用者 id(`String`)
28/// - 到期時間(Unix epoch 毫秒)
29#[derive(Clone, Default)]
30pub struct RTokenManager {
31 /// In-memory token store.
32 ///
33 /// ## 繁體中文
34 ///
35 /// 記憶體中的 token 儲存表。
36 // store: Arc<Mutex<HashMap<String, String>>>,
37 store: Arc<Mutex<HashMap<String, RTokenInfo>>>,
38}
39
40impl RTokenManager {
41 /// Creates an empty manager.
42 ///
43 /// ## 繁體中文
44 ///
45 /// 建立一個空的管理器。
46 pub fn new() -> Self {
47 Self {
48 store: Arc::new(Mutex::new(HashMap::new())),
49 }
50 }
51
52 /// Issues a new token for the given user id.
53 ///
54 /// `expire_time` is treated as TTL in seconds. The token will be considered invalid
55 /// once the stored expiration timestamp is earlier than the current time.
56 ///
57 /// Returns [`RTokenError::MutexPoisoned`] if the internal mutex is poisoned.
58 ///
59 /// ## 繁體中文
60 ///
61 /// 為指定使用者 id 簽發新 token。
62 ///
63 /// `expire_time` 會被視為 TTL(秒)。當儲存的到期時間早於目前時間時,token 會被視為無效。
64 ///
65 /// 若內部 mutex 發生 poisoned,會回傳 [`RTokenError::MutexPoisoned`]。
66 // pub fn login(&self, id: &str, expire_time: u64,role:impl Into<Vec<String>>) -> Result<String, RTokenError> {
67 pub fn login(&self, id: &str, expire_time: u64) -> Result<String, RTokenError> {
68 let token = uuid::Uuid::new_v4().to_string();
69 // Acquire the write lock and insert the token-user mapping into the store
70 // 获取写锁并将 Token-用户映射关系插入到存储中
71 // #[allow(clippy::unwrap_used)]
72 // self.store.lock().unwrap().insert(token.clone(), id.to_string());
73 let now = Utc::now();
74 let ttl = chrono::Duration::seconds(expire_time as i64);
75 let deadline = now + ttl;
76 let expire_time = deadline.timestamp_millis() as u64;
77 let info = RTokenInfo {
78 user_id: id.to_string(),
79 expire_at: expire_time,
80 roles: Vec::new(),
81 };
82 self.store
83 .lock()
84 .map_err(|_| RTokenError::MutexPoisoned)?
85 .insert(token.clone(), info);
86 Ok(token)
87 }
88
89 #[cfg(feature = "rbac")]
90 pub fn login_with_roles(
91 &self,
92 id: &str,
93 expire_time: u64,
94 role: impl Into<Vec<String>>,
95 ) -> Result<String, RTokenError> {
96 let token = uuid::Uuid::new_v4().to_string();
97 let now = Utc::now();
98 let ttl = chrono::Duration::seconds(expire_time as i64);
99 let deadline = now + ttl;
100 let expire_time = deadline.timestamp_millis() as u64;
101 let info = RTokenInfo {
102 user_id: id.to_string(),
103 expire_at: expire_time,
104 roles: role.into(),
105 };
106 self.store
107 .lock()
108 .map_err(|_| RTokenError::MutexPoisoned)?
109 .insert(token.clone(), info);
110 Ok(token)
111 }
112
113 // pub fn set_role(&self, token: &str, role: impl Into<Vec<String>>) -> Result<(), RTokenError> {
114 #[cfg(feature = "rbac")]
115 pub fn set_roles(&self, token: &str, roles: impl Into<Vec<String>>) -> Result<(), RTokenError> {
116 let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
117 if let Some(info) = store.get_mut(token) {
118 info.roles = roles.into();
119 }
120 Ok(())
121 }
122
123 #[cfg(feature = "rbac")]
124 pub fn get_roles(&self, token: &str) -> Result<Option<Vec<String>>, RTokenError> {
125 let store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
126 Ok(store.get(token).map(|info| info.roles.clone()))
127 }
128
129 /// Revokes a token by removing it from the in-memory store.
130 ///
131 /// This operation is idempotent: removing a non-existing token is treated as success.
132 /// Returns [`RTokenError::MutexPoisoned`] if the internal mutex is poisoned.
133 ///
134 /// ## 繁體中文
135 ///
136 /// 從記憶體儲存表中移除 token,以達到註銷效果。
137 ///
138 /// 此操作具冪等性:移除不存在的 token 也視為成功。
139 /// 若內部 mutex 發生 poisoned,會回傳 [`RTokenError::MutexPoisoned`]。
140 pub fn logout(&self, token: &str) -> Result<(), RTokenError> {
141 // self.store.lock().unwrap().remove(token);
142 self.store
143 .lock()
144 .map_err(|_| RTokenError::MutexPoisoned)?
145 .remove(token);
146 Ok(())
147 }
148
149 /// Returns the stored expiration timestamp for a token (milliseconds since Unix epoch).
150 ///
151 /// Returns `Ok(None)` if the token does not exist. This method does not validate
152 /// whether the token has already expired.
153 ///
154 /// ## 繁體中文
155 ///
156 /// 回傳 token 的到期時間戳(Unix epoch 毫秒)。
157 ///
158 /// 若 token 不存在,回傳 `Ok(None)`。本方法不會檢查 token 是否已過期。
159 pub fn expires_at(&self, token: &str) -> Result<Option<u64>, RTokenError> {
160 let store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
161 Ok(store.get(token).map(|info| info.expire_at))
162 }
163
164 /// Returns the remaining TTL in seconds for a token.
165 ///
166 /// Returns:
167 /// - `Ok(None)` when the token does not exist
168 /// - `Ok(Some(0))` when the token is already expired (it is not removed here)
169 ///
170 /// ## 繁體中文
171 ///
172 /// 回傳 token 剩餘 TTL(秒)。
173 ///
174 /// 回傳:
175 /// - token 不存在:`Ok(None)`
176 /// - token 已過期:`Ok(Some(0))`(本方法不會在此移除它)
177 pub fn ttl_seconds(&self, token: &str) -> Result<Option<i64>, RTokenError> {
178 let now_ms = Utc::now().timestamp_millis() as u64;
179 let store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
180 let Some(expire_at) = store.get(token).map(|info| info.expire_at) else {
181 return Ok(None);
182 };
183
184 if expire_at <= now_ms {
185 return Ok(Some(0));
186 }
187
188 let remaining_ms = expire_at - now_ms;
189 let remaining_seconds = remaining_ms.div_ceil(1000) as i64;
190 Ok(Some(remaining_seconds))
191 }
192
193 /// Extends a token's lifetime to `now + ttl_seconds`.
194 ///
195 /// Returns:
196 /// - `Ok(true)` if the token exists and is not expired
197 /// - `Ok(false)` if the token does not exist or is expired (expired tokens are removed)
198 ///
199 /// ## 繁體中文
200 ///
201 /// 將 token 續期為 `now + ttl_seconds`。
202 ///
203 /// 回傳:
204 /// - token 存在且未過期:`Ok(true)`
205 /// - token 不存在或已過期:`Ok(false)`(若已過期會順便移除)
206 pub fn renew(&self, token: &str, ttl_seconds: u64) -> Result<bool, RTokenError> {
207 let now = Utc::now();
208 let ttl = chrono::Duration::seconds(ttl_seconds as i64);
209 let expire_at = (now + ttl).timestamp_millis() as u64;
210
211 let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
212 let Some(info) = store.get_mut(token) else {
213 return Ok(false);
214 };
215
216 if info.expire_at < Utc::now().timestamp_millis() as u64 {
217 store.remove(token);
218 return Ok(false);
219 }
220
221 info.expire_at = expire_at;
222 Ok(true)
223 }
224
225 /// Issues a new token for the same user (and roles) and revokes the old token.
226 ///
227 /// The new token will have a lifetime of `ttl_seconds` from now.
228 ///
229 /// Returns `Ok(None)` if the old token does not exist or is expired (expired tokens
230 /// are removed).
231 ///
232 /// ## 繁體中文
233 ///
234 /// 為同一位使用者(以及角色)換發新 token,並註銷舊 token。
235 ///
236 /// 新 token 的 TTL 會以現在起算 `ttl_seconds`。
237 ///
238 /// 若舊 token 不存在或已過期,回傳 `Ok(None)`(若已過期會順便移除)。
239 pub fn rotate(&self, token: &str, ttl_seconds: u64) -> Result<Option<String>, RTokenError> {
240 let now = Utc::now();
241 let ttl = chrono::Duration::seconds(ttl_seconds as i64);
242 let expire_at = (now + ttl).timestamp_millis() as u64;
243
244 let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
245 let Some(info) = store.get(token).cloned() else {
246 return Ok(None);
247 };
248
249 if info.expire_at < Utc::now().timestamp_millis() as u64 {
250 store.remove(token);
251 return Ok(None);
252 }
253
254 let new_token = uuid::Uuid::new_v4().to_string();
255 let new_info = RTokenInfo {
256 user_id: info.user_id,
257 expire_at,
258 roles: info.roles,
259 };
260
261 store.remove(token);
262 store.insert(new_token.clone(), new_info);
263 Ok(Some(new_token))
264 }
265
266 /// Removes expired tokens from the in-memory store and returns how many were removed.
267 ///
268 /// ## 繁體中文
269 ///
270 /// 從記憶體儲存表中移除已過期的 token,並回傳移除數量。
271 pub fn prune_expired(&self) -> Result<usize, RTokenError> {
272 let now = Utc::now().timestamp_millis() as u64;
273 let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
274
275 let original_len = store.len();
276 store.retain(|_token, info| info.expire_at >= now);
277 Ok(original_len - store.len())
278 }
279
280 /// Validates a token and returns the associated user id if present.
281 ///
282 /// Behavior:
283 /// - Returns `Ok(Some(user_id))` when the token exists and is not expired.
284 /// - Returns `Ok(None)` when the token does not exist or is expired.
285 /// - Expired tokens are removed from the in-memory store during validation.
286 ///
287 /// ## 繁體中文
288 ///
289 /// 驗證 token,若有效則回傳對應的使用者 id。
290 ///
291 /// 行為:
292 /// - token 存在且未過期:回傳 `Ok(Some(user_id))`
293 /// - token 不存在或已過期:回傳 `Ok(None)`
294 /// - 若 token 已過期,會在驗證時從記憶體儲存表中移除
295 pub fn validate(&self, token: &str) -> Result<Option<String>, RTokenError> {
296 #[cfg(feature = "rbac")]
297 {
298 Ok(self
299 .validate_with_roles(token)?
300 .map(|(user_id, _roles)| user_id))
301 }
302
303 #[cfg(not(feature = "rbac"))]
304 {
305 let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
306 let Some(info) = store.get(token) else {
307 return Ok(None);
308 };
309
310 if info.expire_at < Utc::now().timestamp_millis() as u64 {
311 store.remove(token);
312 return Ok(None);
313 }
314
315 Ok(Some(info.user_id.clone()))
316 }
317 }
318
319 #[cfg(feature = "rbac")]
320 /// Validates a token and returns both user id and roles (RBAC enabled).
321 ///
322 /// This has the same expiration behavior as [`RTokenManager::validate`].
323 ///
324 /// ## 繁體中文
325 ///
326 /// 驗證 token,並在 RBAC 啟用時同時回傳使用者 id 與角色列表。
327 ///
328 /// 到期行為與 [`RTokenManager::validate`] 相同。
329 pub fn validate_with_roles(
330 &self,
331 token: &str,
332 ) -> Result<Option<(String, Vec<String>)>, RTokenError> {
333 let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
334 let Some(info) = store.get(token) else {
335 return Ok(None);
336 };
337
338 if info.expire_at < Utc::now().timestamp_millis() as u64 {
339 store.remove(token);
340 return Ok(None);
341 }
342
343 Ok(Some((info.user_id.clone(), info.roles.clone())))
344 }
345}
346
347/// An authenticated request context extracted from actix-web.
348///
349/// If extraction succeeds, `id` is the user id previously passed to
350/// [`RTokenManager::login`], and `token` is the original token from the request.
351///
352/// The token is read from `Authorization` header. Both of the following formats
353/// are accepted:
354/// - `Authorization: <token>`
355/// - `Authorization: Bearer <token>`
356///
357/// ## 繁體中文
358///
359/// 由 actix-web 自動抽取的已驗證使用者上下文。
360///
361/// Extractor 成功時:
362/// - `id` 會是先前傳給 [`RTokenManager::login`] 的使用者 id
363/// - `token` 會是請求中帶來的 token 原文
364///
365/// token 會從 `Authorization` header 讀取,支援以下格式:
366/// - `Authorization: <token>`
367/// - `Authorization: Bearer <token>`
368#[cfg(feature = "actix")]
369#[derive(Debug)]
370pub struct RUser {
371 /// The user id associated with the token.
372 ///
373 /// ## 繁體中文
374 ///
375 /// 與 token 綁定的使用者 id。
376 pub id: String,
377
378 /// The raw token string from the request.
379 ///
380 /// ## 繁體中文
381 ///
382 /// 來自請求的 token 字串原文。
383 pub token: String,
384 #[cfg(feature = "rbac")]
385 pub roles: Vec<String>,
386}
387
388#[cfg(feature = "rbac")]
389impl RUser {
390 pub fn has_role(&self, role: &str) -> bool {
391 self.roles.iter().any(|r| r == role)
392 }
393}
394
395/// Extracts [`RUser`] from an actix-web request.
396///
397/// Failure modes:
398/// - 500: manager is missing from `app_data`, or mutex is poisoned
399/// - 401: token is missing, invalid, or expired
400///
401/// ## 繁體中文
402///
403/// 從 actix-web 請求中抽取 [`RUser`]。
404///
405/// 失敗情況:
406/// - 500:`app_data` 中找不到管理器,或 mutex poisoned
407/// - 401:token 缺失、無效、或已過期
408#[cfg(feature = "actix")]
409impl actix_web::FromRequest for RUser {
410 type Error = actix_web::Error;
411 type Future = std::future::Ready<Result<Self, Self::Error>>;
412
413 fn from_request(
414 req: &actix_web::HttpRequest,
415 _payload: &mut actix_web::dev::Payload,
416 ) -> Self::Future {
417 use actix_web::web;
418
419 // 獲取管理器
420 let manager = match req.app_data::<web::Data<RTokenManager>>() {
421 Some(m) => m,
422 None => {
423 return std::future::ready(Err(actix_web::error::ErrorInternalServerError(
424 "Token manager not found",
425 )));
426 }
427 };
428 let token = match crate::extract_token_from_request(req) {
429 Some(token) => token,
430 None => {
431 return std::future::ready(Err(actix_web::error::ErrorUnauthorized(
432 "Unauthorized",
433 )));
434 }
435 };
436
437 #[cfg(feature = "rbac")]
438 {
439 let user_info = match manager.validate_with_roles(&token) {
440 Ok(user_info) => user_info,
441 Err(_) => {
442 return std::future::ready(Err(actix_web::error::ErrorInternalServerError(
443 "Mutex poisoned",
444 )));
445 }
446 };
447
448 if let Some((user_id, roles)) = user_info {
449 return std::future::ready(Ok(RUser {
450 id: user_id,
451 token,
452 roles,
453 }));
454 }
455
456 std::future::ready(Err(actix_web::error::ErrorUnauthorized("Invalid token")))
457 }
458
459 #[cfg(not(feature = "rbac"))]
460 {
461 let user_id = match manager.validate(&token) {
462 Ok(user_id) => user_id,
463 Err(_) => {
464 return std::future::ready(Err(actix_web::error::ErrorInternalServerError(
465 "Mutex poisoned",
466 )));
467 }
468 };
469
470 if let Some(user_id) = user_id {
471 return std::future::ready(Ok(RUser { id: user_id, token }));
472 }
473
474 std::future::ready(Err(actix_web::error::ErrorUnauthorized("Invalid token")))
475 }
476 }
477}