pinakes-core: add backup, session refresh, share permissions restructure, and fix integrity

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I17da1cf8403bd11d2a6ea31138f97e776a6a6964
This commit is contained in:
raf 2026-03-08 00:42:10 +03:00
commit 4e91cb6679
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
4 changed files with 3096 additions and 2300 deletions

View file

@ -20,6 +20,7 @@ pub struct ShareId(pub Uuid);
impl ShareId {
/// Creates a new share ID.
#[must_use]
pub fn new() -> Self {
Self(Uuid::now_v7())
}
@ -49,7 +50,8 @@ pub enum ShareTarget {
impl ShareTarget {
/// Returns the type of target being shared.
pub fn target_type(&self) -> &'static str {
#[must_use]
pub const fn target_type(&self) -> &'static str {
match self {
Self::Media { .. } => "media",
Self::Collection { .. } => "collection",
@ -59,7 +61,8 @@ impl ShareTarget {
}
/// Returns the ID of the target being shared.
pub fn target_id(&self) -> Uuid {
#[must_use]
pub const fn target_id(&self) -> Uuid {
match self {
Self::Media { media_id } => media_id.0,
Self::Collection { collection_id } => *collection_id,
@ -91,7 +94,8 @@ pub enum ShareRecipient {
impl ShareRecipient {
/// Returns the type of recipient.
pub fn recipient_type(&self) -> &'static str {
#[must_use]
pub const fn recipient_type(&self) -> &'static str {
match self {
Self::PublicLink { .. } => "public_link",
Self::User { .. } => "user",
@ -101,75 +105,117 @@ impl ShareRecipient {
}
}
/// 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 {
/// 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,
#[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 {
can_view: true,
..Default::default()
view: ShareViewPermissions {
can_view: true,
..Default::default()
},
mutate: ShareMutatePermissions::default(),
}
}
/// Creates a new share with download permissions.
#[must_use]
pub fn download() -> Self {
Self {
can_view: true,
can_download: true,
..Default::default()
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 {
can_view: true,
can_download: true,
can_edit: true,
can_add: true,
..Default::default()
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.
pub fn full() -> Self {
#[must_use]
pub const fn full() -> Self {
Self {
can_view: true,
can_download: true,
can_edit: true,
can_delete: true,
can_reshare: true,
can_add: true,
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.
pub fn merge(&self, other: &Self) -> Self {
#[must_use]
pub const 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,
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,
},
}
}
}
@ -196,6 +242,7 @@ pub struct Share {
impl Share {
/// Create a new public link share.
#[must_use]
pub fn new_public_link(
owner_id: UserId,
target: ShareTarget,
@ -224,6 +271,7 @@ impl Share {
}
/// Create a new user share.
#[must_use]
pub fn new_user_share(
owner_id: UserId,
target: ShareTarget,
@ -251,16 +299,19 @@ impl Share {
}
/// Checks if the share has expired.
#[must_use]
pub fn is_expired(&self) -> bool {
self.expires_at.map(|exp| exp < Utc::now()).unwrap_or(false)
self.expires_at.is_some_and(|exp| exp < Utc::now())
}
/// Checks if this is a public link share.
pub fn is_public(&self) -> bool {
#[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),
@ -308,7 +359,7 @@ impl std::str::FromStr for ShareActivityAction {
"revoked" => Ok(Self::Revoked),
"expired" => Ok(Self::Expired),
"password_failed" => Ok(Self::PasswordFailed),
_ => Err(format!("unknown share activity action: {}", s)),
_ => Err(format!("unknown share activity action: {s}")),
}
}
}
@ -327,6 +378,7 @@ pub struct ShareActivity {
impl ShareActivity {
/// Creates a new share activity entry.
#[must_use]
pub fn new(share_id: ShareId, action: ShareActivityAction) -> Self {
Self {
id: Uuid::now_v7(),
@ -340,18 +392,21 @@ impl ShareActivity {
}
/// Sets the actor who performed the activity.
pub fn with_actor(mut self, actor_id: UserId) -> Self {
#[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
@ -391,7 +446,7 @@ impl std::str::FromStr for ShareNotificationType {
"share_revoked" => Ok(Self::ShareRevoked),
"share_expiring" => Ok(Self::ShareExpiring),
"share_accessed" => Ok(Self::ShareAccessed),
_ => Err(format!("unknown share notification type: {}", s)),
_ => Err(format!("unknown share notification type: {s}")),
}
}
}
@ -409,6 +464,7 @@ pub struct ShareNotification {
impl ShareNotification {
/// Creates a new share notification.
#[must_use]
pub fn new(
user_id: UserId,
share_id: ShareId,
@ -426,17 +482,23 @@ impl ShareNotification {
}
/// 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<String, PinakesError> {
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)
}