r_token/
lib.rs

1#![deny(clippy::unwrap_used)]
2#![deny(clippy::expect_used)]
3#![deny(clippy::panic)]
4#![deny(clippy::todo)]
5#![deny(clippy::unimplemented)]
6#![deny(clippy::empty_loop)]
7#![deny(clippy::indexing_slicing)]
8#![deny(unused)]
9//! # r-token
10//!
11//! A small, in-memory token authentication helper for actix-web.
12//!
13//! The library exposes two main building blocks:
14//! - [`RTokenManager`]: issues and revokes tokens (UUID v4) and keeps an in-memory store.
15//! - [`RUser`]: an actix-web extractor that validates `Authorization` automatically.
16//!
17//! ## How authentication works
18//!
19//! 1. Your login handler calls [`RTokenManager::login`] with a user id and a TTL (seconds).
20//! 2. The token is returned to the client (typically as plain text or JSON).
21//! 3. The client sends the token back via `Authorization` header:
22//!    - `Authorization: <token>`
23//!    - `Authorization: Bearer <token>`
24//! 4. Any handler that declares an [`RUser`] parameter becomes a protected endpoint. If extraction
25//!    succeeds, the request is considered authenticated; otherwise actix-web returns an error.
26//!
27//! ## 繁體中文
28//!
29//! 這是一個為 actix-web 設計的輕量級、純記憶體 token 驗證輔助庫。
30//!
31//! 主要由兩個元件構成:
32//! - [`RTokenManager`]: 產生/註銷 token(UUID v4),並在記憶體中維護映射表。
33//! - [`RUser`]: actix-web 的 Extractor,會自動從 `Authorization` 讀取並驗證 token。
34//!
35//! ## 驗證流程
36//!
37//! 1. 登入端點呼叫 [`RTokenManager::login`],傳入使用者 id 與 TTL(秒)。
38//! 2. token 回傳給客戶端(常見為純文字或 JSON)。
39//! 3. 客戶端透過 `Authorization` header 送回 token(支援 `Bearer ` 前綴或不帶前綴)。
40//! 4. 任何 handler 只要宣告 [`RUser`] 參數即視為受保護端點;Extractor 成功才會進入 handler。
41
42mod models;
43
44pub use crate::models::RTokenError;
45use crate::models::RTokenInfo;
46use actix_web::{FromRequest, HttpRequest, web};
47use chrono::Utc;
48use std::future::{Ready, ready};
49use std::{
50    collections::HashMap,
51    sync::{Arc, Mutex},
52};
53
54/// Issues, stores, and revokes authentication tokens.
55///
56/// This type is designed to be stored in actix-web application state
57/// (e.g. `web::Data<RTokenManager>`). Internally it uses an `Arc<Mutex<...>>`,
58/// so `Clone` creates another handle to the same shared store.
59///
60/// Tokens are generated as UUID v4 strings. Each token is associated with:
61/// - a user id (`String`)
62/// - an expiration timestamp (Unix epoch milliseconds)
63///
64/// ## 繁體中文
65///
66/// 負責簽發、儲存與註銷 token 的管理器。
67///
68/// 一般會放在 actix-web 的 application state 中(例如 `web::Data<RTokenManager>`)。
69/// 內部以 `Arc<Mutex<...>>` 共享狀態,因此 `Clone` 只是在同一份映射表上增加一個引用。
70///
71/// token 以 UUID v4 字串產生,並會綁定:
72/// - 使用者 id(`String`)
73/// - 到期時間(Unix epoch 毫秒)
74#[derive(Clone, Default)]
75pub struct RTokenManager {
76    /// In-memory token store.
77    ///
78    /// ## 繁體中文
79    ///
80    /// 記憶體中的 token 儲存表。
81    // store: Arc<Mutex<HashMap<String, String>>>,
82    store: Arc<Mutex<HashMap<String, RTokenInfo>>>,
83}
84
85impl RTokenManager {
86    /// Creates an empty manager.
87    ///
88    /// ## 繁體中文
89    ///
90    /// 建立一個空的管理器。
91    pub fn new() -> Self {
92        Self {
93            store: Arc::new(Mutex::new(HashMap::new())),
94        }
95    }
96
97    /// Issues a new token for the given user id.
98    ///
99    /// `expire_time` is treated as TTL in seconds. The token will be considered invalid
100    /// once the stored expiration timestamp is earlier than the current time.
101    ///
102    /// Returns [`RTokenError::MutexPoisoned`] if the internal mutex is poisoned.
103    ///
104    /// ## 繁體中文
105    ///
106    /// 為指定使用者 id 簽發新 token。
107    ///
108    /// `expire_time` 會被視為 TTL(秒)。當儲存的到期時間早於目前時間時,token 會被視為無效。
109    ///
110    /// 若內部 mutex 發生 poisoned,會回傳 [`RTokenError::MutexPoisoned`]。
111    pub fn login(&self, id: &str, expire_time: u64) -> Result<String, RTokenError> {
112        let token = uuid::Uuid::new_v4().to_string();
113        // Acquire the write lock and insert the token-user mapping into the store
114        // 获取写锁并将 Token-用户映射关系插入到存储中
115        // #[allow(clippy::unwrap_used)]
116        // self.store.lock().unwrap().insert(token.clone(), id.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        };
125        self.store
126            .lock()
127            .map_err(|_| RTokenError::MutexPoisoned)?
128            .insert(token.clone(), info);
129        Ok(token)
130    }
131
132    /// Revokes a token by removing it from the in-memory store.
133    ///
134    /// This operation is idempotent: removing a non-existing token is treated as success.
135    /// Returns [`RTokenError::MutexPoisoned`] if the internal mutex is poisoned.
136    ///
137    /// ## 繁體中文
138    ///
139    /// 從記憶體儲存表中移除 token,以達到註銷效果。
140    ///
141    /// 此操作具冪等性:移除不存在的 token 也視為成功。
142    /// 若內部 mutex 發生 poisoned,會回傳 [`RTokenError::MutexPoisoned`]。
143    pub fn logout(&self, token: &str) -> Result<(), RTokenError> {
144        // self.store.lock().unwrap().remove(token);
145        self.store
146            .lock()
147            .map_err(|_| RTokenError::MutexPoisoned)?
148            .remove(token);
149        Ok(())
150    }
151}
152
153/// An authenticated request context extracted from actix-web.
154///
155/// If extraction succeeds, `id` is the user id previously passed to
156/// [`RTokenManager::login`], and `token` is the original token from the request.
157///
158/// The token is read from `Authorization` header. Both of the following formats
159/// are accepted:
160/// - `Authorization: <token>`
161/// - `Authorization: Bearer <token>`
162///
163/// ## 繁體中文
164///
165/// 由 actix-web 自動抽取的已驗證使用者上下文。
166///
167/// Extractor 成功時:
168/// - `id` 會是先前傳給 [`RTokenManager::login`] 的使用者 id
169/// - `token` 會是請求中帶來的 token 原文
170///
171/// token 會從 `Authorization` header 讀取,支援以下格式:
172/// - `Authorization: <token>`
173/// - `Authorization: Bearer <token>`
174#[derive(Debug)]
175pub struct RUser {
176    /// The user id associated with the token.
177    ///
178    /// ## 繁體中文
179    ///
180    /// 與 token 綁定的使用者 id。
181    pub id: String,
182
183    /// The raw token string from the request.
184    ///
185    /// ## 繁體中文
186    ///
187    /// 來自請求的 token 字串原文。
188    pub token: String,
189}
190
191/// Extracts [`RUser`] from an actix-web request.
192///
193/// Failure modes:
194/// - 500: manager is missing from `app_data`, or mutex is poisoned
195/// - 401: token is missing, invalid, or expired
196///
197/// ## 繁體中文
198///
199/// 從 actix-web 請求中抽取 [`RUser`]。
200///
201/// 失敗情況:
202/// - 500:`app_data` 中找不到管理器,或 mutex poisoned
203/// - 401:token 缺失、無效、或已過期
204impl FromRequest for RUser {
205    type Error = actix_web::Error;
206    type Future = Ready<Result<Self, Self::Error>>;
207
208    fn from_request(req: &HttpRequest, _payload: &mut actix_web::dev::Payload) -> Self::Future {
209        // 獲取管理器
210        let manager = match req.app_data::<web::Data<RTokenManager>>() {
211            Some(m) => m,
212            None => {
213                return ready(Err(actix_web::error::ErrorInternalServerError(
214                    "Token manager not found",
215                )));
216            }
217        };
218        // 獲取Token(優先看header中的Authorization)
219        let token = match req
220            .headers()
221            .get("Authorization")
222            .and_then(|h| h.to_str().ok())
223        {
224            Some(token_str) => token_str
225                .strip_prefix("Bearer ")
226                .unwrap_or(token_str)
227                .to_string(),
228            None => return ready(Err(actix_web::error::ErrorUnauthorized("Unauthorized"))),
229        };
230
231        // 驗證token
232        let store = match manager.store.lock() {
233            Ok(s) => s,
234            Err(_) => {
235                return ready(Err(actix_web::error::ErrorInternalServerError(
236                    "Mutex poisoned",
237                )));
238            }
239        };
240
241        match store.get(&token) {
242            Some(id) => {
243                // 檢查token是否過期
244                if id.expire_at < Utc::now().timestamp_millis() as u64 {
245                    return ready(Err(actix_web::error::ErrorUnauthorized("Token expired")));
246                }
247                ready(Ok(RUser {
248                    id: id.user_id.clone(),
249                    token: token.clone(),
250                }))
251                // return ready(Ok(RUser {
252                //     // id: id.clone(),
253                //     id: id.user_id.clone(),
254                //     token: token.clone(),
255                // }));
256            }
257            None => ready(Err(actix_web::error::ErrorUnauthorized("Invalid token"))),
258        }
259    }
260}