diff --git a/docs/cookbook/src/getting_started/quickstart.md b/docs/cookbook/src/getting_started/quickstart.md index a6a31c3..df81d4d 100644 --- a/docs/cookbook/src/getting_started/quickstart.md +++ b/docs/cookbook/src/getting_started/quickstart.md @@ -12,7 +12,28 @@ cargo rustapi new my-api cd my-api ``` -This commands sets up a complete project structure with handling, models, and tests ready to go. +This command sets up a complete project structure with handling, models, and tests ready to go. + +## The Code + +Open `src/main.rs`. You'll see how simple it is: + +```rust +use rustapi_rs::prelude::*; + +#[rustapi::get("/hello")] +async fn hello() -> Json { + Json("Hello from RustAPI!".to_string()) +} + +#[rustapi::main] +async fn main() -> Result<()> { + // Auto-discovery magic ✨ + RustApi::auto() + .run("127.0.0.1:8080") + .await +} +``` ## Run the Server @@ -25,15 +46,15 @@ cargo run You should see output similar to: ``` -INFO 🚀 Server running at http://127.0.0.1:8080 -INFO 📚 API docs at http://127.0.0.1:8080/docs +INFO rustapi: 🚀 Server running at http://127.0.0.1:8080 +INFO rustapi: 📚 API docs at http://127.0.0.1:8080/docs ``` ## Test It Out Open your browser to [http://127.0.0.1:8080/docs](http://127.0.0.1:8080/docs). -You'll see the **Swagger UI** automatically generated from your code. Try out the `/health` endpoint or create a new Item in the `Items` API. +You'll see the **Swagger UI** automatically generated from your code. Try out the endpoint directly from the browser! ## What Just Happened? diff --git a/docs/cookbook/src/recipes/crud_resource.md b/docs/cookbook/src/recipes/crud_resource.md index fff9814..6b9a8a3 100644 --- a/docs/cookbook/src/recipes/crud_resource.md +++ b/docs/cookbook/src/recipes/crud_resource.md @@ -32,14 +32,24 @@ pub async fn create(Json(payload): Json) -> impl IntoResponse { } ``` -Then register it in `main.rs`: +Then in `main.rs`, simply use `RustApi::auto()`: ```rust -RustApi::new() - .mount(handlers::users::list) - .mount(handlers::users::create) +use rustapi_rs::prelude::*; + +mod handlers; // Make sure the module is part of the compilation unit! + +#[rustapi::main] +async fn main() -> Result<()> { + // RustAPI automatically discovers all routes decorated with macros + RustApi::auto() + .run("127.0.0.1:8080") + .await +} ``` ## Discussion -Using `#[rustapi::mount]` (if available) or manual routing keeps your `main.rs` clean. Organizing handlers by resource (domain-driven design) scales better than organizing by HTTP method. +RustAPI uses **distributed slices** (via `linkme`) to automatically register routes decorated with `#[rustapi::get]`, `#[rustapi::post]`, etc. This means you don't need to manually import or mount every single handler in your `main` function. + +Just ensure your handler modules are reachable (e.g., via `mod handlers;`), and the framework handles the rest. This encourages a clean, Domain-Driven Design (DDD) structure where resources are self-contained. diff --git a/docs/cookbook/src/recipes/jwt_auth.md b/docs/cookbook/src/recipes/jwt_auth.md index a563566..c333da7 100644 --- a/docs/cookbook/src/recipes/jwt_auth.md +++ b/docs/cookbook/src/recipes/jwt_auth.md @@ -1,153 +1,124 @@ # JWT Authentication -Authentication is critical for almost every API. This recipe demonstrates how to implement JSON Web Token (JWT) authentication using the `jsonwebtoken` crate and RustAPI's extractor pattern. +Authentication is critical for almost every API. RustAPI provides a built-in, production-ready JWT authentication system via the `jwt` feature. ## Dependencies -Add `jsonwebtoken` and `serde` to your `Cargo.toml`: +Enable the `jwt` feature in your `Cargo.toml`: ```toml [dependencies] -jsonwebtoken = "9" +rustapi-rs = { version = "0.1", features = ["jwt"] } serde = { version = "1", features = ["derive"] } ``` ## 1. Define Claims -The standard JWT claims. You can add custom fields here (like `role`). +Define your custom claims struct. It must be serializable and deserializable. ```rust use serde::{Deserialize, Serialize}; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Claims { - pub sub: String, // Subject (User ID) - pub exp: usize, // Expiration time - pub role: String, // Custom claim: "admin", "user" + pub sub: String, // Subject (User ID) + pub role: String, // Custom claim: "admin", "user" + pub exp: usize, // Required for JWT expiration validation } ``` -## 2. Configuration State +## 2. Shared State -Store your keys in the application state. +To avoid hardcoding secrets in multiple places, we'll store our secret key in the application state. ```rust -use std::sync::Arc; -use jsonwebtoken::{EncodingKey, DecodingKey}; - #[derive(Clone)] -pub struct AuthState { - pub encoder: EncodingKey, - pub decoder: DecodingKey, -} - -impl AuthState { - pub fn new(secret: &str) -> Self { - Self { - encoder: EncodingKey::from_secret(secret.as_bytes()), - decoder: DecodingKey::from_secret(secret.as_bytes()), - } - } +pub struct AppState { + pub secret: String, } ``` -## 3. The `AuthUser` Extractor +## 3. The Handlers -This is where the magic happens. We create a custom extractor that: -1. Checks the `Authorization` header. -2. Decodes the token. -3. Validates expiration. -4. Returns the claims or rejects the request. +We use the `AuthUser` extractor to protect routes, and `State` to access the secret for signing tokens during login. ```rust -use rustapi::prelude::*; -use jsonwebtoken::{decode, Validation, Algorithm}; - -pub struct AuthUser(pub Claims); - -#[async_trait] -impl FromRequestParts> for AuthUser { - type Rejection = (StatusCode, Json); - - async fn from_request_parts( - parts: &mut Parts, - state: &Arc - ) -> Result { - // 1. Get header - let auth_header = parts.headers.get("Authorization") - .ok_or((StatusCode::UNAUTHORIZED, Json(json!({"error": "Missing token"}))))?; - - let token = auth_header.to_str() - .map_err(|_| (StatusCode::UNAUTHORIZED, Json(json!({"error": "Invalid token format"}))))? - .strip_prefix("Bearer ") - .ok_or((StatusCode::UNAUTHORIZED, Json(json!({"error": "Invalid token type"}))))?; - - // 2. Decode - let token_data = decode::( - token, - &state.decoder, - &Validation::new(Algorithm::HS256) - ).map_err(|e| (StatusCode::UNAUTHORIZED, Json(json!({"error": e.to_string()}))))?; - - Ok(AuthUser(token_data.claims)) - } -} -``` - -## 4. Usage in Handlers +use rustapi_rs::prelude::*; +use std::time::{SystemTime, UNIX_EPOCH}; -Now, securing an endpoint is as simple as adding an argument. - -```rust +#[rustapi::get("/profile")] async fn protected_profile( - AuthUser(claims): AuthUser + // This handler will only be called if a valid token is present + AuthUser(claims): AuthUser ) -> Json { Json(format!("Welcome back, {}! You are a {}.", claims.sub, claims.role)) } -async fn login(State(state): State>) -> Json { +#[rustapi::post("/login")] +async fn login(State(state): State) -> Result> { // In a real app, validate credentials first! + use std::time::{SystemTime, UNIX_EPOCH}; + + let expiration = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + 3600; // Token expires in 1 hour (3600 seconds) + let claims = Claims { sub: "user_123".to_owned(), role: "admin".to_owned(), - exp: 10000000000, // Future timestamp + exp: expiration as usize, }; - let token = jsonwebtoken::encode( - &jsonwebtoken::Header::default(), - &claims, - &state.encoder - ).unwrap(); + // We use the secret from our shared state + let token = create_token(&claims, &state.secret)?; - Json(token) + Ok(Json(token)) } ``` -## 5. Wiring it Up +## 4. Wiring it Up + +Register the `JwtLayer` and the state in your application. ```rust -#[tokio::main] -async fn main() { - let auth_state = Arc::new(AuthState::new("my_secret_key")); +#[rustapi::main] +async fn main() -> Result<()> { + // In production, load this from an environment variable! + let secret = "my_secret_key".to_string(); + + let state = AppState { + secret: secret.clone(), + }; - let app = RustApi::new() - .route("/login", post(login)) - .route("/profile", get(protected_profile)) - .with_state(auth_state); // Inject state + // Configure JWT validation with the same secret + let jwt_layer = JwtLayer::::new(secret); - RustApi::serve("127.0.0.1:3000", app).await.unwrap(); + RustApi::auto() + .state(state) // Register the shared state + .layer(jwt_layer) // Add the middleware + .run("127.0.0.1:8080") + .await } ``` ## Bonus: Role-Based Access Control (RBAC) -Since we have the `role` in our claims, we can enforce permissions easily. +Since we have the `role` in our claims, we can enforce permissions easily within the handler: ```rust -async fn admin_only(AuthUser(claims): AuthUser) -> Result { +#[rustapi::get("/admin")] +async fn admin_only(AuthUser(claims): AuthUser) -> Result { if claims.role != "admin" { return Err(StatusCode::FORBIDDEN); } Ok("Sensitive Admin Data".to_string()) } ``` + +## How It Works + +1. **`JwtLayer` Middleware**: Intercepts requests, looks for `Authorization: Bearer `, validates the signature, and stores the decoded claims in the request extensions. +2. **`AuthUser` Extractor**: Retrieves the claims from the request extensions. If the middleware failed or didn't run, or if the token was missing/invalid, the extractor returns a `401 Unauthorized` error. + +This separation allows you to have some public routes (where `JwtLayer` might just pass through) and some protected routes (where `AuthUser` enforces presence). Note that `JwtLayer` by default does *not* reject requests without tokens; it just doesn't attach claims. The *extractor* does the rejection.