use std::{
sync::atomic::{AtomicU64, Ordering},
time::{Duration, Instant},
};
use dashmap::DashMap;
use sqlx::PgPool;
use super::db_backend::RbacDbError;
struct Entry {
roles: Vec<String>,
expires_at: Instant,
}
pub struct AssignedRoles {
pool: PgPool,
cache: DashMap<(String, Option<String>), Entry>,
generation: AtomicU64,
}
impl AssignedRoles {
pub const TTL: Duration = Duration::from_secs(30);
#[must_use]
pub fn new(pool: PgPool) -> Self {
Self {
pool,
cache: DashMap::new(),
generation: AtomicU64::new(0),
}
}
pub async fn of(&self, sub: &str, tenant: Option<&str>) -> Result<Vec<String>, RbacDbError> {
let key = (sub.to_owned(), tenant.map(str::to_owned));
if let Some(entry) = self.cache.get(&key) {
if Instant::now() < entry.expires_at {
return Ok(entry.roles.clone());
}
}
let generation = self.generation.load(Ordering::Acquire);
let roles: Vec<String> = sqlx::query_scalar(
"SELECT r.name FROM fraiseql_user_roles ur
JOIN fraiseql_roles r ON r.id = ur.role_id
WHERE ur.user_id = $1 AND (ur.tenant_id IS NULL OR ur.tenant_id::text = lower($2))",
)
.bind(sub)
.bind(tenant)
.fetch_all(&self.pool)
.await
.map_err(|e| RbacDbError::QueryError(e.to_string()))?;
if self.generation.load(Ordering::Acquire) == generation {
self.cache.insert(
key.clone(),
Entry {
roles: roles.clone(),
expires_at: Instant::now() + Self::TTL,
},
);
if self.generation.load(Ordering::Acquire) != generation {
self.cache.remove(&key);
}
}
Ok(roles)
}
pub fn flush(&self, sub: &str) {
self.generation.fetch_add(1, Ordering::AcqRel);
self.cache.retain(|(cached, _), _| cached != sub);
}
pub fn flush_all(&self) {
self.generation.fetch_add(1, Ordering::AcqRel);
self.cache.clear();
}
}