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 }))
}