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