koprogo_api/infrastructure/web/
middleware.rs1use 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#[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 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 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 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 let token = auth_header.trim_start_matches("Bearer ").trim();
54
55 match app_state.auth_use_cases.verify_token(token) {
57 Ok(claims) => {
58 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#[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 let user_future = AuthenticatedUser::from_request(req, payload);
97
98 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}