pinakes-server: fix api key timing, notification scoping, and validate progress inputs
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ieb342b4b48034de0a2184cdf89d068316a6a6964
This commit is contained in:
parent
52f0b5defc
commit
2b2c1830a1
7 changed files with 334 additions and 179 deletions
|
|
@ -13,11 +13,13 @@ use pinakes_core::{
|
|||
ShareActivity,
|
||||
ShareActivityAction,
|
||||
ShareId,
|
||||
ShareMutatePermissions,
|
||||
ShareNotification,
|
||||
ShareNotificationType,
|
||||
SharePermissions,
|
||||
ShareRecipient,
|
||||
ShareTarget,
|
||||
ShareViewPermissions,
|
||||
generate_share_token,
|
||||
hash_share_password,
|
||||
verify_share_password,
|
||||
|
|
@ -37,6 +39,7 @@ use crate::{
|
|||
ShareActivityResponse,
|
||||
ShareNotificationResponse,
|
||||
ShareResponse,
|
||||
SharedContentResponse,
|
||||
UpdateShareRequest,
|
||||
},
|
||||
error::{ApiError, ApiResult},
|
||||
|
|
@ -51,14 +54,25 @@ pub async fn create_share(
|
|||
Json(req): Json<CreateShareRequest>,
|
||||
) -> ApiResult<Json<ShareResponse>> {
|
||||
let config = state.config.read().await;
|
||||
if !config.sharing.enabled {
|
||||
if !config.sharing.enabled() {
|
||||
return Err(ApiError::bad_request("Sharing is not enabled"));
|
||||
}
|
||||
|
||||
// Validate public links are allowed
|
||||
if req.recipient_type == "public_link" && !config.sharing.allow_public_links {
|
||||
if req.recipient_type == "public_link" && !config.sharing.allow_public_links()
|
||||
{
|
||||
return Err(ApiError::bad_request("Public links are not allowed"));
|
||||
}
|
||||
|
||||
// Enforce password requirement for public links if configured
|
||||
if req.recipient_type == "public_link"
|
||||
&& config.sharing.require_public_link_password
|
||||
&& req.password.is_none()
|
||||
{
|
||||
return Err(ApiError::bad_request(
|
||||
"Public links require a password per server policy",
|
||||
));
|
||||
}
|
||||
drop(config);
|
||||
|
||||
let owner_id = resolve_user_id(&state.storage, &username).await?;
|
||||
|
|
@ -124,12 +138,16 @@ pub async fn create_share(
|
|||
// Parse permissions
|
||||
let permissions = if let Some(perms) = req.permissions {
|
||||
SharePermissions {
|
||||
can_view: perms.can_view.unwrap_or(true),
|
||||
can_download: perms.can_download.unwrap_or(false),
|
||||
can_edit: perms.can_edit.unwrap_or(false),
|
||||
can_delete: perms.can_delete.unwrap_or(false),
|
||||
can_reshare: perms.can_reshare.unwrap_or(false),
|
||||
can_add: perms.can_add.unwrap_or(false),
|
||||
view: ShareViewPermissions {
|
||||
can_view: perms.can_view.unwrap_or(true),
|
||||
can_download: perms.can_download.unwrap_or(false),
|
||||
can_reshare: perms.can_reshare.unwrap_or(false),
|
||||
},
|
||||
mutate: ShareMutatePermissions {
|
||||
can_edit: perms.can_edit.unwrap_or(false),
|
||||
can_delete: perms.can_delete.unwrap_or(false),
|
||||
can_add: perms.can_add.unwrap_or(false),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
SharePermissions::view_only()
|
||||
|
|
@ -156,9 +174,10 @@ pub async fn create_share(
|
|||
updated_at: Utc::now(),
|
||||
};
|
||||
|
||||
let created = state.storage.create_share(&share).await.map_err(|e| {
|
||||
ApiError::internal(format!("Failed to create share: {}", e))
|
||||
})?;
|
||||
let created =
|
||||
state.storage.create_share(&share).await.map_err(|e| {
|
||||
ApiError::internal(format!("Failed to create share: {e}"))
|
||||
})?;
|
||||
|
||||
// Send notification to recipient if it's a user share
|
||||
if let ShareRecipient::User { user_id } = &created.recipient {
|
||||
|
|
@ -198,7 +217,7 @@ pub async fn list_outgoing(
|
|||
.storage
|
||||
.list_shares_by_owner(user_id, &pagination)
|
||||
.await
|
||||
.map_err(|e| ApiError::internal(format!("Failed to list shares: {}", e)))?;
|
||||
.map_err(|e| ApiError::internal(format!("Failed to list shares: {e}")))?;
|
||||
|
||||
Ok(Json(shares.into_iter().map(Into::into).collect()))
|
||||
}
|
||||
|
|
@ -221,7 +240,7 @@ pub async fn list_incoming(
|
|||
.storage
|
||||
.list_shares_for_user(user_id, &pagination)
|
||||
.await
|
||||
.map_err(|e| ApiError::internal(format!("Failed to list shares: {}", e)))?;
|
||||
.map_err(|e| ApiError::internal(format!("Failed to list shares: {e}")))?;
|
||||
|
||||
Ok(Json(shares.into_iter().map(Into::into).collect()))
|
||||
}
|
||||
|
|
@ -238,7 +257,7 @@ pub async fn get_share(
|
|||
.storage
|
||||
.get_share(ShareId(id))
|
||||
.await
|
||||
.map_err(|e| ApiError::not_found(format!("Share not found: {}", e)))?;
|
||||
.map_err(|e| ApiError::not_found(format!("Share not found: {e}")))?;
|
||||
|
||||
// Check authorization
|
||||
let is_owner = share.owner_id == user_id;
|
||||
|
|
@ -269,7 +288,7 @@ pub async fn update_share(
|
|||
.storage
|
||||
.get_share(ShareId(id))
|
||||
.await
|
||||
.map_err(|e| ApiError::not_found(format!("Share not found: {}", e)))?;
|
||||
.map_err(|e| ApiError::not_found(format!("Share not found: {e}")))?;
|
||||
|
||||
// Only owner can update
|
||||
if share.owner_id != user_id {
|
||||
|
|
@ -279,14 +298,22 @@ pub async fn update_share(
|
|||
// Update fields
|
||||
if let Some(perms) = req.permissions {
|
||||
share.permissions = SharePermissions {
|
||||
can_view: perms.can_view.unwrap_or(share.permissions.can_view),
|
||||
can_download: perms
|
||||
.can_download
|
||||
.unwrap_or(share.permissions.can_download),
|
||||
can_edit: perms.can_edit.unwrap_or(share.permissions.can_edit),
|
||||
can_delete: perms.can_delete.unwrap_or(share.permissions.can_delete),
|
||||
can_reshare: perms.can_reshare.unwrap_or(share.permissions.can_reshare),
|
||||
can_add: perms.can_add.unwrap_or(share.permissions.can_add),
|
||||
view: ShareViewPermissions {
|
||||
can_view: perms.can_view.unwrap_or(share.permissions.view.can_view),
|
||||
can_download: perms
|
||||
.can_download
|
||||
.unwrap_or(share.permissions.view.can_download),
|
||||
can_reshare: perms
|
||||
.can_reshare
|
||||
.unwrap_or(share.permissions.view.can_reshare),
|
||||
},
|
||||
mutate: ShareMutatePermissions {
|
||||
can_edit: perms.can_edit.unwrap_or(share.permissions.mutate.can_edit),
|
||||
can_delete: perms
|
||||
.can_delete
|
||||
.unwrap_or(share.permissions.mutate.can_delete),
|
||||
can_add: perms.can_add.unwrap_or(share.permissions.mutate.can_add),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -304,9 +331,10 @@ pub async fn update_share(
|
|||
|
||||
share.updated_at = Utc::now();
|
||||
|
||||
let updated = state.storage.update_share(&share).await.map_err(|e| {
|
||||
ApiError::internal(format!("Failed to update share: {}", e))
|
||||
})?;
|
||||
let updated =
|
||||
state.storage.update_share(&share).await.map_err(|e| {
|
||||
ApiError::internal(format!("Failed to update share: {e}"))
|
||||
})?;
|
||||
|
||||
// Notify recipient of update
|
||||
if let ShareRecipient::User { user_id } = &updated.recipient {
|
||||
|
|
@ -339,7 +367,7 @@ pub async fn delete_share(
|
|||
.storage
|
||||
.get_share(ShareId(id))
|
||||
.await
|
||||
.map_err(|e| ApiError::not_found(format!("Share not found: {}", e)))?;
|
||||
.map_err(|e| ApiError::not_found(format!("Share not found: {e}")))?;
|
||||
|
||||
// Only owner can delete
|
||||
if share.owner_id != user_id {
|
||||
|
|
@ -362,9 +390,11 @@ pub async fn delete_share(
|
|||
}
|
||||
}
|
||||
|
||||
state.storage.delete_share(ShareId(id)).await.map_err(|e| {
|
||||
ApiError::internal(format!("Failed to delete share: {}", e))
|
||||
})?;
|
||||
state
|
||||
.storage
|
||||
.delete_share(ShareId(id))
|
||||
.await
|
||||
.map_err(|e| ApiError::internal(format!("Failed to delete share: {e}")))?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
|
@ -386,7 +416,7 @@ pub async fn batch_delete(
|
|||
.storage
|
||||
.get_share(*share_id)
|
||||
.await
|
||||
.map_err(|e| ApiError::not_found(format!("Share not found: {}", e)))?;
|
||||
.map_err(|e| ApiError::not_found(format!("Share not found: {e}")))?;
|
||||
|
||||
if share.owner_id != user_id {
|
||||
return Err(ApiError::forbidden(format!(
|
||||
|
|
@ -400,9 +430,7 @@ pub async fn batch_delete(
|
|||
.storage
|
||||
.batch_delete_shares(&share_ids)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ApiError::internal(format!("Failed to batch delete: {}", e))
|
||||
})?;
|
||||
.map_err(|e| ApiError::internal(format!("Failed to batch delete: {e}")))?;
|
||||
|
||||
Ok(Json(serde_json::json!({ "deleted": deleted })))
|
||||
}
|
||||
|
|
@ -414,12 +442,12 @@ pub async fn access_shared(
|
|||
Path(token): Path<String>,
|
||||
Query(params): Query<AccessSharedRequest>,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
) -> ApiResult<Json<MediaResponse>> {
|
||||
) -> ApiResult<Json<SharedContentResponse>> {
|
||||
let share = state
|
||||
.storage
|
||||
.get_share_by_token(&token)
|
||||
.await
|
||||
.map_err(|e| ApiError::not_found(format!("Share not found: {}", e)))?;
|
||||
.map_err(|e| ApiError::not_found(format!("Share not found: {e}")))?;
|
||||
|
||||
// Check expiration
|
||||
if let Some(expires_at) = share.expires_at
|
||||
|
|
@ -463,9 +491,7 @@ pub async fn access_shared(
|
|||
.storage
|
||||
.record_share_access(share.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ApiError::internal(format!("Failed to record access: {}", e))
|
||||
})?;
|
||||
.map_err(|e| ApiError::internal(format!("Failed to record access: {e}")))?;
|
||||
|
||||
// Log the access
|
||||
let activity = ShareActivity {
|
||||
|
|
@ -482,17 +508,87 @@ pub async fn access_shared(
|
|||
// Return the shared content
|
||||
match &share.target {
|
||||
ShareTarget::Media { media_id } => {
|
||||
let item =
|
||||
state.storage.get_media(*media_id).await.map_err(|e| {
|
||||
ApiError::not_found(format!("Media not found: {}", e))
|
||||
let item = state
|
||||
.storage
|
||||
.get_media(*media_id)
|
||||
.await
|
||||
.map_err(|e| ApiError::not_found(format!("Media not found: {e}")))?;
|
||||
|
||||
Ok(Json(SharedContentResponse::Single(MediaResponse::from(
|
||||
item,
|
||||
))))
|
||||
},
|
||||
ShareTarget::Collection { collection_id } => {
|
||||
let members = state
|
||||
.storage
|
||||
.get_collection_members(*collection_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ApiError::not_found(format!("Collection not found: {e}"))
|
||||
})?;
|
||||
|
||||
Ok(Json(item.into()))
|
||||
let items: Vec<MediaResponse> =
|
||||
members.into_iter().map(MediaResponse::from).collect();
|
||||
|
||||
Ok(Json(SharedContentResponse::Multiple { items }))
|
||||
},
|
||||
_ => {
|
||||
Err(ApiError::bad_request(
|
||||
"Collection/tag sharing not yet fully implemented",
|
||||
))
|
||||
ShareTarget::Tag { tag_id } => {
|
||||
let tag = state
|
||||
.storage
|
||||
.get_tag(*tag_id)
|
||||
.await
|
||||
.map_err(|e| ApiError::not_found(format!("Tag not found: {e}")))?;
|
||||
|
||||
let request = pinakes_core::search::SearchRequest {
|
||||
query: pinakes_core::search::SearchQuery::TagFilter(
|
||||
tag.name.clone(),
|
||||
),
|
||||
sort: pinakes_core::search::SortOrder::default(),
|
||||
pagination: Pagination::new(0, 100, None),
|
||||
};
|
||||
|
||||
let results = state
|
||||
.storage
|
||||
.search(&request)
|
||||
.await
|
||||
.map_err(|e| ApiError::internal(format!("Search failed: {e}")))?;
|
||||
|
||||
let items: Vec<MediaResponse> =
|
||||
results.items.into_iter().map(MediaResponse::from).collect();
|
||||
|
||||
Ok(Json(SharedContentResponse::Multiple { items }))
|
||||
},
|
||||
ShareTarget::SavedSearch { search_id } => {
|
||||
let saved =
|
||||
state
|
||||
.storage
|
||||
.get_saved_search(*search_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ApiError::not_found(format!("Saved search not found: {e}"))
|
||||
})?;
|
||||
|
||||
let parsed_query = pinakes_core::search::parse_search_query(&saved.query)
|
||||
.map_err(|e| {
|
||||
ApiError::internal(format!("Failed to parse search query: {e}"))
|
||||
})?;
|
||||
|
||||
let request = pinakes_core::search::SearchRequest {
|
||||
query: parsed_query,
|
||||
sort: pinakes_core::search::SortOrder::default(),
|
||||
pagination: Pagination::new(0, 100, None),
|
||||
};
|
||||
|
||||
let results = state
|
||||
.storage
|
||||
.search(&request)
|
||||
.await
|
||||
.map_err(|e| ApiError::internal(format!("Search failed: {e}")))?;
|
||||
|
||||
let items: Vec<MediaResponse> =
|
||||
results.items.into_iter().map(MediaResponse::from).collect();
|
||||
|
||||
Ok(Json(SharedContentResponse::Multiple { items }))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -510,7 +606,7 @@ pub async fn get_activity(
|
|||
.storage
|
||||
.get_share(ShareId(id))
|
||||
.await
|
||||
.map_err(|e| ApiError::not_found(format!("Share not found: {}", e)))?;
|
||||
.map_err(|e| ApiError::not_found(format!("Share not found: {e}")))?;
|
||||
|
||||
// Only owner can view activity
|
||||
if share.owner_id != user_id {
|
||||
|
|
@ -529,9 +625,7 @@ pub async fn get_activity(
|
|||
.storage
|
||||
.get_share_activity(ShareId(id), &pagination)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ApiError::internal(format!("Failed to get activity: {}", e))
|
||||
})?;
|
||||
.map_err(|e| ApiError::internal(format!("Failed to get activity: {e}")))?;
|
||||
|
||||
Ok(Json(activity.into_iter().map(Into::into).collect()))
|
||||
}
|
||||
|
|
@ -548,7 +642,7 @@ pub async fn get_notifications(
|
|||
.get_unread_notifications(user_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ApiError::internal(format!("Failed to get notifications: {}", e))
|
||||
ApiError::internal(format!("Failed to get notifications: {e}"))
|
||||
})?;
|
||||
|
||||
Ok(Json(notifications.into_iter().map(Into::into).collect()))
|
||||
|
|
@ -558,16 +652,15 @@ pub async fn get_notifications(
|
|||
/// POST /api/notifications/shares/{id}/read
|
||||
pub async fn mark_notification_read(
|
||||
State(state): State<AppState>,
|
||||
Extension(_username): Extension<String>,
|
||||
Extension(username): Extension<String>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> ApiResult<StatusCode> {
|
||||
let user_id = resolve_user_id(&state.storage, &username).await?;
|
||||
state
|
||||
.storage
|
||||
.mark_notification_read(id)
|
||||
.mark_notification_read(id, user_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ApiError::internal(format!("Failed to mark as read: {}", e))
|
||||
})?;
|
||||
.map_err(|e| ApiError::internal(format!("Failed to mark as read: {e}")))?;
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
|
@ -584,7 +677,7 @@ pub async fn mark_all_read(
|
|||
.mark_all_notifications_read(user_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ApiError::internal(format!("Failed to mark all as read: {}", e))
|
||||
ApiError::internal(format!("Failed to mark all as read: {e}"))
|
||||
})?;
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue