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