domain: add Sharing entities and ports (ShareScope, ShareTarget, ShareLink, InviteCode, VisibilityFilter)

This commit is contained in:
2026-05-31 03:33:00 +02:00
parent 1d3060fa12
commit ba53e0fa70
12 changed files with 302 additions and 0 deletions

View 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;
}
}

View File

@@ -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;

View 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;
}
}

View 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,
}
}
}

View 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 }
}
}

View 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
}
}

View File

@@ -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;

View 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>;
}

View 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>;
}

View File

@@ -13,3 +13,5 @@ mod derivative_asset;
mod duplicate;
mod album;
mod tag;
mod share_scope;
mod share_link;

View 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());
}

View 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());
}