feat(grants): add /api/grants/incoming/resources with a cursor for pagination

This commit is contained in:
Edouard Vanbelle
2026-05-24 01:05:13 +02:00
parent 50d13943fc
commit 093c1ad3a5
7 changed files with 466 additions and 34 deletions
+52 -1
View File
@@ -5,9 +5,11 @@
//! storage-agnostic and DTOs can evolve with the HTTP contract.
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use utoipa::{IntoParams, ToSchema};
use uuid::Uuid;
use crate::application::dtos::file_dto::FileDto;
use crate::application::dtos::folder_dto::FolderDto;
use crate::domain::services::authorization::{Grant, Permission, Resource, Subject};
// ════════════════════════════════════════════════════════════════════════════
@@ -220,3 +222,52 @@ impl From<Grant> for GrantDto {
}
}
}
// ════════════════════════════════════════════════════════════════════════════
// Shared-with-me DTOs (GET /api/grants/incoming/resources)
// ════════════════════════════════════════════════════════════════════════════
/// Query parameters for `GET /api/grants/incoming/resources`.
#[derive(Debug, Deserialize, IntoParams)]
pub struct SharedWithMeQuery {
/// Maximum number of items to return (1–200, default 50).
#[serde(default = "shared_with_me_default_limit")]
pub limit: u32,
/// Comma-separated resource types to include, e.g. `file,folder`.
/// Omit to return all known types.
pub resource_types: Option<String>,
/// Opaque cursor returned by a previous call. Omit to start from the
/// most-recently-granted item.
pub cursor: Option<String>,
}
fn shared_with_me_default_limit() -> u32 {
50
}
/// One item in the shared-with-me list. Exactly one of `file` / `folder` is
/// populated, indicated by `resource_type`. Additional optional fields for
/// future resource types (playlist, addressbook, …) will be added here.
#[derive(Debug, Serialize, ToSchema)]
pub struct SharedWithMeItemDto {
pub resource_type: ResourceTypeDto,
/// All permissions the caller holds on this resource (aggregated).
pub permissions: Vec<PermissionDto>,
/// Earliest grant date for this resource.
pub granted_at: chrono::DateTime<chrono::Utc>,
/// UUID of the user who created the (earliest) grant.
pub granted_by: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<FileDto>,
#[serde(skip_serializing_if = "Option::is_none")]
pub folder: Option<FolderDto>,
}
/// Response for `GET /api/grants/incoming/resources`.
#[derive(Debug, Serialize, ToSchema)]
pub struct SharedWithMeDto {
pub items: Vec<SharedWithMeItemDto>,
/// Opaque cursor for the next page. Absent when the last page is reached.
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}
+21 -1
View File
@@ -11,7 +11,9 @@
use uuid::Uuid;
use crate::common::errors::DomainError;
use crate::domain::services::authorization::{Grant, Permission, Resource, Subject};
use crate::domain::services::authorization::{
Grant, GrantCursor, IncomingGrantSummary, Permission, Resource, ResourceKind, Subject,
};
pub trait AuthorizationEngine: Send + Sync + 'static {
/// Returns true if `subject` has `permission` on `resource`, considering
@@ -67,6 +69,24 @@ pub trait AuthorizationEngine: Send + Sync + 'static {
permission_filter: Option<Permission>,
) -> Result<Vec<Grant>, DomainError>;
/// Cursor-paginated list of resources explicitly granted to `subject`,
/// optionally filtered by resource kind. Multiple permission rows for the
/// same resource are collapsed into one `IncomingGrantSummary`.
///
/// Ordered by `MIN(granted_at) DESC, resource_id DESC` — stable across
/// concurrent inserts because the cursor encodes both fields.
///
/// Pass `kinds = &[]` to return all resource kinds.
/// Returns `(summaries, next_cursor)` — `next_cursor` is `None` when the
/// last page has been reached.
async fn list_incoming_resources_paged(
&self,
subject: Subject,
kinds: &[ResourceKind],
limit: u32,
cursor: Option<GrantCursor>,
) -> Result<(Vec<IncomingGrantSummary>, Option<GrantCursor>), DomainError>;
/// All grants on a specific resource (for "Manage sharing" UI). Caller
/// must verify the caller has `Share` on the resource before invoking.
async fn list_grants_on_resource(&self, resource: Resource) -> Result<Vec<Grant>, DomainError>;