Files
poimen-memory/crates/mem-cli/src/handlers/middleware.rs
T

125 lines
3.7 KiB
Rust
Raw Normal View History

/// Handler middleware utilities
///
/// Centralized JWT validation + rate limiting for all HTTP handlers.
/// Eliminates boilerplate across endpoints, improves testability.
use actix_web::{HttpRequest, HttpResponse};
use serde_json::json;
use crate::http_server::AppState;
/// Result type for middleware operations
pub type MiddlewareResult<T> = Result<T, HttpResponse>;
/// Validate JWT token + check rate limit
///
/// Handles:
/// 1. Extract Authorization header
/// 2. Validate JWT (if auth enabled)
/// 3. Check rate limit (if limiter enabled)
/// 4. Return error response on failure
///
/// # Usage
/// ```ignore
/// validate_and_rate_limit(&req, &state, "compact", 10)?;
/// // If we get here, both JWT and rate limit checks passed
/// ```
pub fn validate_and_rate_limit(
req: &HttpRequest,
state: &AppState,
endpoint: &str,
rate_limit: u32,
) -> MiddlewareResult<()> {
// 1. JWT validation (if enabled)
if let Some(jwt_validator) = &state.jwt_validator {
let auth_header = req
.headers()
.get("Authorization")
.and_then(|h| h.to_str().ok())
.ok_or_else(|| {
HttpResponse::Unauthorized().json(json!({
"error": "Missing Authorization header"
}))
})?;
2026-09-08 01:11:14 +00:00
crate::jwt_validator::JwtValidator::extract_bearer_token(auth_header).map_err(|e| {
HttpResponse::Unauthorized().json(json!({
"error": format!("JWT validation failed: {}", e)
}))
})?;
}
// 2. Rate limiting (if enabled)
state
.rate_limiter
2026-09-08 01:11:14 +00:00
.check("default", endpoint)
.map_err(|e| {
HttpResponse::TooManyRequests().json(json!({
2026-09-08 01:11:14 +00:00
"error": format!("Rate limit exceeded: {}", e.reason())
}))
})?;
Ok(())
}
/// Extract user identity from JWT claims (sub field)
///
/// Tries to decode JWT from Authorization header to get `sub` claim.
/// Falls back to "anonymous" if auth is disabled or header missing.
/// Used by metrics to track errors/requests per user.
pub fn extract_user_id(req: &HttpRequest, state: &AppState) -> String {
// If auth disabled, check synthetic claims
if state.jwt_validator.is_none() {
return "anonymous".to_string();
}
// Try to extract sub from JWT
let token = req.headers()
.get("Authorization")
.and_then(|h| h.to_str().ok())
.and_then(|h| h.strip_prefix("Bearer "))
.unwrap_or("");
if token.is_empty() {
return "anonymous".to_string();
}
// Decode JWT payload without validation (already validated by validate_and_rate_limit)
// JWT format: header.payload.signature
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 3 {
return "anonymous".to_string();
}
// Decode base64 payload
use base64::Engine;
let engine = base64::engine::general_purpose::URL_SAFE_NO_PAD;
if let Ok(payload_bytes) = engine.decode(parts[1]) {
if let Ok(payload) = serde_json::from_slice::<serde_json::Value>(&payload_bytes) {
if let Some(sub) = payload.get("sub").and_then(|s| s.as_str()) {
return sub.to_string();
}
}
}
"anonymous".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_middleware_result_type_is_result() {
// Verify type alias works
let _result: MiddlewareResult<()> = Ok(());
let _result: MiddlewareResult<()> = Err(HttpResponse::Unauthorized().finish());
}
#[test]
fn test_validate_and_rate_limit_signature() {
// Just verify the function signature is correct (compile-time test)
// Runtime tests require full AppState with mocks
let _ = validate_and_rate_limit;
}
}