//! Enhanced sharing system. //! //! Provides comprehensive sharing capabilities: //! - Public link sharing with optional password/expiry //! - User-to-user sharing with granular permissions //! - Collection/tag sharing with inheritance //! - Activity logging and notifications use std::fmt; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::model::MediaId; use crate::users::UserId; /// Unique identifier for a share. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ShareId(pub Uuid); impl ShareId { pub fn new() -> Self { Self(Uuid::now_v7()) } } impl Default for ShareId { fn default() -> Self { Self::new() } } impl fmt::Display for ShareId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } /// What is being shared. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ShareTarget { Media { media_id: MediaId }, Collection { collection_id: Uuid }, Tag { tag_id: Uuid }, SavedSearch { search_id: Uuid }, } impl ShareTarget { pub fn target_type(&self) -> &'static str { match self { Self::Media { .. } => "media", Self::Collection { .. } => "collection", Self::Tag { .. } => "tag", Self::SavedSearch { .. } => "saved_search", } } pub fn target_id(&self) -> Uuid { match self { Self::Media { media_id } => media_id.0, Self::Collection { collection_id } => *collection_id, Self::Tag { tag_id } => *tag_id, Self::SavedSearch { search_id } => *search_id, } } } /// Who the share is with. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ShareRecipient { /// Public link accessible to anyone with the token PublicLink { token: String, password_hash: Option, }, /// Shared with a specific user User { user_id: UserId }, /// Shared with a group Group { group_id: Uuid }, /// Shared with a federated user on another server Federated { user_handle: String, server_url: String, }, } impl ShareRecipient { pub fn recipient_type(&self) -> &'static str { match self { Self::PublicLink { .. } => "public_link", Self::User { .. } => "user", Self::Group { .. } => "group", Self::Federated { .. } => "federated", } } } /// Permissions granted by a share. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct SharePermissions { /// Can view the content pub can_view: bool, /// Can download the content pub can_download: bool, /// Can edit the content/metadata pub can_edit: bool, /// Can delete the content pub can_delete: bool, /// Can reshare with others pub can_reshare: bool, /// Can add new items (for collections) pub can_add: bool, } impl SharePermissions { /// View-only permissions pub fn view_only() -> Self { Self { can_view: true, ..Default::default() } } /// Download permissions (includes view) pub fn download() -> Self { Self { can_view: true, can_download: true, ..Default::default() } } /// Edit permissions (includes view and download) pub fn edit() -> Self { Self { can_view: true, can_download: true, can_edit: true, can_add: true, ..Default::default() } } /// Full permissions pub fn full() -> Self { Self { can_view: true, can_download: true, can_edit: true, can_delete: true, can_reshare: true, can_add: true, } } /// Merge permissions (takes the most permissive of each) pub fn merge(&self, other: &Self) -> Self { Self { can_view: self.can_view || other.can_view, can_download: self.can_download || other.can_download, can_edit: self.can_edit || other.can_edit, can_delete: self.can_delete || other.can_delete, can_reshare: self.can_reshare || other.can_reshare, can_add: self.can_add || other.can_add, } } } /// A share record. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Share { pub id: ShareId, pub target: ShareTarget, pub owner_id: UserId, pub recipient: ShareRecipient, pub permissions: SharePermissions, pub note: Option, pub expires_at: Option>, pub access_count: u64, pub last_accessed: Option>, /// Whether children (media in collection, etc.) inherit this share pub inherit_to_children: bool, /// Parent share if this was created via reshare pub parent_share_id: Option, pub created_at: DateTime, pub updated_at: DateTime, } impl Share { /// Create a new public link share. pub fn new_public_link( owner_id: UserId, target: ShareTarget, token: String, permissions: SharePermissions, ) -> Self { let now = Utc::now(); Self { id: ShareId::new(), target, owner_id, recipient: ShareRecipient::PublicLink { token, password_hash: None, }, permissions, note: None, expires_at: None, access_count: 0, last_accessed: None, inherit_to_children: true, parent_share_id: None, created_at: now, updated_at: now, } } /// Create a new user share. pub fn new_user_share( owner_id: UserId, target: ShareTarget, recipient_user_id: UserId, permissions: SharePermissions, ) -> Self { let now = Utc::now(); Self { id: ShareId::new(), target, owner_id, recipient: ShareRecipient::User { user_id: recipient_user_id, }, permissions, note: None, expires_at: None, access_count: 0, last_accessed: None, inherit_to_children: true, parent_share_id: None, created_at: now, updated_at: now, } } /// Check if the share has expired. pub fn is_expired(&self) -> bool { self.expires_at.map(|exp| exp < Utc::now()).unwrap_or(false) } /// Check if this is a public link share. pub fn is_public(&self) -> bool { matches!(self.recipient, ShareRecipient::PublicLink { .. }) } /// Get the public token if this is a public link share. pub fn public_token(&self) -> Option<&str> { match &self.recipient { ShareRecipient::PublicLink { token, .. } => Some(token), _ => None, } } } /// Types of share activity actions. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ShareActivityAction { Created, Updated, Accessed, Downloaded, Revoked, Expired, PasswordFailed, } impl fmt::Display for ShareActivityAction { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Created => write!(f, "created"), Self::Updated => write!(f, "updated"), Self::Accessed => write!(f, "accessed"), Self::Downloaded => write!(f, "downloaded"), Self::Revoked => write!(f, "revoked"), Self::Expired => write!(f, "expired"), Self::PasswordFailed => write!(f, "password_failed"), } } } impl std::str::FromStr for ShareActivityAction { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "created" => Ok(Self::Created), "updated" => Ok(Self::Updated), "accessed" => Ok(Self::Accessed), "downloaded" => Ok(Self::Downloaded), "revoked" => Ok(Self::Revoked), "expired" => Ok(Self::Expired), "password_failed" => Ok(Self::PasswordFailed), _ => Err(format!("unknown share activity action: {}", s)), } } } /// Activity log entry for a share. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ShareActivity { pub id: Uuid, pub share_id: ShareId, pub actor_id: Option, pub actor_ip: Option, pub action: ShareActivityAction, pub details: Option, pub timestamp: DateTime, } impl ShareActivity { pub fn new(share_id: ShareId, action: ShareActivityAction) -> Self { Self { id: Uuid::now_v7(), share_id, actor_id: None, actor_ip: None, action, details: None, timestamp: Utc::now(), } } pub fn with_actor(mut self, actor_id: UserId) -> Self { self.actor_id = Some(actor_id); self } pub fn with_ip(mut self, ip: &str) -> Self { self.actor_ip = Some(ip.to_string()); self } pub fn with_details(mut self, details: &str) -> Self { self.details = Some(details.to_string()); self } } /// Types of share notifications. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ShareNotificationType { NewShare, ShareUpdated, ShareRevoked, ShareExpiring, ShareAccessed, } impl fmt::Display for ShareNotificationType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::NewShare => write!(f, "new_share"), Self::ShareUpdated => write!(f, "share_updated"), Self::ShareRevoked => write!(f, "share_revoked"), Self::ShareExpiring => write!(f, "share_expiring"), Self::ShareAccessed => write!(f, "share_accessed"), } } } impl std::str::FromStr for ShareNotificationType { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "new_share" => Ok(Self::NewShare), "share_updated" => Ok(Self::ShareUpdated), "share_revoked" => Ok(Self::ShareRevoked), "share_expiring" => Ok(Self::ShareExpiring), "share_accessed" => Ok(Self::ShareAccessed), _ => Err(format!("unknown share notification type: {}", s)), } } } /// A notification about a share. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ShareNotification { pub id: Uuid, pub user_id: UserId, pub share_id: ShareId, pub notification_type: ShareNotificationType, pub is_read: bool, pub created_at: DateTime, } impl ShareNotification { pub fn new( user_id: UserId, share_id: ShareId, notification_type: ShareNotificationType, ) -> Self { Self { id: Uuid::now_v7(), user_id, share_id, notification_type, is_read: false, created_at: Utc::now(), } } } /// Generate a random share token using UUID. pub fn generate_share_token() -> String { // Use UUIDv4 for random tokens - simple string representation Uuid::new_v4().simple().to_string() } /// Hash a share password. pub fn hash_share_password(password: &str) -> String { // Use BLAKE3 for password hashing (in production, use Argon2) blake3::hash(password.as_bytes()).to_hex().to_string() } /// Verify a share password. pub fn verify_share_password(password: &str, hash: &str) -> bool { let computed = hash_share_password(password); computed == hash }