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 lightweight, zero-boilerplate authentication library for actix-web applications.
12//!
13//! ## Overview
14//!
15//! r-token provides a minimalist approach to HTTP authentication in Rust web applications.
16//! Inspired by Java's [Sa-Token](https://sa-token.cc/), it leverages actix-web's extractor
17//! pattern to enable "parameter-as-authentication" - simply declare [`RUser`] in your handler
18//! parameters, and authentication is handled automatically.
19//!
20//! ## Key Features
21//!
22//! - **Zero Boilerplate**: No manual token validation or middleware setup required
23//! - **Type-Safe**: Leverages Rust's type system - if your handler receives [`RUser`], the user is authenticated
24//! - **Non-Invasive**: Uses actix-web's [`FromRequest`] trait for seamless integration
25//! - **Thread-Safe**: Built on `Arc<Mutex<HashMap>>` for safe concurrent access
26//! - **Minimalist API**: Only two core methods - [`login`](RTokenManager::login) and [`logout`](RTokenManager::logout)
27//!
28//! ## Quick Start
29//!
30//! Add r-token to your `Cargo.toml`:
31//!
32//! ```toml
33//! [dependencies]
34//! r-token = "0.1"
35//! actix-web = "4"
36//! ```
37//!
38//! Then create your authentication endpoints:
39//!
40//! ```rust,no_run
41//! use actix_web::{get, post, web, HttpResponse, HttpServer, App};
42//! use r_token::{RTokenManager, RUser, RTokenError};
43//!
44//! #[post("/login")]
45//! async fn login(
46//!     manager: web::Data<RTokenManager>
47//! ) -> Result<HttpResponse, RTokenError> {
48//!     let token = manager.login("user_10086")?;
49//!     Ok(HttpResponse::Ok().body(token))
50//! }
51//!
52//! #[get("/profile")]
53//! async fn profile(user: RUser) -> impl actix_web::Responder {
54//!     // If we reach here, the user is guaranteed to be authenticated
55//!     format!("Welcome, user: {}", user.id)
56//! }
57//!
58//! #[post("/logout")]
59//! async fn logout(
60//!     manager: web::Data<RTokenManager>,
61//!     user: RUser,
62//! ) -> Result<HttpResponse, RTokenError> {
63//!     manager.logout(&user.token)?;
64//!     Ok(HttpResponse::Ok().body("Logged out successfully"))
65//! }
66//!
67//! #[actix_web::main]
68//! async fn main() -> std::io::Result<()> {
69//!     let manager = RTokenManager::new();
70//!     
71//!     HttpServer::new(move || {
72//!         App::new()
73//!             .app_data(web::Data::new(manager.clone()))
74//!             .service(login)
75//!             .service(profile)
76//!             .service(logout)
77//!     })
78//!     .bind(("127.0.0.1", 8080))?
79//!     .run()
80//!     .await
81//! }
82//! ```
83//!
84//! ## How It Works
85//!
86//! 1. **Login**: Call [`RTokenManager::login()`] with a user ID to generate a UUID token
87//! 2. **Authenticate**: Add [`RUser`] to any handler that requires authentication
88//! 3. **Automatic Validation**: actix-web verifies the `Authorization` header before calling your handler
89//! 4. **Logout**: Call [`RTokenManager::logout()`] to invalidate a token
90//!
91//! ## Authorization Header Format
92//!
93//! Clients should include the token in the `Authorization` header:
94//!
95//! ```text
96//! Authorization: <token>
97//! ```
98//!
99//! Or with the `Bearer` prefix:
100//!
101//! ```text
102//! Authorization: Bearer <token>
103//! ```
104//!
105//! ## Error Handling
106//!
107//! - **401 Unauthorized**: Returned when token is missing or invalid
108//! - **500 Internal Server Error**: Returned when [`RTokenManager`] is not registered in `app_data`
109//! - **[`RTokenError::MutexPoisoned`]**: Returned when the internal lock is poisoned (rare)
110//!
111//! [`FromRequest`]: actix_web::FromRequest
112
113mod models;
114
115pub use crate::models::RTokenError;
116use actix_web::{FromRequest, HttpRequest, web};
117use std::future::{Ready, ready};
118use std::{
119    collections::HashMap,
120    sync::{Arc, Mutex},
121};
122
123/// The core token management component.
124///
125/// `RTokenManager` maintains an in-memory mapping of tokens to user IDs,
126/// providing thread-safe token generation, validation, and invalidation.
127///
128/// # Thread Safety
129///
130/// This type uses `Arc<Mutex<HashMap>>` internally, making it safe to clone
131/// and share across multiple actix-web worker threads. Each clone shares the
132/// same underlying token storage.
133///
134/// # Usage
135///
136/// In a typical actix-web application:
137///
138/// 1. Create a single instance in your `main()` function
139/// 2. Register it with `.app_data(web::Data::new(manager.clone()))`
140/// 3. Inject it into handlers via `web::Data<RTokenManager>`
141///
142/// # Example
143///
144/// ```rust
145/// use r_token::RTokenManager;
146/// use actix_web::{web, App};
147///
148/// let manager = RTokenManager::new();
149///
150/// // In your actix-web app:
151/// // App::new().app_data(web::Data::new(manager.clone()))
152///
153/// // Generate a token
154/// let token = manager.login("user_12345").unwrap();
155/// println!("Generated token: {}", token);
156///
157/// // Later, invalidate it
158/// manager.logout(&token).unwrap();
159/// ```
160#[derive(Clone,Default)]
161pub struct RTokenManager {
162    /// Internal token storage mapping tokens to user IDs.
163    ///
164    /// Uses `Arc<Mutex<HashMap>>` for thread-safe shared ownership across workers.
165    store: Arc<Mutex<HashMap<String, String>>>,
166}
167
168impl RTokenManager {
169    /// Creates a new token manager with empty storage.
170    ///
171    /// In a typical actix-web application, call this once in `main()` and
172    /// register the instance using `.app_data(web::Data::new(manager.clone()))`.
173    ///
174    /// # Example
175    ///
176    /// ```rust
177    /// use r_token::RTokenManager;
178    ///
179    /// let manager = RTokenManager::new();
180    /// ```
181    pub fn new() -> Self {
182        Self {
183            store: Arc::new(Mutex::new(HashMap::new())),
184        }
185    }
186
187    /// Generates a new authentication token for the given user ID.
188    ///
189    /// This method creates a UUID v4 token, stores the token-to-user-ID mapping
190    /// in memory, and returns the token string.
191    ///
192    /// # Arguments
193    ///
194    /// * `id` - The unique identifier for the user (typically a user ID from your database)
195    ///
196    /// # Returns
197    ///
198    /// Returns `Ok(String)` containing the generated UUID v4 token on success,
199    /// or `Err(RTokenError::MutexPoisoned)` if the internal lock is poisoned.
200    ///
201    /// # Example
202    ///
203    /// ```rust
204    /// use r_token::RTokenManager;
205    ///
206    /// let manager = RTokenManager::new();
207    /// let token = manager.login("user_12345").expect("Failed to generate token");
208    /// assert_eq!(token.len(), 36); // UUID v4 length
209    /// ```
210    pub fn login(&self, id: &str) -> Result<String, RTokenError> {
211        let token = uuid::Uuid::new_v4().to_string();
212        // Acquire the write lock and insert the token-user mapping into the store
213        // 获取写锁并将 Token-用户映射关系插入到存储中
214        // #[allow(clippy::unwrap_used)]
215        // self.store.lock().unwrap().insert(token.clone(), id.to_string());
216        self.store
217            .lock()
218            .map_err(|_| RTokenError::MutexPoisoned)?
219            .insert(token.clone(), id.to_string());
220        Ok(token)
221    }
222
223    /// Invalidates a token by removing it from storage.
224    ///
225    /// After calling this method, the specified token will no longer be valid,
226    /// and any requests using it will receive a 401 Unauthorized response.
227    ///
228    /// # Arguments
229    ///
230    /// * `token` - The token string to invalidate
231    ///
232    /// # Returns
233    ///
234    /// Returns `Ok(())` on success, or `Err(RTokenError::MutexPoisoned)`
235    /// if the internal lock is poisoned.
236    ///
237    /// # Example
238    ///
239    /// ```rust
240    /// use r_token::RTokenManager;
241    ///
242    /// let manager = RTokenManager::new();
243    /// let token = manager.login("user_12345").unwrap();
244    ///
245    /// // Later, invalidate the token
246    /// manager.logout(&token).expect("Failed to logout");
247    /// ```
248    pub fn logout(&self, token: &str) -> Result<(), RTokenError> {
249        // self.store.lock().unwrap().remove(token);
250        self.store
251            .lock()
252            .map_err(|_| RTokenError::MutexPoisoned)?
253            .remove(token);
254        Ok(())
255    }
256}
257
258/// Represents an authenticated user.
259///
260/// `RUser` is the key to r-token's "parameter-as-authentication" pattern.
261/// By implementing actix-web's [`FromRequest`] trait, it enables automatic
262/// authentication validation before your handler is called.
263///
264/// # How It Works
265///
266/// When you declare `RUser` as a handler parameter:
267///
268/// 1. actix-web extracts the token from the `Authorization` header
269/// 2. Validates the token using [`RTokenManager`]
270/// 3. If valid: creates an `RUser` instance and calls your handler
271/// 4. If invalid: returns 401 Unauthorized without calling your handler
272///
273/// # Type Safety Guarantee
274///
275/// If your handler receives an `RUser` parameter, the user is **guaranteed**
276/// to be authenticated. No manual validation needed!
277///
278/// # Example
279///
280/// ```rust,no_run
281/// use actix_web::{get, HttpResponse};
282/// use r_token::RUser;
283///
284/// #[get("/profile")]
285/// async fn profile(user: RUser) -> impl actix_web::Responder {
286///     // If we reach here, authentication succeeded
287///     HttpResponse::Ok().body(format!("User ID: {}", user.id))
288/// }
289/// ```
290///
291/// # Error Responses
292///
293/// - **401 Unauthorized**: Token missing, invalid, or expired
294/// - **500 Internal Server Error**: [`RTokenManager`] not registered in app_data
295///
296/// [`FromRequest`]: actix_web::FromRequest
297#[derive(Debug)]
298pub struct RUser {
299    /// The user's unique identifier.
300    ///
301    /// This corresponds to the ID passed to [`RTokenManager::login()`].
302    pub id: String,
303
304    /// The authentication token.
305    ///
306    /// Extracted from the `Authorization` request header.
307    pub token: String,
308}
309
310/// Implementation of actix-web's `FromRequest` trait for automatic authentication.
311///
312/// This implementation enables the "parameter-as-authentication" pattern.
313///
314/// # Validation Flow
315///
316/// When actix-web processes a request with an `RUser` parameter:
317///
318/// 1. **Retrieve Manager**: Extracts `RTokenManager` from app_data
319/// 2. **Extract Token**: Reads the `Authorization` header (supports `Bearer` prefix)
320/// 3. **Validate Token**: Checks if the token exists in the manager's storage
321/// 4. **Return Result**:
322///    - **Success**: Creates `RUser` and calls the handler
323///    - **Failure**: Returns error response without calling the handler
324///
325/// # Error Responses
326///
327/// - `500 Internal Server Error`: `RTokenManager` not found in app_data or mutex poisoned
328/// - `401 Unauthorized`: Token missing or invalid
329impl FromRequest for RUser {
330    type Error = actix_web::Error;
331    type Future = Ready<Result<Self, Self::Error>>;
332
333    fn from_request(req: &HttpRequest, _payload: &mut actix_web::dev::Payload) -> Self::Future {
334        // 獲取管理器
335        let manager = match req.app_data::<web::Data<RTokenManager>>() {
336            Some(m) => m,
337            None => {
338                return ready(Err(actix_web::error::ErrorInternalServerError(
339                    "Token manager not found",
340                )));
341            }
342        };
343        // 獲取Token(優先看header中的Authorization)
344        let token = match req
345            .headers()
346            .get("Authorization")
347            .and_then(|h| h.to_str().ok())
348        {
349            Some(token_str) => token_str
350                .strip_prefix("Bearer ")
351                .unwrap_or(token_str)
352                .to_string(),
353            None => return ready(Err(actix_web::error::ErrorUnauthorized("Unauthorized"))),
354        };
355
356        // 驗證token
357        let store = match manager.store.lock() {
358            Ok(s) => s,
359            Err(_) => {
360                return ready(Err(actix_web::error::ErrorInternalServerError(
361                    "Mutex poisoned",
362                )));
363            }
364        };
365
366        match store.get(&token) {
367            Some(id) => {
368                return ready(Ok(RUser {
369                    id: id.clone(),
370                    token: token.clone(),
371                }));
372            }
373            None => {
374                return ready(Err(actix_web::error::ErrorUnauthorized("Invalid token")));
375            }
376        }
377    }
378}
379