Skip to main content

ferro_rs/authorization/
middleware.rs

1//! Authorization middleware for route protection.
2//!
3//! Provides middleware to protect routes with authorization checks.
4
5use super::gate::Gate;
6use crate::http::{HttpResponse, Request, Response};
7use crate::middleware::{Middleware, Next};
8use async_trait::async_trait;
9
10/// Middleware that checks authorization before allowing access.
11///
12/// # Example
13///
14/// ```rust,ignore
15/// use ferro_rs::authorization::Authorize;
16/// use ferro_rs::routing::Route;
17///
18/// Route::get("/admin", admin_dashboard)
19///     .middleware(Authorize::ability("view-admin"));
20///
21/// Route::put("/posts/{id}", update_post)
22///     .middleware(Authorize::ability("update-post"));
23/// ```
24pub struct Authorize {
25    ability: String,
26}
27
28impl Authorize {
29    /// Create middleware that checks a specific ability.
30    ///
31    /// # Arguments
32    ///
33    /// * `ability` - The ability to check (e.g., "view-admin", "update-post")
34    ///
35    /// # Example
36    ///
37    /// ```rust,ignore
38    /// Route::get("/dashboard", dashboard)
39    ///     .middleware(Authorize::ability("view-dashboard"));
40    /// ```
41    pub fn ability(ability: impl Into<String>) -> Self {
42        Self {
43            ability: ability.into(),
44        }
45    }
46}
47
48#[async_trait]
49impl Middleware for Authorize {
50    async fn handle(&self, request: Request, next: Next) -> Response {
51        // Check if user is authenticated
52        let user = match crate::auth::Auth::user().await {
53            Ok(Some(u)) => u,
54            Ok(None) => {
55                return Err(HttpResponse::json(serde_json::json!({
56                    "message": "Unauthenticated."
57                }))
58                .status(401));
59            }
60            Err(e) => {
61                eprintln!("Failed to retrieve user for authorization: {e}");
62                return Err(HttpResponse::json(serde_json::json!({
63                    "message": "Authentication error."
64                }))
65                .status(500));
66            }
67        };
68
69        // Check authorization
70        let response = Gate::inspect(user.as_ref(), &self.ability, None);
71
72        if response.denied() {
73            let message = response.message().unwrap_or("This action is unauthorized.");
74            return Err(HttpResponse::json(serde_json::json!({
75                "message": message
76            }))
77            .status(response.status()));
78        }
79
80        next(request).await
81    }
82}
83
84/// Macro for creating authorization middleware.
85///
86/// # Example
87///
88/// ```rust,ignore
89/// use ferro_rs::can;
90///
91/// Route::get("/admin", admin_dashboard)
92///     .middleware(can!("view-admin"));
93///
94/// // Multiple abilities (all must pass)
95/// Route::put("/posts/{id}", update_post)
96///     .middleware(can!("authenticated"))
97///     .middleware(can!("update-post"));
98/// ```
99#[macro_export]
100macro_rules! can {
101    ($ability:expr) => {
102        $crate::authorization::Authorize::ability($ability)
103    };
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_authorize_creation() {
112        let middleware = Authorize::ability("test-ability");
113        assert_eq!(middleware.ability, "test-ability");
114    }
115
116    #[test]
117    fn test_can_macro() {
118        let middleware = can!("view-dashboard");
119        assert_eq!(middleware.ability, "view-dashboard");
120    }
121}