#![cfg(feature = "auth")]
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::error::AppError;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TenantContext {
pub organization_id: Uuid,
#[serde(default)]
pub keycloak_org_id: String,
pub user_sub: String,
#[serde(default)]
pub permissions: Vec<String>,
}
impl TenantContext {
pub fn has_permission(&self, permission: &str) -> bool {
self.permissions.iter().any(|code| code == permission)
}
pub fn require_permission(&self, permission: &str) -> Result<(), AppError> {
if self.has_permission(permission) {
Ok(())
} else {
Err(AppError::Forbidden {
message: format!("permission required: {permission}"),
})
}
}
pub fn require_org_match(&self, organization_id: Uuid) -> Result<(), AppError> {
if self.organization_id == organization_id {
Ok(())
} else {
Err(AppError::Forbidden {
message: "active organization mismatch".to_string(),
})
}
}
}
#[cfg(feature = "web")]
mod web_impl {
use super::*;
use actix_web::{dev::Payload, FromRequest, HttpMessage, HttpRequest};
use futures_util::future::{ready, Ready};
impl FromRequest for TenantContext {
type Error = AppError;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
match req.extensions().get::<TenantContext>().cloned() {
Some(ctx) => ready(Ok(ctx)),
None => ready(Err(AppError::Forbidden {
message: "Missing tenant context (no active organization)".to_string(),
})),
}
}
}
}