use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::errors::AppError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OrgRole {
Owner,
Admin,
Member,
}
impl OrgRole {
pub fn as_str(&self) -> &'static str {
match self {
OrgRole::Owner => "owner",
OrgRole::Admin => "admin",
OrgRole::Member => "member",
}
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Option<Self> {
if s.eq_ignore_ascii_case("owner") {
Some(OrgRole::Owner)
} else if s.eq_ignore_ascii_case("admin") {
Some(OrgRole::Admin)
} else if s.eq_ignore_ascii_case("member") {
Some(OrgRole::Member)
} else {
None
}
}
pub fn has_at_least(&self, other: OrgRole) -> bool {
self.level() >= other.level()
}
fn level(&self) -> u8 {
match self {
OrgRole::Owner => 3,
OrgRole::Admin => 2,
OrgRole::Member => 1,
}
}
}
impl std::fmt::Display for OrgRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct MembershipEntity {
pub id: Uuid,
pub user_id: Uuid,
pub org_id: Uuid,
pub role: OrgRole,
pub joined_at: DateTime<Utc>,
}
#[derive(Debug, Clone)]
pub struct MemberWithUser {
pub membership: MembershipEntity,
pub email: Option<String>,
pub name: Option<String>,
}
impl MembershipEntity {
pub fn new(user_id: Uuid, org_id: Uuid, role: OrgRole) -> Self {
Self {
id: Uuid::new_v4(),
user_id,
org_id,
role,
joined_at: Utc::now(),
}
}
pub fn new_owner(user_id: Uuid, org_id: Uuid) -> Self {
Self::new(user_id, org_id, OrgRole::Owner)
}
}
#[async_trait]
pub trait MembershipRepository: Send + Sync {
async fn find_by_id(&self, id: Uuid) -> Result<Option<MembershipEntity>, AppError>;
async fn find_by_user_and_org(
&self,
user_id: Uuid,
org_id: Uuid,
) -> Result<Option<MembershipEntity>, AppError>;
async fn find_by_user(&self, user_id: Uuid) -> Result<Vec<MembershipEntity>, AppError>;
async fn find_by_user_paged(
&self,
user_id: Uuid,
limit: u32,
offset: u32,
) -> Result<Vec<MembershipEntity>, AppError>;
async fn find_by_org(&self, org_id: Uuid) -> Result<Vec<MembershipEntity>, AppError>;
async fn find_by_org_with_users(&self, org_id: Uuid) -> Result<Vec<MemberWithUser>, AppError>;
async fn find_by_org_with_users_paged(
&self,
org_id: Uuid,
limit: u32,
offset: u32,
) -> Result<Vec<MemberWithUser>, AppError>;
async fn create(&self, membership: MembershipEntity) -> Result<MembershipEntity, AppError>;
async fn update_role(&self, id: Uuid, role: OrgRole) -> Result<MembershipEntity, AppError>;
async fn update_role_if_not_last_owner(
&self,
id: Uuid,
org_id: Uuid,
new_role: OrgRole,
) -> Result<Option<MembershipEntity>, AppError>;
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
async fn delete_if_not_last_owner(&self, id: Uuid, org_id: Uuid) -> Result<bool, AppError>;
async fn delete_by_org(&self, org_id: Uuid) -> Result<u64, AppError>;
async fn count_by_org(&self, org_id: Uuid) -> Result<u64, AppError>;
async fn count_by_user(&self, user_id: Uuid) -> Result<u64, AppError>;
async fn count_owners(&self, org_id: Uuid) -> Result<u64, AppError>;
}
pub struct InMemoryMembershipRepository {
memberships: RwLock<HashMap<Uuid, MembershipEntity>>,
}
impl InMemoryMembershipRepository {
pub fn new() -> Self {
Self {
memberships: RwLock::new(HashMap::new()),
}
}
}
impl Default for InMemoryMembershipRepository {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl MembershipRepository for InMemoryMembershipRepository {
async fn find_by_id(&self, id: Uuid) -> Result<Option<MembershipEntity>, AppError> {
let memberships = self.memberships.read().await;
Ok(memberships.get(&id).cloned())
}
async fn find_by_user_and_org(
&self,
user_id: Uuid,
org_id: Uuid,
) -> Result<Option<MembershipEntity>, AppError> {
let memberships = self.memberships.read().await;
Ok(memberships
.values()
.find(|m| m.user_id == user_id && m.org_id == org_id)
.cloned())
}
async fn find_by_user(&self, user_id: Uuid) -> Result<Vec<MembershipEntity>, AppError> {
let memberships = self.memberships.read().await;
Ok(memberships
.values()
.filter(|m| m.user_id == user_id)
.cloned()
.collect())
}
async fn find_by_user_paged(
&self,
user_id: Uuid,
limit: u32,
offset: u32,
) -> Result<Vec<MembershipEntity>, AppError> {
let memberships = self.memberships.read().await;
let mut results: Vec<_> = memberships
.values()
.filter(|m| m.user_id == user_id)
.cloned()
.collect();
results.sort_by(|a, b| b.joined_at.cmp(&a.joined_at));
let start = offset as usize;
if start >= results.len() {
return Ok(Vec::new());
}
let end = (start + limit as usize).min(results.len());
Ok(results[start..end].to_vec())
}
async fn find_by_org(&self, org_id: Uuid) -> Result<Vec<MembershipEntity>, AppError> {
let memberships = self.memberships.read().await;
Ok(memberships
.values()
.filter(|m| m.org_id == org_id)
.cloned()
.collect())
}
async fn find_by_org_with_users(&self, org_id: Uuid) -> Result<Vec<MemberWithUser>, AppError> {
let memberships = self.find_by_org(org_id).await?;
Ok(memberships
.into_iter()
.map(|membership| MemberWithUser {
membership,
email: None,
name: None,
})
.collect())
}
async fn find_by_org_with_users_paged(
&self,
org_id: Uuid,
limit: u32,
offset: u32,
) -> Result<Vec<MemberWithUser>, AppError> {
let mut members = self.find_by_org_with_users(org_id).await?;
members.sort_by(|a, b| a.membership.joined_at.cmp(&b.membership.joined_at));
let start = offset as usize;
let limit = limit as usize;
Ok(members.into_iter().skip(start).take(limit).collect())
}
async fn create(&self, membership: MembershipEntity) -> Result<MembershipEntity, AppError> {
let mut memberships = self.memberships.write().await;
if let Some(existing) = memberships
.values()
.find(|m| m.user_id == membership.user_id && m.org_id == membership.org_id)
{
return Ok(existing.clone());
}
memberships.insert(membership.id, membership.clone());
Ok(membership)
}
async fn update_role(&self, id: Uuid, role: OrgRole) -> Result<MembershipEntity, AppError> {
let mut memberships = self.memberships.write().await;
let membership = memberships
.get_mut(&id)
.ok_or_else(|| AppError::NotFound("Membership not found".into()))?;
membership.role = role;
Ok(membership.clone())
}
async fn update_role_if_not_last_owner(
&self,
id: Uuid,
org_id: Uuid,
new_role: OrgRole,
) -> Result<Option<MembershipEntity>, AppError> {
let mut memberships = self.memberships.write().await;
let membership = memberships
.get(&id)
.ok_or_else(|| AppError::NotFound("Membership not found".into()))?;
if membership.role == OrgRole::Owner && new_role != OrgRole::Owner {
let owner_count = memberships
.values()
.filter(|m| m.org_id == org_id && m.role == OrgRole::Owner)
.count();
if owner_count <= 1 {
return Ok(None);
}
}
let membership = memberships.get_mut(&id).ok_or_else(|| {
AppError::Internal(anyhow::anyhow!("Membership disappeared during update"))
})?;
membership.role = new_role;
Ok(Some(membership.clone()))
}
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
let mut memberships = self.memberships.write().await;
if memberships.remove(&id).is_none() {
return Err(AppError::NotFound("Membership not found".into()));
}
Ok(())
}
async fn delete_if_not_last_owner(&self, id: Uuid, org_id: Uuid) -> Result<bool, AppError> {
let mut memberships = self.memberships.write().await;
let membership = memberships
.get(&id)
.ok_or_else(|| AppError::NotFound("Membership not found".into()))?;
if membership.role == OrgRole::Owner {
let owner_count = memberships
.values()
.filter(|m| m.org_id == org_id && m.role == OrgRole::Owner)
.count();
if owner_count <= 1 {
return Ok(false);
}
}
memberships.remove(&id);
Ok(true)
}
async fn delete_by_org(&self, org_id: Uuid) -> Result<u64, AppError> {
let mut memberships = self.memberships.write().await;
let ids_to_remove: Vec<Uuid> = memberships
.values()
.filter(|m| m.org_id == org_id)
.map(|m| m.id)
.collect();
let count = ids_to_remove.len() as u64;
for id in ids_to_remove {
memberships.remove(&id);
}
Ok(count)
}
async fn count_by_org(&self, org_id: Uuid) -> Result<u64, AppError> {
let memberships = self.memberships.read().await;
Ok(memberships.values().filter(|m| m.org_id == org_id).count() as u64)
}
async fn count_by_user(&self, user_id: Uuid) -> Result<u64, AppError> {
let memberships = self.memberships.read().await;
Ok(memberships
.values()
.filter(|m| m.user_id == user_id)
.count() as u64)
}
async fn count_owners(&self, org_id: Uuid) -> Result<u64, AppError> {
let memberships = self.memberships.read().await;
Ok(memberships
.values()
.filter(|m| m.org_id == org_id && m.role == OrgRole::Owner)
.count() as u64)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_create_membership() {
let repo = InMemoryMembershipRepository::new();
let user_id = Uuid::new_v4();
let org_id = Uuid::new_v4();
let membership = MembershipEntity::new(user_id, org_id, OrgRole::Member);
let created = repo.create(membership).await.unwrap();
assert_eq!(created.user_id, user_id);
assert_eq!(created.org_id, org_id);
assert_eq!(created.role, OrgRole::Member);
}
#[tokio::test]
async fn test_create_owner_membership() {
let repo = InMemoryMembershipRepository::new();
let user_id = Uuid::new_v4();
let org_id = Uuid::new_v4();
let membership = MembershipEntity::new_owner(user_id, org_id);
let created = repo.create(membership).await.unwrap();
assert_eq!(created.role, OrgRole::Owner);
}
#[tokio::test]
async fn test_duplicate_membership_rejected() {
let repo = InMemoryMembershipRepository::new();
let user_id = Uuid::new_v4();
let org_id = Uuid::new_v4();
let m1 = MembershipEntity::new(user_id, org_id, OrgRole::Member);
let m2 = MembershipEntity::new(user_id, org_id, OrgRole::Admin);
let created = repo.create(m1).await.unwrap();
let duplicate = repo.create(m2).await.unwrap();
assert_eq!(duplicate.id, created.id);
assert_eq!(duplicate.role, OrgRole::Member);
}
#[tokio::test]
async fn test_find_by_user_and_org() {
let repo = InMemoryMembershipRepository::new();
let user_id = Uuid::new_v4();
let org_id = Uuid::new_v4();
let membership = MembershipEntity::new(user_id, org_id, OrgRole::Member);
repo.create(membership).await.unwrap();
let found = repo.find_by_user_and_org(user_id, org_id).await.unwrap();
assert!(found.is_some());
let not_found = repo
.find_by_user_and_org(Uuid::new_v4(), org_id)
.await
.unwrap();
assert!(not_found.is_none());
}
#[tokio::test]
async fn test_update_role() {
let repo = InMemoryMembershipRepository::new();
let user_id = Uuid::new_v4();
let org_id = Uuid::new_v4();
let membership = MembershipEntity::new(user_id, org_id, OrgRole::Member);
let created = repo.create(membership).await.unwrap();
let updated = repo.update_role(created.id, OrgRole::Admin).await.unwrap();
assert_eq!(updated.role, OrgRole::Admin);
}
#[tokio::test]
async fn test_find_by_org_with_users_paged() {
let repo = InMemoryMembershipRepository::new();
let org_id = Uuid::new_v4();
let base_time = Utc::now();
for i in 0..3 {
let mut membership = MembershipEntity::new(Uuid::new_v4(), org_id, OrgRole::Member);
membership.joined_at = base_time + chrono::Duration::seconds(i);
repo.create(membership).await.unwrap();
}
let page = repo
.find_by_org_with_users_paged(org_id, 2, 1)
.await
.unwrap();
assert_eq!(page.len(), 2);
assert!(page[0].membership.joined_at <= page[1].membership.joined_at);
}
#[tokio::test]
async fn test_find_by_user_paged() {
let repo = InMemoryMembershipRepository::new();
let user_id = Uuid::new_v4();
let base_time = Utc::now();
for i in 0..3 {
let mut membership = MembershipEntity::new(user_id, Uuid::new_v4(), OrgRole::Member);
membership.joined_at = base_time + chrono::Duration::seconds(i);
repo.create(membership).await.unwrap();
}
let page = repo.find_by_user_paged(user_id, 2, 1).await.unwrap();
assert_eq!(page.len(), 2);
assert!(page[0].joined_at >= page[1].joined_at);
}
#[tokio::test]
async fn test_count_by_user() {
let repo = InMemoryMembershipRepository::new();
let user_id = Uuid::new_v4();
repo.create(MembershipEntity::new(
user_id,
Uuid::new_v4(),
OrgRole::Member,
))
.await
.unwrap();
repo.create(MembershipEntity::new(
user_id,
Uuid::new_v4(),
OrgRole::Admin,
))
.await
.unwrap();
repo.create(MembershipEntity::new(
Uuid::new_v4(),
Uuid::new_v4(),
OrgRole::Member,
))
.await
.unwrap();
let count = repo.count_by_user(user_id).await.unwrap();
assert_eq!(count, 2);
}
#[tokio::test]
async fn test_find_by_org_with_users_returns_memberships() {
let repo = InMemoryMembershipRepository::new();
let org_id = Uuid::new_v4();
let user_id = Uuid::new_v4();
repo.create(MembershipEntity::new(user_id, org_id, OrgRole::Member))
.await
.unwrap();
let results = repo.find_by_org_with_users(org_id).await.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].membership.user_id, user_id);
assert!(results[0].email.is_none());
assert!(results[0].name.is_none());
}
#[tokio::test]
async fn test_role_hierarchy() {
assert!(OrgRole::Owner.has_at_least(OrgRole::Admin));
assert!(OrgRole::Owner.has_at_least(OrgRole::Member));
assert!(OrgRole::Admin.has_at_least(OrgRole::Member));
assert!(!OrgRole::Member.has_at_least(OrgRole::Admin));
}
#[tokio::test]
async fn test_count_owners() {
let repo = InMemoryMembershipRepository::new();
let org_id = Uuid::new_v4();
let m1 = MembershipEntity::new(Uuid::new_v4(), org_id, OrgRole::Owner);
let m2 = MembershipEntity::new(Uuid::new_v4(), org_id, OrgRole::Admin);
let m3 = MembershipEntity::new(Uuid::new_v4(), org_id, OrgRole::Member);
repo.create(m1).await.unwrap();
repo.create(m2).await.unwrap();
repo.create(m3).await.unwrap();
let count = repo.count_owners(org_id).await.unwrap();
assert_eq!(count, 1);
}
}