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/// ## 日本語
10///
11/// 認証 token の発行・保存・失効を行うマネージャです。
12///
13/// actix-web のアプリケーション state(例:`web::Data<RTokenManager>`)に保持する想定で、
14/// 内部では `Arc<Mutex<...>>` を使って状態を共有します。そのため `Clone` は同じストアへの
15/// ハンドルを増やすだけです。
16///
17/// token は UUID v4 文字列として生成され、次と紐づきます:
18/// - ユーザー ID(`String`)
19/// - 有効期限(Unix epoch ミリ秒)
20///
21/// ## English
22///
23/// Issues, stores, and revokes authentication tokens.
24///
25/// This type is designed to be stored in actix-web application state
26/// (e.g. `web::Data<RTokenManager>`). Internally it uses an `Arc<Mutex<...>>`,
27/// so `Clone` creates another handle to the same shared store.
28///
29/// Tokens are generated as UUID v4 strings. Each token is associated with:
30/// - a user id (`String`)
31/// - an expiration timestamp (Unix epoch milliseconds)
32#[derive(Clone, Default)]
33pub struct RTokenManager {
34    /// ## 日本語
35    ///
36    /// インメモリの token ストア。
37    ///
38    /// ## English
39    ///
40    /// In-memory token store.
41    // store: Arc<Mutex<HashMap<String, String>>>,
42    store: Arc<Mutex<HashMap<String, RTokenInfo>>>,
43}
44
45impl RTokenManager {
46    /// ## 日本語
47    ///
48    /// 空のマネージャを作成します。
49    ///
50    /// ## English
51    ///
52    /// Creates an empty manager.
53    pub fn new() -> Self {
54        Self {
55            store: Arc::new(Mutex::new(HashMap::new())),
56        }
57    }
58
59    /// ## 日本語
60    ///
61    /// 指定ユーザー ID の新しい token を発行します。
62    ///
63    /// `expire_time` は TTL(秒)として扱います。保存された有効期限が現在時刻より過去であれば、
64    /// token は無効とみなされます。
65    ///
66    /// 内部 mutex が poisoned の場合は [`RTokenError::MutexPoisoned`] を返します。
67    ///
68    /// ## English
69    ///
70    /// Issues a new token for the given user id.
71    ///
72    /// `expire_time` is treated as TTL in seconds. The token will be considered invalid
73    /// once the stored expiration timestamp is earlier than the current time.
74    ///
75    /// Returns [`RTokenError::MutexPoisoned`] if the internal mutex is poisoned.
76    pub fn login(&self, id: &str, expire_time: u64) -> Result<String, RTokenError> {
77        let token = uuid::Uuid::new_v4().to_string();
78        // Acquire the write lock and insert the token-user mapping into the store
79        // 获取写锁并将 Token-用户映射关系插入到存储中
80        // #[allow(clippy::unwrap_used)]
81        // self.store.lock().unwrap().insert(token.clone(), id.to_string());
82        let now = Utc::now();
83        let ttl = chrono::Duration::seconds(expire_time as i64);
84        let deadline = now + ttl;
85        let expire_time = deadline.timestamp_millis() as u64;
86        let info = RTokenInfo {
87            user_id: id.to_string(),
88            expire_at: expire_time,
89            roles: Vec::new(),
90        };
91        self.store
92            .lock()
93            .map_err(|_| RTokenError::MutexPoisoned)?
94            .insert(token.clone(), info);
95        Ok(token)
96    }
97
98    #[cfg(feature = "rbac")]
99    /// ## 日本語
100    ///
101    /// 指定ユーザー ID と役割(roles)を紐づけた新しい token を発行します(RBAC 有効時)。
102    ///
103    /// `expire_time` は TTL(秒)として扱います。
104    ///
105    /// ## English
106    ///
107    /// Issues a new token for the given user id and roles (RBAC enabled).
108    ///
109    /// `expire_time` is treated as TTL in seconds.
110    pub fn login_with_roles(
111        &self,
112        id: &str,
113        expire_time: u64,
114        role: impl Into<Vec<String>>,
115    ) -> Result<String, RTokenError> {
116        let token = uuid::Uuid::new_v4().to_string();
117        let now = Utc::now();
118        let ttl = chrono::Duration::seconds(expire_time as i64);
119        let deadline = now + ttl;
120        let expire_time = deadline.timestamp_millis() as u64;
121        let info = RTokenInfo {
122            user_id: id.to_string(),
123            expire_at: expire_time,
124            roles: role.into(),
125        };
126        self.store
127            .lock()
128            .map_err(|_| RTokenError::MutexPoisoned)?
129            .insert(token.clone(), info);
130        Ok(token)
131    }
132
133    // pub fn set_role(&self, token: &str, role: impl Into<Vec<String>>) -> Result<(), RTokenError> {
134    #[cfg(feature = "rbac")]
135    /// ## 日本語
136    ///
137    /// 既存 token の roles を更新します(RBAC 有効時)。
138    ///
139    /// token が存在しない場合でも成功として扱います(冪等)。
140    ///
141    /// ## English
142    ///
143    /// Updates roles for an existing token (RBAC enabled).
144    ///
145    /// This operation is idempotent: if the token does not exist, it is treated as success.
146    pub fn set_roles(&self, token: &str, roles: impl Into<Vec<String>>) -> Result<(), RTokenError> {
147        let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
148        if let Some(info) = store.get_mut(token) {
149            info.roles = roles.into();
150        }
151        Ok(())
152    }
153
154    #[cfg(feature = "rbac")]
155    /// ## 日本語
156    ///
157    /// token に紐づく roles を返します(RBAC 有効時)。
158    ///
159    /// token が存在しない場合は `Ok(None)` を返します。
160    ///
161    /// ## English
162    ///
163    /// Returns roles associated with a token (RBAC enabled).
164    ///
165    /// Returns `Ok(None)` if the token does not exist.
166    pub fn get_roles(&self, token: &str) -> Result<Option<Vec<String>>, RTokenError> {
167        let store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
168        Ok(store.get(token).map(|info| info.roles.clone()))
169    }
170
171    /// ## 日本語
172    ///
173    /// token をインメモリストアから削除して失効させます。
174    ///
175    /// この操作は冪等です。存在しない token を削除しても成功として扱います。
176    /// 内部 mutex が poisoned の場合は [`RTokenError::MutexPoisoned`] を返します。
177    ///
178    /// ## English
179    ///
180    /// Revokes a token by removing it from the in-memory store.
181    ///
182    /// This operation is idempotent: removing a non-existing token is treated as success.
183    /// Returns [`RTokenError::MutexPoisoned`] if the internal mutex is poisoned.
184    pub fn logout(&self, token: &str) -> Result<(), RTokenError> {
185        // self.store.lock().unwrap().remove(token);
186        self.store
187            .lock()
188            .map_err(|_| RTokenError::MutexPoisoned)?
189            .remove(token);
190        Ok(())
191    }
192
193    /// ## 日本語
194    ///
195    /// token に保存されている有効期限(Unix epoch ミリ秒)を返します。
196    ///
197    /// token が存在しない場合は `Ok(None)` を返します。本メソッドは token の期限切れ判定は
198    /// 行いません。
199    ///
200    /// ## English
201    ///
202    /// Returns the stored expiration timestamp for a token (milliseconds since Unix epoch).
203    ///
204    /// Returns `Ok(None)` if the token does not exist. This method does not validate
205    /// whether the token has already expired.
206    pub fn expires_at(&self, token: &str) -> Result<Option<u64>, RTokenError> {
207        let store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
208        Ok(store.get(token).map(|info| info.expire_at))
209    }
210
211    /// ## 日本語
212    ///
213    /// token の残り TTL(秒)を返します。
214    ///
215    /// 返り値:
216    /// - token が存在しない:`Ok(None)`
217    /// - token がすでに期限切れ:`Ok(Some(0))`(本メソッドでは削除しません)
218    ///
219    /// ## English
220    ///
221    /// Returns the remaining TTL in seconds for a token.
222    ///
223    /// Returns:
224    /// - `Ok(None)` when the token does not exist
225    /// - `Ok(Some(0))` when the token is already expired (it is not removed here)
226    pub fn ttl_seconds(&self, token: &str) -> Result<Option<i64>, RTokenError> {
227        let now_ms = Utc::now().timestamp_millis() as u64;
228        let store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
229        let Some(expire_at) = store.get(token).map(|info| info.expire_at) else {
230            return Ok(None);
231        };
232
233        if expire_at <= now_ms {
234            return Ok(Some(0));
235        }
236
237        let remaining_ms = expire_at - now_ms;
238        let remaining_seconds = remaining_ms.div_ceil(1000) as i64;
239        Ok(Some(remaining_seconds))
240    }
241
242    /// ## 日本語
243    ///
244    /// token の有効期限を `now + ttl_seconds` に延長します。
245    ///
246    /// 返り値:
247    /// - token が存在し、期限切れでない:`Ok(true)`
248    /// - token が存在しない、または期限切れ:`Ok(false)`(期限切れの場合は削除します)
249    ///
250    /// ## English
251    ///
252    /// Extends a token's lifetime to `now + ttl_seconds`.
253    ///
254    /// Returns:
255    /// - `Ok(true)` if the token exists and is not expired
256    /// - `Ok(false)` if the token does not exist or is expired (expired tokens are removed)
257    pub fn renew(&self, token: &str, ttl_seconds: u64) -> Result<bool, RTokenError> {
258        let now = Utc::now();
259        let ttl = chrono::Duration::seconds(ttl_seconds as i64);
260        let expire_at = (now + ttl).timestamp_millis() as u64;
261
262        let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
263        let Some(info) = store.get_mut(token) else {
264            return Ok(false);
265        };
266
267        if info.expire_at < Utc::now().timestamp_millis() as u64 {
268            store.remove(token);
269            return Ok(false);
270        }
271
272        info.expire_at = expire_at;
273        Ok(true)
274    }
275
276    /// ## 日本語
277    ///
278    /// 同じユーザー(および roles)に対して新しい token を発行し、古い token を失効させます。
279    ///
280    /// 新しい token の TTL は「現在から `ttl_seconds`」になります。
281    ///
282    /// 古い token が存在しない、または期限切れの場合は `Ok(None)` を返します(期限切れの場合は
283    /// 削除します)。
284    ///
285    /// ## English
286    ///
287    /// Issues a new token for the same user (and roles) and revokes the old token.
288    ///
289    /// The new token will have a lifetime of `ttl_seconds` from now.
290    ///
291    /// Returns `Ok(None)` if the old token does not exist or is expired (expired tokens
292    /// are removed).
293    pub fn rotate(&self, token: &str, ttl_seconds: u64) -> Result<Option<String>, RTokenError> {
294        let now = Utc::now();
295        let ttl = chrono::Duration::seconds(ttl_seconds as i64);
296        let expire_at = (now + ttl).timestamp_millis() as u64;
297
298        let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
299        let Some(info) = store.get(token).cloned() else {
300            return Ok(None);
301        };
302
303        if info.expire_at < Utc::now().timestamp_millis() as u64 {
304            store.remove(token);
305            return Ok(None);
306        }
307
308        let new_token = uuid::Uuid::new_v4().to_string();
309        let new_info = RTokenInfo {
310            user_id: info.user_id,
311            expire_at,
312            roles: info.roles,
313        };
314
315        store.remove(token);
316        store.insert(new_token.clone(), new_info);
317        Ok(Some(new_token))
318    }
319
320    /// ## 日本語
321    ///
322    /// インメモリストアから期限切れの token を削除し、削除した件数を返します。
323    ///
324    /// ## English
325    ///
326    /// Removes expired tokens from the in-memory store and returns how many were removed.
327    pub fn prune_expired(&self) -> Result<usize, RTokenError> {
328        let now = Utc::now().timestamp_millis() as u64;
329        let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
330
331        let original_len = store.len();
332        store.retain(|_token, info| info.expire_at >= now);
333        Ok(original_len - store.len())
334    }
335
336    /// ## 日本語
337    ///
338    /// token を検証し、有効であれば紐づくユーザー ID を返します。
339    ///
340    /// 振る舞い:
341    /// - token が存在し、期限切れでない:`Ok(Some(user_id))`
342    /// - token が存在しない、または期限切れ:`Ok(None)`
343    /// - 期限切れ token は検証時にストアから削除されます
344    ///
345    /// ## English
346    ///
347    /// Validates a token and returns the associated user id if present.
348    ///
349    /// Behavior:
350    /// - Returns `Ok(Some(user_id))` when the token exists and is not expired.
351    /// - Returns `Ok(None)` when the token does not exist or is expired.
352    /// - Expired tokens are removed from the in-memory store during validation.
353    pub fn validate(&self, token: &str) -> Result<Option<String>, RTokenError> {
354        #[cfg(feature = "rbac")]
355        {
356            Ok(self
357                .validate_with_roles(token)?
358                .map(|(user_id, _roles)| user_id))
359        }
360
361        #[cfg(not(feature = "rbac"))]
362        {
363            let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
364            let Some(info) = store.get(token) else {
365                return Ok(None);
366            };
367
368            if info.expire_at < Utc::now().timestamp_millis() as u64 {
369                store.remove(token);
370                return Ok(None);
371            }
372
373            Ok(Some(info.user_id.clone()))
374        }
375    }
376
377    #[cfg(feature = "rbac")]
378    /// ## 日本語
379    ///
380    /// token を検証し、ユーザー ID と roles を返します(RBAC 有効時)。
381    ///
382    /// 期限切れの扱いは [`RTokenManager::validate`] と同じです。
383    ///
384    /// ## English
385    ///
386    /// Validates a token and returns both user id and roles (RBAC enabled).
387    ///
388    /// This has the same expiration behavior as [`RTokenManager::validate`].
389    pub fn validate_with_roles(
390        &self,
391        token: &str,
392    ) -> Result<Option<(String, Vec<String>)>, RTokenError> {
393        let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
394        let Some(info) = store.get(token) else {
395            return Ok(None);
396        };
397
398        if info.expire_at < Utc::now().timestamp_millis() as u64 {
399            store.remove(token);
400            return Ok(None);
401        }
402
403        Ok(Some((info.user_id.clone(), info.roles.clone())))
404    }
405}
406
407/// ## 日本語
408///
409/// actix-web から抽出される認証済みユーザーコンテキストです。
410///
411/// 抽出が成功した場合:
412/// - `id` は [`RTokenManager::login`] に渡したユーザー ID
413/// - `token` はリクエストに含まれていた token の生文字列
414///
415/// token は `Authorization` header から読み取ります。次の形式に対応します:
416/// - `Authorization: <token>`
417/// - `Authorization: Bearer <token>`
418///
419/// ## English
420///
421/// An authenticated request context extracted from actix-web.
422///
423/// If extraction succeeds, `id` is the user id previously passed to
424/// [`RTokenManager::login`], and `token` is the original token from the request.
425///
426/// The token is read from `Authorization` header. Both of the following formats
427/// are accepted:
428/// - `Authorization: <token>`
429/// - `Authorization: Bearer <token>`
430#[cfg(feature = "actix")]
431#[derive(Debug)]
432pub struct RUser {
433    /// ## 日本語
434    ///
435    /// token に紐づくユーザー ID。
436    ///
437    /// ## English
438    ///
439    /// The user id associated with the token.
440    pub id: String,
441
442    /// ## 日本語
443    ///
444    /// リクエストに含まれていた token の生文字列。
445    ///
446    /// ## English
447    ///
448    /// The raw token string from the request.
449    pub token: String,
450    #[cfg(feature = "rbac")]
451    /// ## 日本語
452    ///
453    /// token に紐づく roles(RBAC 有効時)。
454    ///
455    /// ## English
456    ///
457    /// Roles associated with the token (RBAC enabled).
458    pub roles: Vec<String>,
459}
460
461#[cfg(feature = "rbac")]
462impl RUser {
463    /// ## 日本語
464    ///
465    /// 指定した role を持つかどうかを返します。
466    ///
467    /// ## English
468    ///
469    /// Returns whether the user has the given role.
470    pub fn has_role(&self, role: &str) -> bool {
471        self.roles.iter().any(|r| r == role)
472    }
473}
474
475/// ## 日本語
476///
477/// actix-web のリクエストから [`RUser`] を抽出します。
478///
479/// 失敗時:
480/// - 500:`app_data` にマネージャが無い、または mutex が poisoned
481/// - 401:token が無い/無効/期限切れ
482///
483/// ## English
484///
485/// Extracts [`RUser`] from an actix-web request.
486///
487/// Failure modes:
488/// - 500: manager is missing from `app_data`, or mutex is poisoned
489/// - 401: token is missing, invalid, or expired
490#[cfg(feature = "actix")]
491impl actix_web::FromRequest for RUser {
492    type Error = actix_web::Error;
493    type Future = std::future::Ready<Result<Self, Self::Error>>;
494
495    fn from_request(
496        req: &actix_web::HttpRequest,
497        _payload: &mut actix_web::dev::Payload,
498    ) -> Self::Future {
499        use actix_web::web;
500
501        // 獲取管理器
502        let manager = match req.app_data::<web::Data<RTokenManager>>() {
503            Some(m) => m,
504            None => {
505                return std::future::ready(Err(actix_web::error::ErrorInternalServerError(
506                    "Token manager not found",
507                )));
508            }
509        };
510        let token = match crate::extract_token_from_request(req) {
511            Some(token) => token,
512            None => {
513                return std::future::ready(Err(actix_web::error::ErrorUnauthorized(
514                    "Unauthorized",
515                )));
516            }
517        };
518
519        #[cfg(feature = "rbac")]
520        {
521            let user_info = match manager.validate_with_roles(&token) {
522                Ok(user_info) => user_info,
523                Err(_) => {
524                    return std::future::ready(Err(actix_web::error::ErrorInternalServerError(
525                        "Mutex poisoned",
526                    )));
527                }
528            };
529
530            if let Some((user_id, roles)) = user_info {
531                return std::future::ready(Ok(RUser {
532                    id: user_id,
533                    token,
534                    roles,
535                }));
536            }
537
538            std::future::ready(Err(actix_web::error::ErrorUnauthorized("Invalid token")))
539        }
540
541        #[cfg(not(feature = "rbac"))]
542        {
543            let user_id = match manager.validate(&token) {
544                Ok(user_id) => user_id,
545                Err(_) => {
546                    return std::future::ready(Err(actix_web::error::ErrorInternalServerError(
547                        "Mutex poisoned",
548                    )));
549                }
550            };
551
552            if let Some(user_id) = user_id {
553                return std::future::ready(Ok(RUser { id: user_id, token }));
554            }
555
556            std::future::ready(Err(actix_web::error::ErrorUnauthorized("Invalid token")))
557        }
558    }
559}