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