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}