koprogo_api/infrastructure/web/
middleware.rs

1use crate::infrastructure::web::app_state::AppState;
2use actix_web::{dev::Payload, error::ErrorUnauthorized, web, Error, FromRequest, HttpRequest};
3use std::future::{ready, Ready};
4use uuid::Uuid;
5
6/// Authenticated user claims extracted from JWT token
7///
8/// This struct automatically extracts and validates JWT tokens from the Authorization header.
9/// Use it as a parameter in your handler functions to require authentication:
10///
11/// ```rust
12/// async fn protected_handler(claims: AuthenticatedUser) -> impl Responder {
13///     // claims.user_id and claims.organization_id are now available
14/// }
15/// ```
16#[derive(Debug, Clone)]
17pub struct AuthenticatedUser {
18    pub user_id: Uuid,
19    pub email: String,
20    pub role: String,
21    pub organization_id: Option<Uuid>,
22}
23
24impl AuthenticatedUser {
25    /// Get the organization_id or return an error if not present
26    pub fn require_organization(&self) -> Result<Uuid, Error> {
27        self.organization_id
28            .ok_or_else(|| ErrorUnauthorized("User does not belong to an organization"))
29    }
30}
31
32impl FromRequest for AuthenticatedUser {
33    type Error = Error;
34    type Future = Ready<Result<Self, Self::Error>>;
35
36    fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
37        // Get AppState from request
38        let app_state = match req.app_data::<web::Data<AppState>>() {
39            Some(state) => state,
40            None => return ready(Err(ErrorUnauthorized("Internal server error"))),
41        };
42
43        // Extract Authorization header
44        let auth_header = match req.headers().get("Authorization") {
45            Some(header) => match header.to_str() {
46                Ok(s) => s,
47                Err(_) => return ready(Err(ErrorUnauthorized("Invalid authorization header"))),
48            },
49            None => return ready(Err(ErrorUnauthorized("Missing authorization header"))),
50        };
51
52        // Extract token from "Bearer <token>"
53        let token = auth_header.trim_start_matches("Bearer ").trim();
54
55        // Verify token and extract claims
56        match app_state.auth_use_cases.verify_token(token) {
57            Ok(claims) => {
58                // Parse user_id from claims.sub
59                match Uuid::parse_str(&claims.sub) {
60                    Ok(user_id) => ready(Ok(AuthenticatedUser {
61                        user_id,
62                        email: claims.email,
63                        role: claims.role,
64                        organization_id: claims.organization_id,
65                    })),
66                    Err(_) => ready(Err(ErrorUnauthorized("Invalid user ID in token"))),
67                }
68            }
69            Err(e) => ready(Err(ErrorUnauthorized(e))),
70        }
71    }
72}
73
74/// Organization ID extracted from authenticated user's JWT token
75///
76/// This extractor requires that the user belongs to an organization.
77/// Use it when you need to enforce organization-scoped operations:
78///
79/// ```rust
80/// async fn create_building(
81///     organization: OrganizationId,
82///     dto: web::Json<CreateBuildingDto>
83/// ) -> impl Responder {
84///     // organization.0 contains the Uuid
85/// }
86/// ```
87#[derive(Debug, Clone, Copy)]
88pub struct OrganizationId(pub Uuid);
89
90impl FromRequest for OrganizationId {
91    type Error = Error;
92    type Future = Ready<Result<Self, Self::Error>>;
93
94    fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
95        // First extract AuthenticatedUser
96        let user_future = AuthenticatedUser::from_request(req, payload);
97
98        // Get the result
99        match user_future.into_inner() {
100            Ok(user) => match user.organization_id {
101                Some(org_id) => ready(Ok(OrganizationId(org_id))),
102                None => ready(Err(ErrorUnauthorized(
103                    "User does not belong to an organization",
104                ))),
105            },
106            Err(e) => ready(Err(e)),
107        }
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_authenticated_user_require_organization() {
117        let user_with_org = AuthenticatedUser {
118            user_id: Uuid::new_v4(),
119            email: "test@example.com".to_string(),
120            role: "admin".to_string(),
121            organization_id: Some(Uuid::new_v4()),
122        };
123
124        assert!(user_with_org.require_organization().is_ok());
125
126        let user_without_org = AuthenticatedUser {
127            user_id: Uuid::new_v4(),
128            email: "test@example.com".to_string(),
129            role: "admin".to_string(),
130            organization_id: None,
131        };
132
133        assert!(user_without_org.require_organization().is_err());
134    }
135}