use axum::body::Body; use axum::http::Request; use axum::middleware::Next; use axum::response::Response; use erp_core::error::AppError; use erp_core::types::TenantContext; use crate::service::token_service::TokenService; /// JWT authentication middleware function. /// /// Extracts the `Bearer` token from the `Authorization` header, validates it /// using `TokenService::decode_token`, and injects a `TenantContext` into the /// request extensions so downstream handlers can access tenant/user identity. /// /// The `jwt_secret` parameter is passed explicitly by the server crate at /// middleware construction time, avoiding any circular dependency between /// erp-auth and erp-server. /// /// # Errors /// /// Returns `AppError::Unauthorized` if: /// - The `Authorization` header is missing /// - The header value does not start with `"Bearer "` /// - The token cannot be decoded or has expired /// - The token type is not "access" pub async fn jwt_auth_middleware_fn( jwt_secret: String, req: Request, next: Next, ) -> Result { let auth_header = req .headers() .get("Authorization") .and_then(|v| v.to_str().ok()) .ok_or(AppError::Unauthorized)?; let token = auth_header .strip_prefix("Bearer ") .ok_or(AppError::Unauthorized)?; let claims = TokenService::decode_token(token, &jwt_secret).map_err(|_| AppError::Unauthorized)?; // Verify this is an access token, not a refresh token if claims.token_type != "access" { return Err(AppError::Unauthorized); } let ctx = TenantContext { tenant_id: claims.tid, user_id: claims.sub, roles: claims.roles, permissions: claims.permissions, }; // Reconstruct the request with the TenantContext injected into extensions. // We cannot borrow `req` mutably after reading headers, so we rebuild. let (parts, body) = req.into_parts(); let mut req = Request::from_parts(parts, body); req.extensions_mut().insert(ctx); Ok(next.run(req).await) }