use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::errors::AppError;
use crate::repositories::pagination::{cap_limit, cap_offset};
use crate::services::Permission;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomRole {
pub id: Uuid,
pub org_id: Uuid,
pub name: String,
pub description: Option<String>,
pub permissions: HashSet<String>,
pub is_default: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl CustomRole {
pub fn new(org_id: Uuid, name: impl Into<String>, permissions: HashSet<String>) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
org_id,
name: name.into(),
description: None,
permissions,
is_default: false,
created_at: now,
updated_at: now,
}
}
pub fn has_permission(&self, permission: &str) -> bool {
self.permissions.contains(permission)
}
pub fn has_permission_enum(&self, permission: Permission) -> bool {
self.permissions.contains(permission.as_str())
}
}
#[async_trait]
pub trait CustomRoleRepository: Send + Sync {
async fn create(&self, role: CustomRole) -> Result<CustomRole, AppError>;
async fn find_by_id(&self, id: Uuid) -> Result<Option<CustomRole>, AppError>;
async fn find_by_org(&self, org_id: Uuid) -> Result<Vec<CustomRole>, AppError>;
async fn find_by_org_paged(
&self,
org_id: Uuid,
limit: u32,
offset: u32,
) -> Result<Vec<CustomRole>, AppError>;
async fn count_by_org(&self, org_id: Uuid) -> Result<u64, AppError>;
async fn find_by_org_and_name(
&self,
org_id: Uuid,
name: &str,
) -> Result<Option<CustomRole>, AppError>;
async fn update(&self, role: CustomRole) -> Result<CustomRole, AppError>;
async fn delete(&self, id: Uuid) -> Result<bool, AppError>;
async fn delete_by_org(&self, org_id: Uuid) -> Result<u64, AppError>;
async fn get_default_role(&self, org_id: Uuid) -> Result<Option<CustomRole>, AppError>;
async fn set_default_role(&self, org_id: Uuid, role_id: Uuid) -> Result<(), AppError>;
}
pub struct InMemoryCustomRoleRepository {
roles: RwLock<HashMap<Uuid, CustomRole>>,
}
impl InMemoryCustomRoleRepository {
pub fn new() -> Self {
Self {
roles: RwLock::new(HashMap::new()),
}
}
}
impl Default for InMemoryCustomRoleRepository {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl CustomRoleRepository for InMemoryCustomRoleRepository {
async fn create(&self, role: CustomRole) -> Result<CustomRole, AppError> {
let mut roles = self.roles.write().await;
for existing in roles.values() {
if existing.org_id == role.org_id
&& existing.name.to_lowercase() == role.name.to_lowercase()
{
return Err(AppError::Validation(format!(
"Role '{}' already exists in this organization",
role.name
)));
}
}
roles.insert(role.id, role.clone());
Ok(role)
}
async fn find_by_id(&self, id: Uuid) -> Result<Option<CustomRole>, AppError> {
let roles = self.roles.read().await;
Ok(roles.get(&id).cloned())
}
async fn find_by_org(&self, org_id: Uuid) -> Result<Vec<CustomRole>, AppError> {
let roles = self.roles.read().await;
Ok(roles
.values()
.filter(|r| r.org_id == org_id)
.cloned()
.collect())
}
async fn find_by_org_paged(
&self,
org_id: Uuid,
limit: u32,
offset: u32,
) -> Result<Vec<CustomRole>, AppError> {
let roles = self.roles.read().await;
let mut results: Vec<_> = roles
.values()
.filter(|r| r.org_id == org_id)
.cloned()
.collect();
results.sort_by(|a, b| a.created_at.cmp(&b.created_at));
let capped_limit = cap_limit(limit);
let capped_offset = cap_offset(offset);
let start = capped_offset as usize;
let end = start.saturating_add(capped_limit as usize);
Ok(results.into_iter().skip(start).take(end - start).collect())
}
async fn count_by_org(&self, org_id: Uuid) -> Result<u64, AppError> {
let roles = self.roles.read().await;
Ok(roles.values().filter(|r| r.org_id == org_id).count() as u64)
}
async fn find_by_org_and_name(
&self,
org_id: Uuid,
name: &str,
) -> Result<Option<CustomRole>, AppError> {
let roles = self.roles.read().await;
Ok(roles
.values()
.find(|r| r.org_id == org_id && r.name.to_lowercase() == name.to_lowercase())
.cloned())
}
async fn update(&self, role: CustomRole) -> Result<CustomRole, AppError> {
let mut roles = self.roles.write().await;
if !roles.contains_key(&role.id) {
return Err(AppError::NotFound("Role not found".into()));
}
for existing in roles.values() {
if existing.id != role.id
&& existing.org_id == role.org_id
&& existing.name.to_lowercase() == role.name.to_lowercase()
{
return Err(AppError::Validation(format!(
"Role '{}' already exists in this organization",
role.name
)));
}
}
let mut updated = role;
updated.updated_at = Utc::now();
roles.insert(updated.id, updated.clone());
Ok(updated)
}
async fn delete(&self, id: Uuid) -> Result<bool, AppError> {
let mut roles = self.roles.write().await;
Ok(roles.remove(&id).is_some())
}
async fn delete_by_org(&self, org_id: Uuid) -> Result<u64, AppError> {
let mut roles = self.roles.write().await;
let before = roles.len();
roles.retain(|_, role| role.org_id != org_id);
Ok((before - roles.len()) as u64)
}
async fn get_default_role(&self, org_id: Uuid) -> Result<Option<CustomRole>, AppError> {
let roles = self.roles.read().await;
Ok(roles
.values()
.find(|r| r.org_id == org_id && r.is_default)
.cloned())
}
async fn set_default_role(&self, org_id: Uuid, role_id: Uuid) -> Result<(), AppError> {
let mut roles = self.roles.write().await;
let role = roles
.get(&role_id)
.ok_or(AppError::NotFound("Role not found".into()))?;
if role.org_id != org_id {
return Err(AppError::Forbidden(
"Role does not belong to this org".into(),
));
}
for r in roles.values_mut() {
if r.org_id == org_id {
r.is_default = false;
}
}
if let Some(r) = roles.get_mut(&role_id) {
r.is_default = true;
r.updated_at = Utc::now();
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_role(org_id: Uuid, name: &str) -> CustomRole {
let mut permissions = HashSet::new();
permissions.insert("member.read".to_string());
permissions.insert("org.read".to_string());
CustomRole::new(org_id, name, permissions)
}
#[tokio::test]
async fn test_create_and_find_role() {
let repo = InMemoryCustomRoleRepository::new();
let org_id = Uuid::new_v4();
let role = create_test_role(org_id, "Developer");
let created = repo.create(role).await.unwrap();
assert_eq!(created.name, "Developer");
assert_eq!(created.org_id, org_id);
let found = repo.find_by_id(created.id).await.unwrap();
assert!(found.is_some());
assert_eq!(found.unwrap().name, "Developer");
}
#[tokio::test]
async fn test_find_by_org() {
let repo = InMemoryCustomRoleRepository::new();
let org_id = Uuid::new_v4();
let other_org_id = Uuid::new_v4();
repo.create(create_test_role(org_id, "Developer"))
.await
.unwrap();
repo.create(create_test_role(org_id, "Designer"))
.await
.unwrap();
repo.create(create_test_role(other_org_id, "Manager"))
.await
.unwrap();
let roles = repo.find_by_org(org_id).await.unwrap();
assert_eq!(roles.len(), 2);
}
#[tokio::test]
async fn test_find_by_org_paged_and_count() {
let repo = InMemoryCustomRoleRepository::new();
let org_id = Uuid::new_v4();
repo.create(create_test_role(org_id, "Developer"))
.await
.unwrap();
repo.create(create_test_role(org_id, "Designer"))
.await
.unwrap();
let total = repo.count_by_org(org_id).await.unwrap();
assert_eq!(total, 2);
let roles = repo.find_by_org_paged(org_id, 1, 1).await.unwrap();
assert_eq!(roles.len(), 1);
}
#[tokio::test]
async fn test_duplicate_name_rejected() {
let repo = InMemoryCustomRoleRepository::new();
let org_id = Uuid::new_v4();
repo.create(create_test_role(org_id, "Developer"))
.await
.unwrap();
let result = repo.create(create_test_role(org_id, "developer")).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_same_name_different_org_allowed() {
let repo = InMemoryCustomRoleRepository::new();
let org1 = Uuid::new_v4();
let org2 = Uuid::new_v4();
repo.create(create_test_role(org1, "Developer"))
.await
.unwrap();
let result = repo.create(create_test_role(org2, "Developer")).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_update_role() {
let repo = InMemoryCustomRoleRepository::new();
let org_id = Uuid::new_v4();
let role = repo
.create(create_test_role(org_id, "Developer"))
.await
.unwrap();
let mut updated = role.clone();
updated.name = "Senior Developer".to_string();
updated.description = Some("Senior engineering role".to_string());
let result = repo.update(updated).await.unwrap();
assert_eq!(result.name, "Senior Developer");
assert_eq!(
result.description,
Some("Senior engineering role".to_string())
);
}
#[tokio::test]
async fn test_set_default_role() {
let repo = InMemoryCustomRoleRepository::new();
let org_id = Uuid::new_v4();
let role1 = repo
.create(create_test_role(org_id, "Member"))
.await
.unwrap();
let role2 = repo
.create(create_test_role(org_id, "Viewer"))
.await
.unwrap();
repo.set_default_role(org_id, role1.id).await.unwrap();
let default = repo.get_default_role(org_id).await.unwrap();
assert!(default.is_some());
assert_eq!(default.unwrap().id, role1.id);
repo.set_default_role(org_id, role2.id).await.unwrap();
let default = repo.get_default_role(org_id).await.unwrap();
assert!(default.is_some());
assert_eq!(default.unwrap().id, role2.id);
let role1_updated = repo.find_by_id(role1.id).await.unwrap().unwrap();
assert!(!role1_updated.is_default);
}
#[tokio::test]
async fn test_has_permission() {
let org_id = Uuid::new_v4();
let role = create_test_role(org_id, "Developer");
assert!(role.has_permission("member.read"));
assert!(role.has_permission("org.read"));
assert!(!role.has_permission("org.delete"));
}
}