admin_required.rs 144 lignes · 4702 octets
/// Admin Required Middleware
/// Feature 009: Admin & Settings
///
/// Middleware to protect routes that require admin role.
/// Returns 403 Forbidden if user is not an admin.
use axum::{
    extract::{Request, State},
    http::{StatusCode, Uri},
    middleware::Next,
    response::{IntoResponse, Response},
};
use sea_orm::DatabaseConnection;

use crate::error::AppError;
use crate::middleware::auth_required::extract_user_from_jwt;

/// Middleware that requires admin role
///
/// Checks if the user has a valid JWT token and the "admin" role.
/// If admin, allows the request to proceed.
/// If not authenticated, redirects to `/login`.
/// If authenticated but not admin, returns 403 Forbidden.
///
/// # Arguments
///
/// * `State(db)` - Database connection (for JWT validation)
/// * `request` - The HTTP request (contains URI)
/// * `next` - Next middleware or handler
///
/// # Returns
///
/// * `200 OK` if admin (passes to handler)
/// * `302 Found` redirects to `/login` if not authenticated
/// * `403 Forbidden` if authenticated but not admin
///
/// # Example
///
/// ```rust,ignore
/// use axum::Router;
/// use axum::routing::get;
/// use axum::middleware;
///
/// let admin_routes = Router::new()
///     .route("/admin", get(admin_handler))
///     .route_layer(middleware::from_fn_with_state(db.clone(), admin_required));
/// ```
///
/// # Note
///
/// This function signature is compatible with `axum::middleware::from_fn_with_state`.
/// The URI is extracted from `request.uri()` internally.
/// Returns `Response` directly (not `Result`) as required by `from_fn_with_state`.
///
/// According to Axum documentation, `from_fn_with_state` provides the state to the function,
/// which can be extracted using `State` extractor. The function signature should be:
/// `async fn(State<S>, Request, Next) -> Response`
pub async fn admin_required(
    State(db): State<DatabaseConnection>,
    request: Request,
    next: Next,
) -> Response {
    // Extract URI from request (as per Axum middleware best practices)
    let uri = request.uri().clone();

    tracing::debug!(
        "admin_required middleware: checking admin access for {}",
        uri
    );

    // Extract user from JWT (reuse auth_required logic)
    let claims = match extract_user_from_jwt(&db, &request).await {
        Some(claims) => claims,
        None => {
            tracing::warn!("Unauthenticated access attempt to admin route: {}", uri);
            return crate::middleware::auth_service::auth_failure_from_extensions(
                request.extensions(),
                &uri,
            );
        }
    };

    // Check if user has admin role
    if !claims.roles.contains(&"admin".to_string()) {
        tracing::warn!(
            "Non-admin user {} attempted to access admin route: {}",
            claims.sub,
            uri
        );
        return (StatusCode::FORBIDDEN, "Access denied: Admin role required").into_response();
    }

    tracing::debug!("Admin user {} accessing: {}", claims.sub, uri);

    // Store claims in request extensions for handlers to use
    let (mut parts, body) = request.into_parts();
    parts.extensions.insert(claims.clone());
    let request = Request::from_parts(parts, body);

    // User is admin, proceed to handler
    next.run(request).await
}

/// Internal implementation that takes db as parameter
/// Made public for use in main.rs with from_fn closure
pub async fn admin_required_impl(
    db: &DatabaseConnection,
    uri: Uri,
    request: Request,
    next: Next,
) -> Result<Response, AppError> {
    // Extract user from JWT
    let claims = match extract_user_from_jwt(db, &request).await {
        Some(claims) => claims,
        None => {
            tracing::warn!("Unauthenticated access attempt to admin route: {}", uri);
            return Ok(
                crate::middleware::auth_service::auth_failure_from_extensions(
                    request.extensions(),
                    &uri,
                ),
            );
        }
    };

    // Check if user has admin role
    if !claims.roles.contains(&"admin".to_string()) {
        tracing::warn!(
            "Non-admin user {} attempted to access admin route: {}",
            claims.sub,
            uri
        );
        return Ok((StatusCode::FORBIDDEN, "Access denied: Admin role required").into_response());
    }

    tracing::debug!("Admin user {} accessing: {}", claims.sub, uri);

    // Store claims in request extensions for handlers to use
    let (mut parts, body) = request.into_parts();
    parts.extensions.insert(claims);
    let request = Request::from_parts(parts, body);

    // User is admin, proceed to handler
    Ok(next.run(request).await)
}