r_token/
memory.rs

1use crate::models::RTokenInfo;
2use crate::RTokenError;
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) -> Result<String, RTokenError> {
67        let token = uuid::Uuid::new_v4().to_string();
68        // Acquire the write lock and insert the token-user mapping into the store
69        // 获取写锁并将 Token-用户映射关系插入到存储中
70        // #[allow(clippy::unwrap_used)]
71        // self.store.lock().unwrap().insert(token.clone(), id.to_string());
72        let now = Utc::now();
73        let ttl = chrono::Duration::seconds(expire_time as i64);
74        let deadline = now + ttl;
75        let expire_time = deadline.timestamp_millis() as u64;
76        let info = RTokenInfo {
77            user_id: id.to_string(),
78            expire_at: expire_time,
79        };
80        self.store
81            .lock()
82            .map_err(|_| RTokenError::MutexPoisoned)?
83            .insert(token.clone(), info);
84        Ok(token)
85    }
86
87    /// Revokes a token by removing it from the in-memory store.
88    ///
89    /// This operation is idempotent: removing a non-existing token is treated as success.
90    /// Returns [`RTokenError::MutexPoisoned`] if the internal mutex is poisoned.
91    ///
92    /// ## 繁體中文
93    ///
94    /// 從記憶體儲存表中移除 token,以達到註銷效果。
95    ///
96    /// 此操作具冪等性:移除不存在的 token 也視為成功。
97    /// 若內部 mutex 發生 poisoned,會回傳 [`RTokenError::MutexPoisoned`]。
98    pub fn logout(&self, token: &str) -> Result<(), RTokenError> {
99        // self.store.lock().unwrap().remove(token);
100        self.store
101            .lock()
102            .map_err(|_| RTokenError::MutexPoisoned)?
103            .remove(token);
104        Ok(())
105    }
106}
107
108/// An authenticated request context extracted from actix-web.
109///
110/// If extraction succeeds, `id` is the user id previously passed to
111/// [`RTokenManager::login`], and `token` is the original token from the request.
112///
113/// The token is read from `Authorization` header. Both of the following formats
114/// are accepted:
115/// - `Authorization: <token>`
116/// - `Authorization: Bearer <token>`
117///
118/// ## 繁體中文
119///
120/// 由 actix-web 自動抽取的已驗證使用者上下文。
121///
122/// Extractor 成功時:
123/// - `id` 會是先前傳給 [`RTokenManager::login`] 的使用者 id
124/// - `token` 會是請求中帶來的 token 原文
125///
126/// token 會從 `Authorization` header 讀取,支援以下格式:
127/// - `Authorization: <token>`
128/// - `Authorization: Bearer <token>`
129#[cfg(feature = "actix")]
130#[derive(Debug)]
131pub struct RUser {
132    /// The user id associated with the token.
133    ///
134    /// ## 繁體中文
135    ///
136    /// 與 token 綁定的使用者 id。
137    pub id: String,
138
139    /// The raw token string from the request.
140    ///
141    /// ## 繁體中文
142    ///
143    /// 來自請求的 token 字串原文。
144    pub token: String,
145}
146
147/// Extracts [`RUser`] from an actix-web request.
148///
149/// Failure modes:
150/// - 500: manager is missing from `app_data`, or mutex is poisoned
151/// - 401: token is missing, invalid, or expired
152///
153/// ## 繁體中文
154///
155/// 從 actix-web 請求中抽取 [`RUser`]。
156///
157/// 失敗情況:
158/// - 500:`app_data` 中找不到管理器,或 mutex poisoned
159/// - 401:token 缺失、無效、或已過期
160#[cfg(feature = "actix")]
161impl actix_web::FromRequest for RUser {
162    type Error = actix_web::Error;
163    type Future = std::future::Ready<Result<Self, Self::Error>>;
164
165    fn from_request(
166        req: &actix_web::HttpRequest,
167        _payload: &mut actix_web::dev::Payload,
168    ) -> Self::Future {
169        use actix_web::web;
170
171        // 獲取管理器
172        let manager = match req.app_data::<web::Data<RTokenManager>>() {
173            Some(m) => m,
174            None => {
175                return std::future::ready(Err(actix_web::error::ErrorInternalServerError(
176                    "Token manager not found",
177                )));
178            }
179        };
180        // 獲取Token(優先看header中的Authorization)
181        let token = match req
182            .headers()
183            .get("Authorization")
184            .and_then(|h| h.to_str().ok())
185        {
186            Some(token_str) => token_str
187                .strip_prefix("Bearer ")
188                .unwrap_or(token_str)
189                .to_string(),
190            None => {
191                return std::future::ready(Err(actix_web::error::ErrorUnauthorized(
192                    "Unauthorized",
193                )));
194            }
195        };
196
197        // 驗證token
198        let store = match manager.store.lock() {
199            Ok(s) => s,
200            Err(_) => {
201                return std::future::ready(Err(actix_web::error::ErrorInternalServerError(
202                    "Mutex poisoned",
203                )));
204            }
205        };
206
207        match store.get(&token) {
208            Some(id) => {
209                // 檢查token是否過期
210                if id.expire_at < Utc::now().timestamp_millis() as u64 {
211                    return std::future::ready(Err(actix_web::error::ErrorUnauthorized(
212                        "Token expired",
213                    )));
214                }
215                std::future::ready(Ok(RUser {
216                    id: id.user_id.clone(),
217                    token: token.clone(),
218                }))
219                // return ready(Ok(RUser {
220                //     // id: id.clone(),
221                //     id: id.user_id.clone(),
222                //     token: token.clone(),
223                // }));
224            }
225            None => std::future::ready(Err(actix_web::error::ErrorUnauthorized("Invalid token"))),
226        }
227    }
228}