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}