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}