domain: add Sharing entities and ports (ShareScope, ShareTarget, ShareLink, InviteCode, VisibilityFilter)
This commit is contained in:
45
crates/domain/src/entities/invite_code.rs
Normal file
45
crates/domain/src/entities/invite_code.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use chrono::Utc;
|
||||
use crate::value_objects::{DateTimeStamp, SystemId};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct InviteCode {
|
||||
pub code_id: SystemId,
|
||||
pub scope_id: SystemId,
|
||||
pub created_by_user_id: SystemId,
|
||||
pub expires_at: Option<DateTimeStamp>,
|
||||
pub max_uses: Option<u32>,
|
||||
pub use_count: u32,
|
||||
pub assigned_role_id: SystemId,
|
||||
}
|
||||
|
||||
impl InviteCode {
|
||||
pub fn new(scope_id: SystemId, created_by: SystemId, role_id: SystemId) -> Self {
|
||||
Self {
|
||||
code_id: SystemId::new(),
|
||||
scope_id,
|
||||
created_by_user_id: created_by,
|
||||
expires_at: None,
|
||||
max_uses: None,
|
||||
use_count: 0,
|
||||
assigned_role_id: role_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
if let Some(exp) = &self.expires_at {
|
||||
if exp.as_datetime() < &Utc::now() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(max) = self.max_uses {
|
||||
if self.use_count >= max {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn record_use(&mut self) {
|
||||
self.use_count += 1;
|
||||
}
|
||||
}
|
||||
@@ -33,3 +33,15 @@ mod collection;
|
||||
pub use album::{Album, AlbumEntry};
|
||||
pub use tag::{AssetTag, Tag, TagSource};
|
||||
pub use collection::Collection;
|
||||
|
||||
mod share_scope;
|
||||
mod share_target;
|
||||
mod share_link;
|
||||
mod invite_code;
|
||||
mod visibility_filter;
|
||||
|
||||
pub use share_scope::{ScopeType, ShareScope, ShareableType};
|
||||
pub use share_target::{ShareTarget, TargetType};
|
||||
pub use share_link::{LinkAccessLevel, ShareLink};
|
||||
pub use invite_code::InviteCode;
|
||||
pub use visibility_filter::VisibilityFilter;
|
||||
|
||||
58
crates/domain/src/entities/share_link.rs
Normal file
58
crates/domain/src/entities/share_link.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use chrono::Utc;
|
||||
use crate::value_objects::{DateTimeStamp, SystemId};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum LinkAccessLevel {
|
||||
ViewOnly,
|
||||
LimitedSearch,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ShareLink {
|
||||
pub scope_id: SystemId,
|
||||
pub token: String,
|
||||
pub expires_at: Option<DateTimeStamp>,
|
||||
pub access_level: LinkAccessLevel,
|
||||
pub is_active: bool,
|
||||
pub max_uses: Option<u32>,
|
||||
pub use_count: u32,
|
||||
}
|
||||
|
||||
impl ShareLink {
|
||||
pub fn new(scope_id: SystemId, token: impl Into<String>, access_level: LinkAccessLevel) -> Self {
|
||||
Self {
|
||||
scope_id,
|
||||
token: token.into(),
|
||||
expires_at: None,
|
||||
access_level,
|
||||
is_active: true,
|
||||
max_uses: None,
|
||||
use_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
if !self.is_active {
|
||||
return false;
|
||||
}
|
||||
if let Some(exp) = &self.expires_at {
|
||||
if exp.as_datetime() < &Utc::now() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(max) = self.max_uses {
|
||||
if self.use_count >= max {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn record_use(&mut self) {
|
||||
self.use_count += 1;
|
||||
}
|
||||
|
||||
pub fn deactivate(&mut self) {
|
||||
self.is_active = false;
|
||||
}
|
||||
}
|
||||
56
crates/domain/src/entities/share_scope.rs
Normal file
56
crates/domain/src/entities/share_scope.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use chrono::Utc;
|
||||
use crate::value_objects::{DateTimeStamp, SystemId};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ScopeType {
|
||||
Private,
|
||||
User,
|
||||
Group,
|
||||
Link,
|
||||
Public,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ShareableType {
|
||||
Asset,
|
||||
Album,
|
||||
Collection,
|
||||
Directory,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ShareScope {
|
||||
pub scope_id: SystemId,
|
||||
pub scope_type: ScopeType,
|
||||
pub shareable_type: ShareableType,
|
||||
pub shareable_id: SystemId,
|
||||
pub created_by_user_id: SystemId,
|
||||
pub expires_at: Option<DateTimeStamp>,
|
||||
pub created_at: DateTimeStamp,
|
||||
}
|
||||
|
||||
impl ShareScope {
|
||||
pub fn new(
|
||||
scope_type: ScopeType,
|
||||
shareable_type: ShareableType,
|
||||
shareable_id: SystemId,
|
||||
created_by: SystemId,
|
||||
) -> Self {
|
||||
Self {
|
||||
scope_id: SystemId::new(),
|
||||
scope_type,
|
||||
shareable_type,
|
||||
shareable_id,
|
||||
created_by_user_id: created_by,
|
||||
expires_at: None,
|
||||
created_at: DateTimeStamp::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_expired(&self) -> bool {
|
||||
match &self.expires_at {
|
||||
Some(exp) => exp.as_datetime() < &Utc::now(),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
21
crates/domain/src/entities/share_target.rs
Normal file
21
crates/domain/src/entities/share_target.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use crate::value_objects::SystemId;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum TargetType {
|
||||
User,
|
||||
Group,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ShareTarget {
|
||||
pub scope_id: SystemId,
|
||||
pub target_type: TargetType,
|
||||
pub target_id: SystemId,
|
||||
pub role_id: SystemId,
|
||||
}
|
||||
|
||||
impl ShareTarget {
|
||||
pub fn new(scope_id: SystemId, target_type: TargetType, target_id: SystemId, role_id: SystemId) -> Self {
|
||||
Self { scope_id, target_type, target_id, role_id }
|
||||
}
|
||||
}
|
||||
28
crates/domain/src/entities/visibility_filter.rs
Normal file
28
crates/domain/src/entities/visibility_filter.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use crate::value_objects::{StructuredData, SystemId};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct VisibilityFilter {
|
||||
pub filter_id: SystemId,
|
||||
pub scope_id: SystemId,
|
||||
pub role_id: SystemId,
|
||||
pub hidden_fields: Vec<String>,
|
||||
}
|
||||
|
||||
impl VisibilityFilter {
|
||||
pub fn new(scope_id: SystemId, role_id: SystemId, hidden_fields: Vec<String>) -> Self {
|
||||
Self {
|
||||
filter_id: SystemId::new(),
|
||||
scope_id,
|
||||
role_id,
|
||||
hidden_fields,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply(&self, data: &StructuredData) -> StructuredData {
|
||||
let mut result = data.clone();
|
||||
for field in &self.hidden_fields {
|
||||
result.remove(field);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
@@ -39,3 +39,9 @@ mod collection_repo;
|
||||
pub use album_repo::AlbumRepository;
|
||||
pub use tag_repo::TagRepository;
|
||||
pub use collection_repo::CollectionRepository;
|
||||
|
||||
mod share_repo;
|
||||
mod visibility_filter_repo;
|
||||
|
||||
pub use share_repo::ShareRepository;
|
||||
pub use visibility_filter_repo::VisibilityFilterRepository;
|
||||
|
||||
24
crates/domain/src/ports/share_repo.rs
Normal file
24
crates/domain/src/ports/share_repo.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{
|
||||
entities::{InviteCode, ShareLink, ShareScope, ShareTarget},
|
||||
errors::DomainError,
|
||||
value_objects::SystemId,
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
pub trait ShareRepository: Send + Sync {
|
||||
async fn save_scope(&self, scope: &ShareScope) -> Result<(), DomainError>;
|
||||
async fn find_scope_by_id(&self, id: &SystemId) -> Result<Option<ShareScope>, DomainError>;
|
||||
async fn find_scopes_for_resource(&self, resource_id: &SystemId) -> Result<Vec<ShareScope>, DomainError>;
|
||||
async fn delete_scope(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
|
||||
async fn save_target(&self, target: &ShareTarget) -> Result<(), DomainError>;
|
||||
async fn find_targets_for_scope(&self, scope_id: &SystemId) -> Result<Vec<ShareTarget>, DomainError>;
|
||||
async fn find_targets_for_user(&self, user_id: &SystemId) -> Result<Vec<ShareTarget>, DomainError>;
|
||||
|
||||
async fn save_link(&self, link: &ShareLink) -> Result<(), DomainError>;
|
||||
async fn find_link_by_token(&self, token: &str) -> Result<Option<ShareLink>, DomainError>;
|
||||
|
||||
async fn save_invite(&self, invite: &InviteCode) -> Result<(), DomainError>;
|
||||
async fn find_invite_by_id(&self, id: &SystemId) -> Result<Option<InviteCode>, DomainError>;
|
||||
}
|
||||
9
crates/domain/src/ports/visibility_filter_repo.rs
Normal file
9
crates/domain/src/ports/visibility_filter_repo.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::{entities::VisibilityFilter, errors::DomainError, value_objects::SystemId};
|
||||
|
||||
#[async_trait]
|
||||
pub trait VisibilityFilterRepository: Send + Sync {
|
||||
async fn find_by_scope_and_role(&self, scope_id: &SystemId, role_id: &SystemId) -> Result<Option<VisibilityFilter>, DomainError>;
|
||||
async fn save(&self, filter: &VisibilityFilter) -> Result<(), DomainError>;
|
||||
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -13,3 +13,5 @@ mod derivative_asset;
|
||||
mod duplicate;
|
||||
mod album;
|
||||
mod tag;
|
||||
mod share_scope;
|
||||
mod share_link;
|
||||
|
||||
25
crates/domain/tests/entities/share_link.rs
Normal file
25
crates/domain/tests/entities/share_link.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use domain::entities::{LinkAccessLevel, ShareLink};
|
||||
use domain::value_objects::SystemId;
|
||||
|
||||
#[test]
|
||||
fn new_link_is_valid() {
|
||||
let link = ShareLink::new(SystemId::new(), "tok123", LinkAccessLevel::ViewOnly);
|
||||
assert!(link.is_valid());
|
||||
assert_eq!(link.use_count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deactivated_invalid() {
|
||||
let mut link = ShareLink::new(SystemId::new(), "tok123", LinkAccessLevel::ViewOnly);
|
||||
link.deactivate();
|
||||
assert!(!link.is_valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_uses_exhausted() {
|
||||
let mut link = ShareLink::new(SystemId::new(), "tok123", LinkAccessLevel::ViewOnly);
|
||||
link.max_uses = Some(2);
|
||||
link.record_use();
|
||||
link.record_use();
|
||||
assert!(!link.is_valid());
|
||||
}
|
||||
16
crates/domain/tests/entities/share_scope.rs
Normal file
16
crates/domain/tests/entities/share_scope.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use chrono::{Duration, Utc};
|
||||
use domain::entities::{ScopeType, ShareScope, ShareableType};
|
||||
use domain::value_objects::{DateTimeStamp, SystemId};
|
||||
|
||||
#[test]
|
||||
fn not_expired_when_no_expiry() {
|
||||
let scope = ShareScope::new(ScopeType::Link, ShareableType::Album, SystemId::new(), SystemId::new());
|
||||
assert!(!scope.is_expired());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expired_when_past() {
|
||||
let mut scope = ShareScope::new(ScopeType::Link, ShareableType::Album, SystemId::new(), SystemId::new());
|
||||
scope.expires_at = Some(DateTimeStamp::from_datetime(Utc::now() - Duration::hours(1)));
|
||||
assert!(scope.is_expired());
|
||||
}
|
||||
Reference in New Issue
Block a user