corteq 0.1.0

Enterprise-grade multi-tenant SaaS framework for Rust with security-first design
Documentation
//! Middleware for tenant context extraction and validation

use crate::{CorteqApp, TenantContext};
use actix_web::{
    dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
    error::{ErrorBadRequest, ErrorUnauthorized},
    web, Error, FromRequest, HttpMessage,
};
use futures::future::{ready, LocalBoxFuture, Ready};
use std::rc::Rc;

/// Actix Web extractor for tenant context
///
/// This extractor ensures that every request has a valid tenant context.
/// It will automatically reject requests without valid tenant information.
///
/// # Example
///
/// ```rust,no_run
/// use actix_web::{web, HttpResponse};
/// use corteq::TenantExtractor;
///
/// async fn my_handler(tenant: TenantExtractor) -> HttpResponse {
///     HttpResponse::Ok().body(format!("Tenant: {}", tenant.0.tenant_id))
/// }
/// ```
#[derive(Debug, Clone)]
pub struct TenantExtractor(pub TenantContext);

impl FromRequest for TenantExtractor {
    type Error = Error;
    type Future = Ready<Result<Self, Self::Error>>;

    fn from_request(
        req: &actix_web::HttpRequest,
        _payload: &mut actix_web::dev::Payload,
    ) -> Self::Future {
        // Try to get tenant context from request extensions
        match req.extensions().get::<TenantContext>().cloned() {
            Some(ctx) => ready(Ok(TenantExtractor(ctx))),
            None => ready(Err(ErrorBadRequest("Missing tenant context"))),
        }
    }
}

/// Middleware factory for tenant context injection
///
/// This middleware extracts the JWT token, validates it, and injects
/// the tenant context into request extensions for downstream handlers.
#[derive(Debug, Default)]
pub struct TenantContextMiddleware;

impl<S, B> Transform<S, ServiceRequest> for TenantContextMiddleware
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type InitError = ();
    type Transform = TenantContextMiddlewareService<S>;
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        ready(Ok(TenantContextMiddlewareService {
            service: Rc::new(service),
        }))
    }
}

/// Service implementation for tenant context middleware
pub struct TenantContextMiddlewareService<S> {
    service: Rc<S>,
}

impl<S> std::fmt::Debug for TenantContextMiddlewareService<S> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("TenantContextMiddlewareService")
            .finish_non_exhaustive()
    }
}

/// Extract JWT token from request
///
/// Checks in order of priority:
/// 1. Cookie named "token" (httpOnly cookie)
/// 2. Authorization header with Bearer scheme
fn extract_jwt_token(req: &ServiceRequest) -> Option<String> {
    // Priority 1: Check for httpOnly cookie named "token"
    if let Some(cookie_header) = req.headers().get("cookie") {
        if let Ok(cookie_str) = cookie_header.to_str() {
            // Parse cookies manually (simple approach)
            for cookie_pair in cookie_str.split(';') {
                let parts: Vec<&str> = cookie_pair.trim().splitn(2, '=').collect();
                if parts.len() == 2 && parts[0] == "token" {
                    tracing::debug!("JWT token found in cookie");
                    return Some(parts[1].to_string());
                }
            }
        }
    }

    // Priority 2: Check Authorization header for Bearer token
    if let Some(auth_header) = req.headers().get("authorization") {
        if let Ok(auth_str) = auth_header.to_str() {
            if let Some(token) = auth_str.strip_prefix("Bearer ") {
                tracing::debug!("JWT token found in Authorization header");
                return Some(token.to_string());
            }
        }
    }

    None
}

impl<S, B> Service<ServiceRequest> for TenantContextMiddlewareService<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

    forward_ready!(service);

    fn call(&self, req: ServiceRequest) -> Self::Future {
        let svc = self.service.clone();

        Box::pin(async move {
            // Extract JWT token from Cookie (priority) or Authorization header
            let token = extract_jwt_token(&req);

            if token.is_none() {
                tracing::warn!("No JWT token found in request");
                return Err(ErrorUnauthorized("Authentication required"));
            }

            let token = token.unwrap();

            // Get CorteqApp from app_data
            let app_config = req.app_data::<web::Data<CorteqApp>>().ok_or_else(|| {
                tracing::error!("CorteqApp not found in app_data");
                ErrorBadRequest("Application configuration error")
            })?;

            // Decode JWT to extract claims
            let claims = app_config.jwt_service.decode(&token).map_err(|e| {
                tracing::warn!("JWT validation failed: {}", e);
                ErrorUnauthorized("Invalid or expired token")
            })?;

            let tenant_id = claims.tenant_id;

            // Try to get tenant context from cache first
            let tenant_context = if let Some(ctx) = app_config.tenant_cache.get(&tenant_id).await {
                tracing::debug!("Tenant {} found in cache", tenant_id);
                ctx
            } else {
                // Cache miss - load from database
                tracing::debug!("Cache miss for tenant {}, loading from database", tenant_id);

                let tenant = app_config
                    .tenant_repository
                    .find_by_id(&tenant_id)
                    .await
                    .map_err(|e| {
                        tracing::error!("Database error loading tenant: {}", e);
                        ErrorBadRequest("Failed to load tenant")
                    })?
                    .ok_or_else(|| {
                        tracing::warn!("Tenant {} not found or soft-deleted", tenant_id);
                        ErrorUnauthorized("Tenant not found")
                    })?;

                // Create tenant context from loaded tenant
                let ctx = TenantContext::from(tenant);

                // Cache the context for future requests
                app_config.tenant_cache.set(tenant_id, ctx.clone()).await;

                ctx
            };

            // Inject tenant context into request extensions for downstream handlers
            req.extensions_mut().insert(tenant_context);

            // Continue to the next service
            let res = svc.call(req).await?;
            Ok(res)
        })
    }
}

// Integration tests are in tests/ directory