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, RTokenInfo>>>,
42}
43
44impl RTokenManager {
45 /// ## 日本語
46 ///
47 /// 空のマネージャを作成します。
48 ///
49 /// ## English
50 ///
51 /// Creates an empty manager.
52 pub fn new() -> Self {
53 Self {
54 store: Arc::new(Mutex::new(HashMap::new())),
55 }
56 }
57
58 /// ## 日本語
59 ///
60 /// 指定ユーザー ID の新しい token を発行します。
61 ///
62 /// `expire_time` は TTL(秒)として扱います。保存された有効期限が現在時刻より過去であれば、
63 /// token は無効とみなされます。
64 ///
65 /// 内部 mutex が poisoned の場合は [`RTokenError::MutexPoisoned`] を返します。
66 ///
67 /// ## English
68 ///
69 /// Issues a new token for the given user id.
70 ///
71 /// `expire_time` is treated as TTL in seconds. The token will be considered invalid
72 /// once the stored expiration timestamp is earlier than the current time.
73 ///
74 /// Returns [`RTokenError::MutexPoisoned`] if the internal mutex is poisoned.
75 pub fn login(&self, id: &str, expire_time: u64) -> Result<String, RTokenError> {
76 // 日本語: token は UUID v4 文字列で生成する
77 // English: Tokens are generated as UUID v4 strings
78 let token = uuid::Uuid::new_v4().to_string();
79
80 // 日本語: expire_time は秒 TTL として扱い、現在時刻から期限 (ms) を計算する
81 // English: Treat expire_time as TTL seconds and compute deadline in milliseconds
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
87 // 日本語: token と紐づく情報(user_id / expire_at / roles)を作る
88 // English: Build token info (user_id / expire_at / roles)
89 let info = RTokenInfo {
90 user_id: id.to_string(),
91 expire_at: expire_time,
92 roles: Vec::new(),
93 };
94
95 // 日本語: mutex をロックしてストアに保存する(poisoned はライブラリのエラーに変換)
96 // English: Lock the store mutex and insert (map poisoned to library error)
97 self.store
98 .lock()
99 .map_err(|_| RTokenError::MutexPoisoned)?
100 .insert(token.clone(), info);
101 Ok(token)
102 }
103
104 #[cfg(feature = "rbac")]
105 /// ## 日本語
106 ///
107 /// 指定ユーザー ID と役割(roles)を紐づけた新しい token を発行します(RBAC 有効時)。
108 ///
109 /// `expire_time` は TTL(秒)として扱います。
110 ///
111 /// ## English
112 ///
113 /// Issues a new token for the given user id and roles (RBAC enabled).
114 ///
115 /// `expire_time` is treated as TTL in seconds.
116 pub fn login_with_roles(
117 &self,
118 id: &str,
119 expire_time: u64,
120 role: impl Into<Vec<String>>,
121 ) -> Result<String, RTokenError> {
122 // 日本語: token は UUID v4 文字列で生成する
123 // English: Tokens are generated as UUID v4 strings
124 let token = uuid::Uuid::new_v4().to_string();
125
126 // 日本語: expire_time は秒 TTL として扱い、現在時刻から期限 (ms) を計算する
127 // English: Treat expire_time as TTL seconds and compute deadline in milliseconds
128 let now = Utc::now();
129 let ttl = chrono::Duration::seconds(expire_time as i64);
130 let deadline = now + ttl;
131 let expire_time = deadline.timestamp_millis() as u64;
132
133 // 日本語: roles を含む token 情報を作って保存する
134 // English: Build token info including roles and store it
135 let info = RTokenInfo {
136 user_id: id.to_string(),
137 expire_at: expire_time,
138 roles: role.into(),
139 };
140 self.store
141 .lock()
142 .map_err(|_| RTokenError::MutexPoisoned)?
143 .insert(token.clone(), info);
144 Ok(token)
145 }
146
147 // pub fn set_role(&self, token: &str, role: impl Into<Vec<String>>) -> Result<(), RTokenError> {
148 #[cfg(feature = "rbac")]
149 /// ## 日本語
150 ///
151 /// 既存 token の roles を更新します(RBAC 有効時)。
152 ///
153 /// token が存在しない場合でも成功として扱います(冪等)。
154 ///
155 /// ## English
156 ///
157 /// Updates roles for an existing token (RBAC enabled).
158 ///
159 /// This operation is idempotent: if the token does not exist, it is treated as success.
160 pub fn set_roles(&self, token: &str, roles: impl Into<Vec<String>>) -> Result<(), RTokenError> {
161 // 日本語: まずストアをロックする
162 // English: Lock the store first
163 let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
164 if let Some(info) = store.get_mut(token) {
165 // 日本語: token が存在する場合のみ roles を更新する
166 // English: Update roles only when the token exists
167 info.roles = roles.into();
168 }
169 Ok(())
170 }
171
172 #[cfg(feature = "rbac")]
173 /// ## 日本語
174 ///
175 /// token に紐づく roles を返します(RBAC 有効時)。
176 ///
177 /// token が存在しない場合は `Ok(None)` を返します。
178 ///
179 /// ## English
180 ///
181 /// Returns roles associated with a token (RBAC enabled).
182 ///
183 /// Returns `Ok(None)` if the token does not exist.
184 pub fn get_roles(&self, token: &str) -> Result<Option<Vec<String>>, RTokenError> {
185 // 日本語: 読み取りのためストアをロックする
186 // English: Lock the store for reading
187 let store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
188 // 日本語: Vec を返すため clone する(ストア内部を露出しない)
189 // English: Clone the Vec to avoid exposing internal storage
190 Ok(store.get(token).map(|info| info.roles.clone()))
191 }
192
193 /// ## 日本語
194 ///
195 /// token をインメモリストアから削除して失効させます。
196 ///
197 /// この操作は冪等です。存在しない token を削除しても成功として扱います。
198 /// 内部 mutex が poisoned の場合は [`RTokenError::MutexPoisoned`] を返します。
199 ///
200 /// ## English
201 ///
202 /// Revokes a token by removing it from the in-memory store.
203 ///
204 /// This operation is idempotent: removing a non-existing token is treated as success.
205 /// Returns [`RTokenError::MutexPoisoned`] if the internal mutex is poisoned.
206 pub fn logout(&self, token: &str) -> Result<(), RTokenError> {
207 // 日本語: remove は「存在しない token」でも何もしないため冪等
208 // English: remove is idempotent (no-op for non-existing tokens)
209 self.store
210 .lock()
211 .map_err(|_| RTokenError::MutexPoisoned)?
212 .remove(token);
213 Ok(())
214 }
215
216 /// ## 日本語
217 ///
218 /// token に保存されている有効期限(Unix epoch ミリ秒)を返します。
219 ///
220 /// token が存在しない場合は `Ok(None)` を返します。本メソッドは token の期限切れ判定は
221 /// 行いません。
222 ///
223 /// ## English
224 ///
225 /// Returns the stored expiration timestamp for a token (milliseconds since Unix epoch).
226 ///
227 /// Returns `Ok(None)` if the token does not exist. This method does not validate
228 /// whether the token has already expired.
229 pub fn expires_at(&self, token: &str) -> Result<Option<u64>, RTokenError> {
230 // 日本語: token の存在確認のみ(期限切れ判定はしない)
231 // English: Only checks existence (does not validate expiration)
232 let store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
233 Ok(store.get(token).map(|info| info.expire_at))
234 }
235
236 /// ## 日本語
237 ///
238 /// token の残り TTL(秒)を返します。
239 ///
240 /// 返り値:
241 /// - token が存在しない:`Ok(None)`
242 /// - token がすでに期限切れ:`Ok(Some(0))`(本メソッドでは削除しません)
243 ///
244 /// ## English
245 ///
246 /// Returns the remaining TTL in seconds for a token.
247 ///
248 /// Returns:
249 /// - `Ok(None)` when the token does not exist
250 /// - `Ok(Some(0))` when the token is already expired (it is not removed here)
251 pub fn ttl_seconds(&self, token: &str) -> Result<Option<i64>, RTokenError> {
252 // 日本語: 現在時刻 (ms) と保存された expire_at (ms) の差から残り秒数を計算する
253 // English: Compute remaining seconds from now_ms and stored expire_at (milliseconds)
254 let now_ms = Utc::now().timestamp_millis() as u64;
255 let store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
256 let Some(expire_at) = store.get(token).map(|info| info.expire_at) else {
257 return Ok(None);
258 };
259
260 if expire_at <= now_ms {
261 return Ok(Some(0));
262 }
263
264 let remaining_ms = expire_at - now_ms;
265 // 日本語: ms を秒に変換(端数は切り上げ)
266 // English: Convert ms to seconds (ceil)
267 let remaining_seconds = remaining_ms.div_ceil(1000) as i64;
268 Ok(Some(remaining_seconds))
269 }
270
271 /// ## 日本語
272 ///
273 /// token の有効期限を `now + ttl_seconds` に延長します。
274 ///
275 /// 返り値:
276 /// - token が存在し、期限切れでない:`Ok(true)`
277 /// - token が存在しない、または期限切れ:`Ok(false)`(期限切れの場合は削除します)
278 ///
279 /// ## English
280 ///
281 /// Extends a token's lifetime to `now + ttl_seconds`.
282 ///
283 /// Returns:
284 /// - `Ok(true)` if the token exists and is not expired
285 /// - `Ok(false)` if the token does not exist or is expired (expired tokens are removed)
286 pub fn renew(&self, token: &str, ttl_seconds: u64) -> Result<bool, RTokenError> {
287 // 日本語: now + ttl_seconds で新しい expire_at (ms) を計算する
288 // English: Compute new expire_at (ms) as now + ttl_seconds
289 let now = Utc::now();
290 let ttl = chrono::Duration::seconds(ttl_seconds as i64);
291 let expire_at = (now + ttl).timestamp_millis() as u64;
292
293 // 日本語: 対象 token を更新するためストアをロックする
294 // English: Lock the store to update the token
295 let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
296 let Some(info) = store.get_mut(token) else {
297 return Ok(false);
298 };
299
300 // 日本語: 期限切れは renew 失敗として扱い、ストアから削除する
301 // English: Treat expired token as failure and remove it from store
302 if info.expire_at < Utc::now().timestamp_millis() as u64 {
303 store.remove(token);
304 return Ok(false);
305 }
306
307 // 日本語: 有効な token の期限を更新する
308 // English: Update expiration for valid token
309 info.expire_at = expire_at;
310 Ok(true)
311 }
312
313 /// ## 日本語
314 ///
315 /// 同じユーザー(および roles)に対して新しい token を発行し、古い token を失効させます。
316 ///
317 /// 新しい token の TTL は「現在から `ttl_seconds`」になります。
318 ///
319 /// 古い token が存在しない、または期限切れの場合は `Ok(None)` を返します(期限切れの場合は
320 /// 削除します)。
321 ///
322 /// ## English
323 ///
324 /// Issues a new token for the same user (and roles) and revokes the old token.
325 ///
326 /// The new token will have a lifetime of `ttl_seconds` from now.
327 ///
328 /// Returns `Ok(None)` if the old token does not exist or is expired (expired tokens
329 /// are removed).
330 pub fn rotate(&self, token: &str, ttl_seconds: u64) -> Result<Option<String>, RTokenError> {
331 // 日本語: 新 token の期限を now + ttl_seconds で計算する
332 // English: Compute new token expiration as now + ttl_seconds
333 let now = Utc::now();
334 let ttl = chrono::Duration::seconds(ttl_seconds as i64);
335 let expire_at = (now + ttl).timestamp_millis() as u64;
336
337 // 日本語: old token の情報を参照して新 token に引き継ぐため clone する
338 // English: Clone old info so we can reuse it for the new token
339 let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
340 let Some(info) = store.get(token).cloned() else {
341 return Ok(None);
342 };
343
344 // 日本語: old token が期限切れなら削除して None を返す
345 // English: If old token expired, remove it and return None
346 if info.expire_at < Utc::now().timestamp_millis() as u64 {
347 store.remove(token);
348 return Ok(None);
349 }
350
351 // 日本語: 新 token を生成して情報を引き継ぐ(user_id / roles)
352 // English: Generate a new token and carry over user_id / roles
353 let new_token = uuid::Uuid::new_v4().to_string();
354 let new_info = RTokenInfo {
355 user_id: info.user_id,
356 expire_at,
357 roles: info.roles,
358 };
359
360 // 日本語: old token を削除し、新 token を追加する
361 // English: Remove old token and insert the new token
362 store.remove(token);
363 store.insert(new_token.clone(), new_info);
364 Ok(Some(new_token))
365 }
366
367 /// ## 日本語
368 ///
369 /// インメモリストアから期限切れの token を削除し、削除した件数を返します。
370 ///
371 /// ## English
372 ///
373 /// Removes expired tokens from the in-memory store and returns how many were removed.
374 pub fn prune_expired(&self) -> Result<usize, RTokenError> {
375 // 日本語: retain を使って期限切れのエントリを一括削除する
376 // English: Use retain to bulk-remove expired entries
377 let now = Utc::now().timestamp_millis() as u64;
378 let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
379
380 let original_len = store.len();
381 store.retain(|_token, info| info.expire_at >= now);
382 Ok(original_len - store.len())
383 }
384
385 /// ## 日本語
386 ///
387 /// token を検証し、有効であれば紐づくユーザー ID を返します。
388 ///
389 /// 振る舞い:
390 /// - token が存在し、期限切れでない:`Ok(Some(user_id))`
391 /// - token が存在しない、または期限切れ:`Ok(None)`
392 /// - 期限切れ token は検証時にストアから削除されます
393 ///
394 /// ## English
395 ///
396 /// Validates a token and returns the associated user id if present.
397 ///
398 /// Behavior:
399 /// - Returns `Ok(Some(user_id))` when the token exists and is not expired.
400 /// - Returns `Ok(None)` when the token does not exist or is expired.
401 /// - Expired tokens are removed from the in-memory store during validation.
402 pub fn validate(&self, token: &str) -> Result<Option<String>, RTokenError> {
403 #[cfg(feature = "rbac")]
404 {
405 Ok(self
406 .validate_with_roles(token)?
407 .map(|(user_id, _roles)| user_id))
408 }
409
410 #[cfg(not(feature = "rbac"))]
411 {
412 // 日本語: 検証時は期限切れを掃除するため書き込みロックを取る
413 // English: Take a write lock so we can remove expired tokens during validation
414 let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
415 let Some(info) = store.get(token) else {
416 return Ok(None);
417 };
418
419 // 日本語: 期限切れなら削除して無効扱いにする
420 // English: If expired, remove and treat as invalid
421 if info.expire_at < Utc::now().timestamp_millis() as u64 {
422 store.remove(token);
423 return Ok(None);
424 }
425
426 // 日本語: 有効 token の user_id を返す
427 // English: Return user_id for a valid token
428 Ok(Some(info.user_id.clone()))
429 }
430 }
431
432 #[cfg(feature = "rbac")]
433 /// ## 日本語
434 ///
435 /// token を検証し、ユーザー ID と roles を返します(RBAC 有効時)。
436 ///
437 /// 期限切れの扱いは [`RTokenManager::validate`] と同じです。
438 ///
439 /// ## English
440 ///
441 /// Validates a token and returns both user id and roles (RBAC enabled).
442 ///
443 /// This has the same expiration behavior as [`RTokenManager::validate`].
444 pub fn validate_with_roles(
445 &self,
446 token: &str,
447 ) -> Result<Option<(String, Vec<String>)>, RTokenError> {
448 // 日本語: 検証時に期限切れの削除があり得るため書き込みロックを取る
449 // English: Take a write lock because we may remove expired tokens
450 let mut store = self.store.lock().map_err(|_| RTokenError::MutexPoisoned)?;
451 let Some(info) = store.get(token) else {
452 return Ok(None);
453 };
454
455 // 日本語: 期限切れなら削除して無効扱いにする
456 // English: If expired, remove and treat as invalid
457 if info.expire_at < Utc::now().timestamp_millis() as u64 {
458 store.remove(token);
459 return Ok(None);
460 }
461
462 // 日本語: user_id と roles を返す(clone して内部を露出しない)
463 // English: Return user_id and roles (clone to avoid exposing internals)
464 Ok(Some((info.user_id.clone(), info.roles.clone())))
465 }
466}
467
468/// ## 日本語
469///
470/// actix-web / axum から抽出される認証済みユーザーコンテキストです。
471///
472/// 抽出が成功した場合:
473/// - `id` は [`RTokenManager::login`] に渡したユーザー ID
474/// - `token` はリクエストに含まれていた token の生文字列
475///
476/// token は `Authorization` header から読み取ります。次の形式に対応します:
477/// - `Authorization: <token>`
478/// - `Authorization: Bearer <token>`
479///
480/// ## English
481///
482/// An authenticated request context extracted from actix-web / axum.
483///
484/// If extraction succeeds, `id` is the user id previously passed to
485/// [`RTokenManager::login`], and `token` is the original token from the request.
486///
487/// The token is read from `Authorization` header. Both of the following formats
488/// are accepted:
489/// - `Authorization: <token>`
490/// - `Authorization: Bearer <token>`
491#[cfg(any(feature = "actix", feature = "axum"))]
492#[derive(Debug)]
493pub struct RUser {
494 /// ## 日本語
495 ///
496 /// token に紐づくユーザー ID。
497 ///
498 /// ## English
499 ///
500 /// The user id associated with the token.
501 pub id: String,
502
503 /// ## 日本語
504 ///
505 /// リクエストに含まれていた token の生文字列。
506 ///
507 /// ## English
508 ///
509 /// The raw token string from the request.
510 pub token: String,
511 #[cfg(feature = "rbac")]
512 /// ## 日本語
513 ///
514 /// token に紐づく roles(RBAC 有効時)。
515 ///
516 /// ## English
517 ///
518 /// Roles associated with the token (RBAC enabled).
519 pub roles: Vec<String>,
520}
521
522#[cfg(feature = "rbac")]
523impl RUser {
524 /// ## 日本語
525 ///
526 /// 指定した role を持つかどうかを返します。
527 ///
528 /// ## English
529 ///
530 /// Returns whether the user has the given role.
531 pub fn has_role(&self, role: &str) -> bool {
532 self.roles.iter().any(|r| r == role)
533 }
534}
535
536/// ## 日本語
537///
538/// actix-web のリクエストから [`RUser`] を抽出します。
539///
540/// 失敗時:
541/// - 500:`app_data` にマネージャが無い、または mutex が poisoned
542/// - 401:token が無い/無効/期限切れ
543///
544/// ## English
545///
546/// Extracts [`RUser`] from an actix-web request.
547///
548/// Failure modes:
549/// - 500: manager is missing from `app_data`, or mutex is poisoned
550/// - 401: token is missing, invalid, or expired
551#[cfg(feature = "actix")]
552impl actix_web::FromRequest for RUser {
553 type Error = actix_web::Error;
554 type Future = std::future::Ready<Result<Self, Self::Error>>;
555
556 fn from_request(
557 req: &actix_web::HttpRequest,
558 _payload: &mut actix_web::dev::Payload,
559 ) -> Self::Future {
560 use actix_web::web;
561
562 // 日本語: app_data からマネージャを取得する
563 // English: Fetch the manager from app_data
564 let manager = match req.app_data::<web::Data<RTokenManager>>() {
565 Some(m) => m,
566 None => {
567 return std::future::ready(Err(actix_web::error::ErrorInternalServerError(
568 "Token manager not found",
569 )));
570 }
571 };
572 let token = match crate::extract_token_from_request(req) {
573 Some(token) => token,
574 None => {
575 return std::future::ready(Err(actix_web::error::ErrorUnauthorized(
576 "Unauthorized",
577 )));
578 }
579 };
580
581 #[cfg(feature = "rbac")]
582 {
583 let user_info = match manager.validate_with_roles(&token) {
584 Ok(user_info) => user_info,
585 Err(_) => {
586 return std::future::ready(Err(actix_web::error::ErrorInternalServerError(
587 "Mutex poisoned",
588 )));
589 }
590 };
591
592 if let Some((user_id, roles)) = user_info {
593 return std::future::ready(Ok(RUser {
594 id: user_id,
595 token,
596 roles,
597 }));
598 }
599
600 std::future::ready(Err(actix_web::error::ErrorUnauthorized("Invalid token")))
601 }
602
603 #[cfg(not(feature = "rbac"))]
604 {
605 let user_id = match manager.validate(&token) {
606 Ok(user_id) => user_id,
607 Err(_) => {
608 return std::future::ready(Err(actix_web::error::ErrorInternalServerError(
609 "Mutex poisoned",
610 )));
611 }
612 };
613
614 if let Some(user_id) = user_id {
615 return std::future::ready(Ok(RUser { id: user_id, token }));
616 }
617
618 std::future::ready(Err(actix_web::error::ErrorUnauthorized("Invalid token")))
619 }
620 }
621}