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;
#[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 {
match req.extensions().get::<TenantContext>().cloned() {
Some(ctx) => ready(Ok(TenantExtractor(ctx))),
None => ready(Err(ErrorBadRequest("Missing tenant context"))),
}
}
}
#[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),
}))
}
}
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()
}
}
fn extract_jwt_token(req: &ServiceRequest) -> Option<String> {
if let Some(cookie_header) = req.headers().get("cookie") {
if let Ok(cookie_str) = cookie_header.to_str() {
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());
}
}
}
}
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 {
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();
let app_config = req.app_data::<web::Data<CorteqApp>>().ok_or_else(|| {
tracing::error!("CorteqApp not found in app_data");
ErrorBadRequest("Application configuration error")
})?;
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;
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 {
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")
})?;
let ctx = TenantContext::from(tenant);
app_config.tenant_cache.set(tenant_id, ctx.clone()).await;
ctx
};
req.extensions_mut().insert(tenant_context);
let res = svc.call(req).await?;
Ok(res)
})
}
}