//! 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::{error::PinakesError, model::MediaId, users::UserId}; /// Unique identifier for a share. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ShareId(pub Uuid); impl ShareId { /// Creates a new share ID. #[must_use] 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 { /// Returns the type of target being shared. #[must_use] pub const fn target_type(&self) -> &'static str { match self { Self::Media { .. } => "media", Self::Collection { .. } => "collection", Self::Tag { .. } => "tag", Self::SavedSearch { .. } => "saved_search", } } /// Returns the ID of the target being shared. #[must_use] pub const 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 { /// Returns the type of recipient. #[must_use] pub const fn recipient_type(&self) -> &'static str { match self { Self::PublicLink { .. } => "public_link", Self::User { .. } => "user", Self::Group { .. } => "group", Self::Federated { .. } => "federated", } } } /// Read-access permissions granted by a share. #[derive( Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, )] pub struct ShareViewPermissions { /// Can view the content pub can_view: bool, /// Can download the content pub can_download: bool, /// Can reshare with others pub can_reshare: bool, } /// Write-access permissions granted by a share. #[derive( Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, )] pub struct ShareMutatePermissions { /// Can edit the content/metadata pub can_edit: bool, /// Can delete the content pub can_delete: bool, /// Can add new items (for collections) pub can_add: bool, } /// Permissions granted by a share. #[derive( Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, )] pub struct SharePermissions { #[serde(flatten)] pub view: ShareViewPermissions, #[serde(flatten)] pub mutate: ShareMutatePermissions, } impl SharePermissions { /// Creates a new share with view-only permissions. #[must_use] pub fn view_only() -> Self { Self { view: ShareViewPermissions { can_view: true, ..Default::default() }, mutate: ShareMutatePermissions::default(), } } /// Creates a new share with download permissions. #[must_use] pub fn download() -> Self { Self { view: ShareViewPermissions { can_view: true, can_download: true, ..Default::default() }, mutate: ShareMutatePermissions::default(), } } /// Creates a new share with edit permissions. #[must_use] pub fn edit() -> Self { Self { view: ShareViewPermissions { can_view: true, can_download: true, ..Default::default() }, mutate: ShareMutatePermissions { can_edit: true, can_add: true, ..Default::default() }, } } /// Creates a new share with full permissions. #[must_use] pub const fn full() -> Self { Self { view: ShareViewPermissions { can_view: true, can_download: true, can_reshare: true, }, mutate: ShareMutatePermissions { can_edit: true, can_delete: true, can_add: true, }, } } /// Merges two permission sets, taking the most permissive values. #[must_use] pub const fn merge(&self, other: &Self) -> Self { Self { view: ShareViewPermissions { can_view: self.view.can_view || other.view.can_view, can_download: self.view.can_download || other.view.can_download, can_reshare: self.view.can_reshare || other.view.can_reshare, }, mutate: ShareMutatePermissions { can_edit: self.mutate.can_edit || other.mutate.can_edit, can_delete: self.mutate.can_delete || other.mutate.can_delete, can_add: self.mutate.can_add || other.mutate.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. #[must_use] 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. #[must_use] 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, } } /// Checks if the share has expired. #[must_use] pub fn is_expired(&self) -> bool { self.expires_at.is_some_and(|exp| exp < Utc::now()) } /// Checks if this is a public link share. #[must_use] pub const fn is_public(&self) -> bool { matches!(self.recipient, ShareRecipient::PublicLink { .. }) } /// Returns the public token if this is a public link share. #[must_use] 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 { /// Creates a new share activity entry. #[must_use] 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(), } } /// Sets the actor who performed the activity. #[must_use] pub const fn with_actor(mut self, actor_id: UserId) -> Self { self.actor_id = Some(actor_id); self } /// Sets the IP address of the actor. #[must_use] pub fn with_ip(mut self, ip: &str) -> Self { self.actor_ip = Some(ip.to_string()); self } /// Sets additional details about the activity. #[must_use] 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 { /// Creates a new share notification. #[must_use] 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(), } } } /// Generates a random share token. #[must_use] pub fn generate_share_token() -> String { // Use UUIDv4 for random tokens - simple string representation Uuid::new_v4().simple().to_string() } /// Hashes a share password using Argon2id. /// /// # Errors /// /// Returns an error if hashing fails. pub fn hash_share_password(password: &str) -> Result { crate::users::auth::hash_password(password) } /// Verifies a share password against an Argon2id hash. #[must_use] pub fn verify_share_password(password: &str, hash: &str) -> bool { crate::users::auth::verify_password(password, hash).unwrap_or(false) }