custom_auth_stack.rs
88 lignes · 3581 octets
//! Hand-composed tower stack (RFC 0042) //! //! Shows how to wire `AuthService` / `AdminService` yourself, outside //! `RustwardenApp::builder()`, while still honouring a resolved //! `AuthConfig` (custom `login_path`, explicit `auth_failure_mode`). //! The middlewares seed `Extension(Arc<AuthConfig>)` into every request, //! so no `Extension` layer is needed for the extractors (`AuthUser`, //! `auth_required`, `admin_required`) to see the same instance. //! //! ```bash //! DATABASE_URL=postgres://... JWT_SECRET=... cargo run -p rustwarden-app --example custom_auth_stack //! curl -si http://localhost:3000/api/private/whoami | head -1 # → HTTP/1.1 401 Unauthorized //! curl -si http://localhost:3000/api/v1/admin/ping | head -1 # → HTTP/1.1 401 Unauthorized //! ``` use std::sync::Arc; use axum::{Json, Router, routing::get}; use rustwarden_core::config::auth::{AuthConfig, AuthFailureMode}; use rustwarden_core::config::database; use rustwarden_core::middleware::admin_service::AdminService; use rustwarden_core::middleware::auth_service::AuthService; use sea_orm::DatabaseConnection; use serde_json::json; use tower::ServiceBuilder; #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); let db: DatabaseConnection = database::create_connection().await?; // Resolve the configuration once; apply your own overrides here. let mut auth_config = AuthConfig::from_env().map_err(|e| anyhow::anyhow!(e))?; auth_config.auth_failure_mode = AuthFailureMode::Json401; auth_config.login_path = "/auth/signin".to_string(); let auth_config = Arc::new(auth_config); let protected_prefixes = Arc::new(vec!["/api/private".to_string()]); let app = Router::new() .route("/api/status", get(status)) .route("/api/private/whoami", get(whoami)) .route("/api/v1/admin/ping", get(admin_ping)) .layer( ServiceBuilder::new() .layer_fn({ let (db, cfg) = (db.clone(), auth_config.clone()); move |inner| AdminService::with_auth_config(inner, db.clone(), cfg.clone()) }) .layer_fn({ let (db, cfg, prefixes) = (db.clone(), auth_config.clone(), protected_prefixes.clone()); move |inner| { AuthService::with_auth_config( inner, db.clone(), prefixes.clone(), cfg.clone(), ) } }) .into_inner(), ); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; tracing::info!("custom_auth_stack listening on http://0.0.0.0:3000"); axum::serve(listener, app).await?; Ok(()) } async fn status() -> Json<serde_json::Value> { Json(json!({ "status": "ok", "stack": "custom" })) } /// Reached only with a valid JWT carrying the `admin` role: `/api/v1/admin*` /// is enforced by `AdminService` (anonymous → `401`, non-admin → `403`). async fn admin_ping() -> Json<serde_json::Value> { Json(json!({ "admin": "pong" })) } /// Reached only with a valid JWT: `/api/private` is a protected prefix, so /// anonymous requests get `401 {"error":"unauthorized"}` from `AuthService`. async fn whoami( axum::Extension(claims): axum::Extension<rustwarden_core::auth::jwt::Claims>, ) -> Json<serde_json::Value> { Json(json!({ "sub": claims.sub, "username": claims.username })) }
GitRust