use std::collections::HashMap;
use chrono::Utc;
use serde_json::Value as JsonValue;
#[derive(Debug, Clone)]
pub struct TenantContext {
id: String,
created_at: String,
metadata: HashMap<String, String>,
}
impl TenantContext {
#[must_use]
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
created_at: Utc::now().to_rfc3339(),
metadata: HashMap::new(),
}
}
#[must_use]
pub fn id(&self) -> &str {
&self.id
}
#[must_use]
pub fn created_at_iso8601(&self) -> Option<&str> {
if self.created_at.is_empty() {
None
} else {
Some(&self.created_at)
}
}
pub fn set_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.metadata.insert(key.into(), value.into());
}
#[must_use]
pub fn get_metadata(&self, key: &str) -> Option<&str> {
self.metadata.get(key).map(String::as_str)
}
#[must_use]
pub const fn metadata(&self) -> &HashMap<String, String> {
&self.metadata
}
pub fn from_jwt_claims(claims: &JsonValue) -> Result<Self, String> {
let tenant_id = claims
.get("tenant_id")
.and_then(|v| v.as_str())
.ok_or_else(|| "Missing or invalid 'tenant_id' claim in JWT".to_string())?;
Ok(Self::new(tenant_id))
}
#[must_use]
pub fn where_clause(&self) -> String {
validate_tenant_id_for_interpolation(&self.id);
format!("tenant_id = '{}'", self.id)
}
#[must_use]
pub fn where_clause_postgresql(&self, param_index: usize) -> String {
format!("tenant_id = ${}", param_index)
}
#[must_use]
pub fn where_clause_parameterized(&self) -> String {
"tenant_id = ?".to_string()
}
}
fn validate_tenant_id_for_interpolation(tenant_id: &str) {
assert!(
!tenant_id.is_empty()
&& tenant_id.chars().all(|c| c.is_alphanumeric() || matches!(c, '.' | '_' | '-')),
"SECURITY: tenant_id '{tenant_id}' contains characters that are unsafe for SQL interpolation. \
Use where_clause_postgresql() or where_clause_parameterized() instead."
);
}
#[must_use]
pub fn where_clause(tenant_id: &str) -> String {
validate_tenant_id_for_interpolation(tenant_id);
format!("tenant_id = '{}'", tenant_id)
}
#[must_use]
pub fn where_clause_postgresql(param_index: usize) -> String {
format!("tenant_id = ${}", param_index)
}
#[must_use]
pub fn where_clause_parameterized() -> String {
"tenant_id = ?".to_string()
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod jwt_extraction_tests;
#[cfg(test)]
mod query_filter_tests;