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/// ## 日本語
10///
11/// 認証 token の発行・保存・失効を行うマネージャです。
12///
13/// actix-web のアプリケーション state(例:`web::Data<RTokenManager>`)に保持する想定で、
14/// 内部では `Arc<Mutex<...>>` を使って状態を共有します。そのため `Clone` は同じストアへの
15/// ハンドルを増やすだけです。
16///
17/// token は UUID v4 文字列として生成され、次と紐づきます:
18/// - ユーザー ID(`String`)
19/// - 有効期限(Unix epoch ミリ秒)
20///
21/// ## English
22///
23/// Issues, stores, and revokes authentication tokens.
24///
25/// This type is designed to be stored in actix-web application state
26/// (e.g. `web::Data<RTokenManager>`). Internally it uses an `Arc<Mutex<...>>`,
27/// so `Clone` creates another handle to the same shared store.
28///
29/// Tokens are generated as UUID v4 strings. Each token is associated with:
30/// - a user id (`String`)
31/// - an expiration timestamp (Unix epoch milliseconds)
32#[derive(Clone, Default)]
33pub struct RTokenManager {
34 /// ## 日本語
35 ///
36 /// インメモリの token ストア。
37 ///
38 /// ## English
39 ///
40 /// In-memory token store.
41 // store: Arc<Mutex<HashMap<String, String>>>,
42 store: Arc<Mutex<HashMap<String, RTokenInfo>>>,
43}
44
45impl RTokenManager {
46 /// ## 日本語
47 ///
48 /// 空のマネージャを作成します。
49 ///
50 /// ## English
51 ///
52 /// Creates an empty manager.
53 pub fn new() -> Self {
54 Self {
55 store: Arc::new(Mutex::new(HashMap::new())),
56 }
57 }
58
59 /// ## 日本語
60 ///
61 /// 指定ユーザー ID の新しい token を発行します。
62 ///
63 /// `expire_time` は TTL(秒)として扱います。保存された有効期限が現在時刻より過去であれば、
64 /// token は無効とみなされます。
65 ///
66 /// 内部 mutex が poisoned の場合は [`RTokenError::MutexPoisoned`] を返します。
67 ///
68 /// ## English
69 ///
70 /// Issues a new token for the given user id.
71 ///
72 /// `expire_time` is treated as TTL in seconds. The token will be considered invalid
73 /// once the stored expiration timestamp is earlier than the current time.
74 ///
75 /// Returns [`RTokenError::MutexPoisoned`] if the internal mutex is poisoned.
76 pub fn login(&self, id: &str, expire_time: u64) -> Result<String, RTokenError> {
77 let token = uuid::Uuid::new_v4().to_string();
78 // Acquire the write lock and insert the token-user mapping into the store
79 // 获取写锁并将 Token-用户映射关系插入到存储中
80 // #[allow(clippy::unwrap_used)]
81 // self.store.lock().unwrap().insert(token.clone(), id.to_string());
82 let now = Utc::now();
83 let ttl = chrono::Duration::seconds(expire_time as i64);
84 let deadline = now + ttl;
85 let expire_time = deadline.timestamp_millis() as u64;
86 let info = RTokenInfo {
87 user_id: id.to_string(),
88 expire_at: expire_time,
89 roles: Vec::new(),
90 };
91 self.store
92 .lock()
93 .map_err(|_| RTokenError::MutexPoisoned)?
94 .insert(token.clone(), info);
95 Ok(token)
96 }
97
98 #[cfg(feature = "rbac")]
99 /// ## 日本語
100 ///
101 /// 指定ユーザー ID と役割(roles)を紐づけた新しい token を発行します(RBAC 有効時)。
102 ///
103 /// `expire_time` は TTL(秒)として扱います。
104 ///
105 /// ## English
106 ///
107 /// Issues a new token for the given user id and roles (RBAC enabled).
108 ///
109 /// `expire_time` is treated as TTL in seconds.
110 pub fn login_with_roles(
111 &self,
112 id: &str,
113 expire_time: u64,
114 role: impl Into<Vec<String>>,
115 ) -> Result<String, RTokenError> {
116 let token = uuid::Uuid::new_v4().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 roles: role.into(),
125 };
126 self.store
127 .lock()
128 .map_err(|_| RTokenError::MutexPoisoned)?
129 .insert(token.clone(), info);
130 Ok(token)
131 }
132
133 // pub fn set_role(&self, token: &str, role: impl Into<Vec<String>>) -> Result<(), RTokenError> {
134 #[cfg(feature = "rbac")]
135 /// ## 日本語
136 ///
137 /// 既存 token の roles を更新します(RBAC 有効時)。
138 ///
139 /// token が存在しない場合でも成功として扱います(冪等)。
140 ///
141 /// ## English
142 ///
143 /// Updates roles for an existing token (RBAC enabled).
144 ///
145 /// This operation is idempotent: if the token does not exist, it is treated as success.
146 pub fn set_roles(&self, token: &str, roles: impl Into<Vec<String>>) -> Result<(), RTokenError> {
147 let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
148 if let Some(info) = store.get_mut(token) {
149 info.roles = roles.into();
150 }
151 Ok(())
152 }
153
154 #[cfg(feature = "rbac")]
155 /// ## 日本語
156 ///
157 /// token に紐づく roles を返します(RBAC 有効時)。
158 ///
159 /// token が存在しない場合は `Ok(None)` を返します。
160 ///
161 /// ## English
162 ///
163 /// Returns roles associated with a token (RBAC enabled).
164 ///
165 /// Returns `Ok(None)` if the token does not exist.
166 pub fn get_roles(&self, token: &str) -> Result<Option<Vec<String>>, RTokenError> {
167 let store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
168 Ok(store.get(token).map(|info| info.roles.clone()))
169 }
170
171 /// ## 日本語
172 ///
173 /// token をインメモリストアから削除して失効させます。
174 ///
175 /// この操作は冪等です。存在しない token を削除しても成功として扱います。
176 /// 内部 mutex が poisoned の場合は [`RTokenError::MutexPoisoned`] を返します。
177 ///
178 /// ## English
179 ///
180 /// Revokes a token by removing it from the in-memory store.
181 ///
182 /// This operation is idempotent: removing a non-existing token is treated as success.
183 /// Returns [`RTokenError::MutexPoisoned`] if the internal mutex is poisoned.
184 pub fn logout(&self, token: &str) -> Result<(), RTokenError> {
185 // self.store.lock().unwrap().remove(token);
186 self.store
187 .lock()
188 .map_err(|_| RTokenError::MutexPoisoned)?
189 .remove(token);
190 Ok(())
191 }
192
193 /// ## 日本語
194 ///
195 /// token に保存されている有効期限(Unix epoch ミリ秒)を返します。
196 ///
197 /// token が存在しない場合は `Ok(None)` を返します。本メソッドは token の期限切れ判定は
198 /// 行いません。
199 ///
200 /// ## English
201 ///
202 /// Returns the stored expiration timestamp for a token (milliseconds since Unix epoch).
203 ///
204 /// Returns `Ok(None)` if the token does not exist. This method does not validate
205 /// whether the token has already expired.
206 pub fn expires_at(&self, token: &str) -> Result<Option<u64>, RTokenError> {
207 let store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
208 Ok(store.get(token).map(|info| info.expire_at))
209 }
210
211 /// ## 日本語
212 ///
213 /// token の残り TTL(秒)を返します。
214 ///
215 /// 返り値:
216 /// - token が存在しない:`Ok(None)`
217 /// - token がすでに期限切れ:`Ok(Some(0))`(本メソッドでは削除しません)
218 ///
219 /// ## English
220 ///
221 /// Returns the remaining TTL in seconds for a token.
222 ///
223 /// Returns:
224 /// - `Ok(None)` when the token does not exist
225 /// - `Ok(Some(0))` when the token is already expired (it is not removed here)
226 pub fn ttl_seconds(&self, token: &str) -> Result<Option<i64>, RTokenError> {
227 let now_ms = Utc::now().timestamp_millis() as u64;
228 let store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
229 let Some(expire_at) = store.get(token).map(|info| info.expire_at) else {
230 return Ok(None);
231 };
232
233 if expire_at <= now_ms {
234 return Ok(Some(0));
235 }
236
237 let remaining_ms = expire_at - now_ms;
238 let remaining_seconds = remaining_ms.div_ceil(1000) as i64;
239 Ok(Some(remaining_seconds))
240 }
241
242 /// ## 日本語
243 ///
244 /// token の有効期限を `now + ttl_seconds` に延長します。
245 ///
246 /// 返り値:
247 /// - token が存在し、期限切れでない:`Ok(true)`
248 /// - token が存在しない、または期限切れ:`Ok(false)`(期限切れの場合は削除します)
249 ///
250 /// ## English
251 ///
252 /// Extends a token's lifetime to `now + ttl_seconds`.
253 ///
254 /// Returns:
255 /// - `Ok(true)` if the token exists and is not expired
256 /// - `Ok(false)` if the token does not exist or is expired (expired tokens are removed)
257 pub fn renew(&self, token: &str, ttl_seconds: u64) -> Result<bool, RTokenError> {
258 let now = Utc::now();
259 let ttl = chrono::Duration::seconds(ttl_seconds as i64);
260 let expire_at = (now + ttl).timestamp_millis() as u64;
261
262 let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
263 let Some(info) = store.get_mut(token) else {
264 return Ok(false);
265 };
266
267 if info.expire_at < Utc::now().timestamp_millis() as u64 {
268 store.remove(token);
269 return Ok(false);
270 }
271
272 info.expire_at = expire_at;
273 Ok(true)
274 }
275
276 /// ## 日本語
277 ///
278 /// 同じユーザー(および roles)に対して新しい token を発行し、古い token を失効させます。
279 ///
280 /// 新しい token の TTL は「現在から `ttl_seconds`」になります。
281 ///
282 /// 古い token が存在しない、または期限切れの場合は `Ok(None)` を返します(期限切れの場合は
283 /// 削除します)。
284 ///
285 /// ## English
286 ///
287 /// Issues a new token for the same user (and roles) and revokes the old token.
288 ///
289 /// The new token will have a lifetime of `ttl_seconds` from now.
290 ///
291 /// Returns `Ok(None)` if the old token does not exist or is expired (expired tokens
292 /// are removed).
293 pub fn rotate(&self, token: &str, ttl_seconds: u64) -> Result<Option<String>, RTokenError> {
294 let now = Utc::now();
295 let ttl = chrono::Duration::seconds(ttl_seconds as i64);
296 let expire_at = (now + ttl).timestamp_millis() as u64;
297
298 let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
299 let Some(info) = store.get(token).cloned() else {
300 return Ok(None);
301 };
302
303 if info.expire_at < Utc::now().timestamp_millis() as u64 {
304 store.remove(token);
305 return Ok(None);
306 }
307
308 let new_token = uuid::Uuid::new_v4().to_string();
309 let new_info = RTokenInfo {
310 user_id: info.user_id,
311 expire_at,
312 roles: info.roles,
313 };
314
315 store.remove(token);
316 store.insert(new_token.clone(), new_info);
317 Ok(Some(new_token))
318 }
319
320 /// ## 日本語
321 ///
322 /// インメモリストアから期限切れの token を削除し、削除した件数を返します。
323 ///
324 /// ## English
325 ///
326 /// Removes expired tokens from the in-memory store and returns how many were removed.
327 pub fn prune_expired(&self) -> Result<usize, RTokenError> {
328 let now = Utc::now().timestamp_millis() as u64;
329 let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
330
331 let original_len = store.len();
332 store.retain(|_token, info| info.expire_at >= now);
333 Ok(original_len - store.len())
334 }
335
336 /// ## 日本語
337 ///
338 /// token を検証し、有効であれば紐づくユーザー ID を返します。
339 ///
340 /// 振る舞い:
341 /// - token が存在し、期限切れでない:`Ok(Some(user_id))`
342 /// - token が存在しない、または期限切れ:`Ok(None)`
343 /// - 期限切れ token は検証時にストアから削除されます
344 ///
345 /// ## English
346 ///
347 /// Validates a token and returns the associated user id if present.
348 ///
349 /// Behavior:
350 /// - Returns `Ok(Some(user_id))` when the token exists and is not expired.
351 /// - Returns `Ok(None)` when the token does not exist or is expired.
352 /// - Expired tokens are removed from the in-memory store during validation.
353 pub fn validate(&self, token: &str) -> Result<Option<String>, RTokenError> {
354 #[cfg(feature = "rbac")]
355 {
356 Ok(self
357 .validate_with_roles(token)?
358 .map(|(user_id, _roles)| user_id))
359 }
360
361 #[cfg(not(feature = "rbac"))]
362 {
363 let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
364 let Some(info) = store.get(token) else {
365 return Ok(None);
366 };
367
368 if info.expire_at < Utc::now().timestamp_millis() as u64 {
369 store.remove(token);
370 return Ok(None);
371 }
372
373 Ok(Some(info.user_id.clone()))
374 }
375 }
376
377 #[cfg(feature = "rbac")]
378 /// ## 日本語
379 ///
380 /// token を検証し、ユーザー ID と roles を返します(RBAC 有効時)。
381 ///
382 /// 期限切れの扱いは [`RTokenManager::validate`] と同じです。
383 ///
384 /// ## English
385 ///
386 /// Validates a token and returns both user id and roles (RBAC enabled).
387 ///
388 /// This has the same expiration behavior as [`RTokenManager::validate`].
389 pub fn validate_with_roles(
390 &self,
391 token: &str,
392 ) -> Result<Option<(String, Vec<String>)>, RTokenError> {
393 let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
394 let Some(info) = store.get(token) else {
395 return Ok(None);
396 };
397
398 if info.expire_at < Utc::now().timestamp_millis() as u64 {
399 store.remove(token);
400 return Ok(None);
401 }
402
403 Ok(Some((info.user_id.clone(), info.roles.clone())))
404 }
405}
406
407/// ## 日本語
408///
409/// actix-web から抽出される認証済みユーザーコンテキストです。
410///
411/// 抽出が成功した場合:
412/// - `id` は [`RTokenManager::login`] に渡したユーザー ID
413/// - `token` はリクエストに含まれていた token の生文字列
414///
415/// token は `Authorization` header から読み取ります。次の形式に対応します:
416/// - `Authorization: <token>`
417/// - `Authorization: Bearer <token>`
418///
419/// ## English
420///
421/// An authenticated request context extracted from actix-web.
422///
423/// If extraction succeeds, `id` is the user id previously passed to
424/// [`RTokenManager::login`], and `token` is the original token from the request.
425///
426/// The token is read from `Authorization` header. Both of the following formats
427/// are accepted:
428/// - `Authorization: <token>`
429/// - `Authorization: Bearer <token>`
430#[cfg(feature = "actix")]
431#[derive(Debug)]
432pub struct RUser {
433 /// ## 日本語
434 ///
435 /// token に紐づくユーザー ID。
436 ///
437 /// ## English
438 ///
439 /// The user id associated with the token.
440 pub id: String,
441
442 /// ## 日本語
443 ///
444 /// リクエストに含まれていた token の生文字列。
445 ///
446 /// ## English
447 ///
448 /// The raw token string from the request.
449 pub token: String,
450 #[cfg(feature = "rbac")]
451 /// ## 日本語
452 ///
453 /// token に紐づく roles(RBAC 有効時)。
454 ///
455 /// ## English
456 ///
457 /// Roles associated with the token (RBAC enabled).
458 pub roles: Vec<String>,
459}
460
461#[cfg(feature = "rbac")]
462impl RUser {
463 /// ## 日本語
464 ///
465 /// 指定した role を持つかどうかを返します。
466 ///
467 /// ## English
468 ///
469 /// Returns whether the user has the given role.
470 pub fn has_role(&self, role: &str) -> bool {
471 self.roles.iter().any(|r| r == role)
472 }
473}
474
475/// ## 日本語
476///
477/// actix-web のリクエストから [`RUser`] を抽出します。
478///
479/// 失敗時:
480/// - 500:`app_data` にマネージャが無い、または mutex が poisoned
481/// - 401:token が無い/無効/期限切れ
482///
483/// ## English
484///
485/// Extracts [`RUser`] from an actix-web request.
486///
487/// Failure modes:
488/// - 500: manager is missing from `app_data`, or mutex is poisoned
489/// - 401: token is missing, invalid, or expired
490#[cfg(feature = "actix")]
491impl actix_web::FromRequest for RUser {
492 type Error = actix_web::Error;
493 type Future = std::future::Ready<Result<Self, Self::Error>>;
494
495 fn from_request(
496 req: &actix_web::HttpRequest,
497 _payload: &mut actix_web::dev::Payload,
498 ) -> Self::Future {
499 use actix_web::web;
500
501 // 獲取管理器
502 let manager = match req.app_data::<web::Data<RTokenManager>>() {
503 Some(m) => m,
504 None => {
505 return std::future::ready(Err(actix_web::error::ErrorInternalServerError(
506 "Token manager not found",
507 )));
508 }
509 };
510 let token = match crate::extract_token_from_request(req) {
511 Some(token) => token,
512 None => {
513 return std::future::ready(Err(actix_web::error::ErrorUnauthorized(
514 "Unauthorized",
515 )));
516 }
517 };
518
519 #[cfg(feature = "rbac")]
520 {
521 let user_info = match manager.validate_with_roles(&token) {
522 Ok(user_info) => user_info,
523 Err(_) => {
524 return std::future::ready(Err(actix_web::error::ErrorInternalServerError(
525 "Mutex poisoned",
526 )));
527 }
528 };
529
530 if let Some((user_id, roles)) = user_info {
531 return std::future::ready(Ok(RUser {
532 id: user_id,
533 token,
534 roles,
535 }));
536 }
537
538 std::future::ready(Err(actix_web::error::ErrorUnauthorized("Invalid token")))
539 }
540
541 #[cfg(not(feature = "rbac"))]
542 {
543 let user_id = match manager.validate(&token) {
544 Ok(user_id) => user_id,
545 Err(_) => {
546 return std::future::ready(Err(actix_web::error::ErrorInternalServerError(
547 "Mutex poisoned",
548 )));
549 }
550 };
551
552 if let Some(user_id) = user_id {
553 return std::future::ready(Ok(RUser { id: user_id, token }));
554 }
555
556 std::future::ready(Err(actix_web::error::ErrorUnauthorized("Invalid token")))
557 }
558 }
559}