feat: add VisibilityFilteredAssetRepository decorator for automatic access control on asset queries
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod queries;
|
pub mod queries;
|
||||||
|
pub mod visibility;
|
||||||
|
|
||||||
pub use commands::register_asset::{RegisterAssetCommand, RegisterAssetHandler};
|
pub use commands::register_asset::{RegisterAssetCommand, RegisterAssetHandler};
|
||||||
pub use commands::update_metadata::{UpdateMetadataCommand, UpdateMetadataHandler};
|
pub use commands::update_metadata::{UpdateMetadataCommand, UpdateMetadataHandler};
|
||||||
pub use queries::get_asset::{GetAssetHandler, GetAssetQuery};
|
pub use queries::get_asset::{GetAssetHandler, GetAssetQuery};
|
||||||
pub use queries::get_timeline::{GetTimelineHandler, GetTimelineQuery};
|
pub use queries::get_timeline::{GetTimelineHandler, GetTimelineQuery};
|
||||||
pub use queries::read_asset_file::{AssetFileResult, ReadAssetFileHandler, ReadAssetFileQuery};
|
pub use queries::read_asset_file::{AssetFileResult, ReadAssetFileHandler, ReadAssetFileQuery};
|
||||||
|
pub use visibility::VisibilityFilteredAssetRepository;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
use crate::catalog::visibility::VisibilityFilteredAssetRepository;
|
||||||
use domain::{
|
use domain::{
|
||||||
catalog::entities::Asset,
|
catalog::entities::Asset,
|
||||||
catalog::services::resolve_metadata,
|
catalog::services::resolve_metadata,
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
ports::{AssetMetadataRepository, AssetRepository},
|
ports::{AssetMetadataRepository, AssetRepository, ShareRepository},
|
||||||
value_objects::{StructuredData, SystemId},
|
value_objects::{StructuredData, SystemId},
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -16,6 +17,7 @@ pub struct GetAssetQuery {
|
|||||||
pub struct GetAssetHandler {
|
pub struct GetAssetHandler {
|
||||||
asset_repo: Arc<dyn AssetRepository>,
|
asset_repo: Arc<dyn AssetRepository>,
|
||||||
metadata_repo: Arc<dyn AssetMetadataRepository>,
|
metadata_repo: Arc<dyn AssetMetadataRepository>,
|
||||||
|
share_repo: Option<Arc<dyn ShareRepository>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GetAssetHandler {
|
impl GetAssetHandler {
|
||||||
@@ -26,6 +28,28 @@ impl GetAssetHandler {
|
|||||||
Self {
|
Self {
|
||||||
asset_repo,
|
asset_repo,
|
||||||
metadata_repo,
|
metadata_repo,
|
||||||
|
share_repo: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable sharing-aware visibility filtering. When set, the handler
|
||||||
|
/// wraps the inner `AssetRepository` with a `VisibilityFilteredAssetRepository`
|
||||||
|
/// so that shared assets are visible to the caller.
|
||||||
|
pub fn with_visibility_filter(mut self, share_repo: Arc<dyn ShareRepository>) -> Self {
|
||||||
|
self.share_repo = Some(share_repo);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the effective asset repo — wrapped with a visibility filter
|
||||||
|
/// when a `ShareRepository` has been configured, otherwise the raw inner repo.
|
||||||
|
fn effective_repo(&self, caller_id: SystemId) -> Arc<dyn AssetRepository> {
|
||||||
|
match &self.share_repo {
|
||||||
|
Some(share_repo) => Arc::new(VisibilityFilteredAssetRepository::new(
|
||||||
|
self.asset_repo.clone(),
|
||||||
|
share_repo.clone(),
|
||||||
|
caller_id,
|
||||||
|
)),
|
||||||
|
None => self.asset_repo.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,13 +57,16 @@ impl GetAssetHandler {
|
|||||||
&self,
|
&self,
|
||||||
query: GetAssetQuery,
|
query: GetAssetQuery,
|
||||||
) -> Result<(Asset, StructuredData), DomainError> {
|
) -> Result<(Asset, StructuredData), DomainError> {
|
||||||
let asset = self
|
let repo = self.effective_repo(query.user_id);
|
||||||
.asset_repo
|
|
||||||
|
let asset = repo
|
||||||
.find_by_id(&query.asset_id)
|
.find_by_id(&query.asset_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| DomainError::NotFound(format!("Asset {} not found", query.asset_id)))?;
|
.ok_or_else(|| DomainError::NotFound(format!("Asset {} not found", query.asset_id)))?;
|
||||||
|
|
||||||
if asset.owner_user_id != query.user_id {
|
// When the visibility filter is active it already enforces access.
|
||||||
|
// When it is not, fall back to the original owner-only check.
|
||||||
|
if self.share_repo.is_none() && asset.owner_user_id != query.user_id {
|
||||||
return Err(DomainError::Forbidden("Access denied".to_string()));
|
return Err(DomainError::Forbidden("Access denied".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
use crate::catalog::visibility::VisibilityFilteredAssetRepository;
|
||||||
use domain::{
|
use domain::{
|
||||||
catalog::entities::Asset,
|
catalog::entities::Asset,
|
||||||
catalog::services::resolve_metadata,
|
catalog::services::resolve_metadata,
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
ports::{AssetMetadataRepository, AssetRepository},
|
ports::{AssetMetadataRepository, AssetRepository, ShareRepository},
|
||||||
value_objects::{StructuredData, SystemId},
|
value_objects::{StructuredData, SystemId},
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -10,6 +11,7 @@ use std::sync::Arc;
|
|||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct GetTimelineQuery {
|
pub struct GetTimelineQuery {
|
||||||
pub owner_id: SystemId,
|
pub owner_id: SystemId,
|
||||||
|
pub caller_id: Option<SystemId>,
|
||||||
pub limit: u32,
|
pub limit: u32,
|
||||||
pub offset: u32,
|
pub offset: u32,
|
||||||
}
|
}
|
||||||
@@ -17,6 +19,7 @@ pub struct GetTimelineQuery {
|
|||||||
pub struct GetTimelineHandler {
|
pub struct GetTimelineHandler {
|
||||||
asset_repo: Arc<dyn AssetRepository>,
|
asset_repo: Arc<dyn AssetRepository>,
|
||||||
metadata_repo: Arc<dyn AssetMetadataRepository>,
|
metadata_repo: Arc<dyn AssetMetadataRepository>,
|
||||||
|
share_repo: Option<Arc<dyn ShareRepository>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GetTimelineHandler {
|
impl GetTimelineHandler {
|
||||||
@@ -27,6 +30,24 @@ impl GetTimelineHandler {
|
|||||||
Self {
|
Self {
|
||||||
asset_repo,
|
asset_repo,
|
||||||
metadata_repo,
|
metadata_repo,
|
||||||
|
share_repo: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable sharing-aware visibility filtering on timeline queries.
|
||||||
|
pub fn with_visibility_filter(mut self, share_repo: Arc<dyn ShareRepository>) -> Self {
|
||||||
|
self.share_repo = Some(share_repo);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn effective_repo(&self, caller_id: SystemId) -> Arc<dyn AssetRepository> {
|
||||||
|
match &self.share_repo {
|
||||||
|
Some(share_repo) => Arc::new(VisibilityFilteredAssetRepository::new(
|
||||||
|
self.asset_repo.clone(),
|
||||||
|
share_repo.clone(),
|
||||||
|
caller_id,
|
||||||
|
)),
|
||||||
|
None => self.asset_repo.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,8 +55,10 @@ impl GetTimelineHandler {
|
|||||||
&self,
|
&self,
|
||||||
query: GetTimelineQuery,
|
query: GetTimelineQuery,
|
||||||
) -> Result<Vec<(Asset, StructuredData)>, DomainError> {
|
) -> Result<Vec<(Asset, StructuredData)>, DomainError> {
|
||||||
let assets = self
|
let caller_id = query.caller_id.unwrap_or(query.owner_id);
|
||||||
.asset_repo
|
let repo = self.effective_repo(caller_id);
|
||||||
|
|
||||||
|
let assets = repo
|
||||||
.find_by_owner(&query.owner_id, query.limit, query.offset)
|
.find_by_owner(&query.owner_id, query.limit, query.offset)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
296
crates/application/src/catalog/visibility.rs
Normal file
296
crates/application/src/catalog/visibility.rs
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use domain::{
|
||||||
|
catalog::entities::Asset,
|
||||||
|
errors::DomainError,
|
||||||
|
ports::{AssetRepository, ShareRepository},
|
||||||
|
value_objects::{Checksum, SystemId},
|
||||||
|
};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Decorator that wraps an `AssetRepository` and filters query results
|
||||||
|
/// based on sharing permissions. The caller sees only assets they own
|
||||||
|
/// or have been granted access to via a `ShareScope` + `ShareTarget`.
|
||||||
|
///
|
||||||
|
/// Write operations (`save`, `delete`) pass through to the inner repository
|
||||||
|
/// unchanged — authorization for writes is handled at the use-case layer.
|
||||||
|
pub struct VisibilityFilteredAssetRepository {
|
||||||
|
inner: Arc<dyn AssetRepository>,
|
||||||
|
share_repo: Arc<dyn ShareRepository>,
|
||||||
|
caller_id: SystemId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VisibilityFilteredAssetRepository {
|
||||||
|
pub fn new(
|
||||||
|
inner: Arc<dyn AssetRepository>,
|
||||||
|
share_repo: Arc<dyn ShareRepository>,
|
||||||
|
caller_id: SystemId,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
share_repo,
|
||||||
|
caller_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the caller owns the asset or has been granted
|
||||||
|
/// access through a share scope that targets them.
|
||||||
|
async fn caller_can_access(&self, asset: &Asset) -> Result<bool, DomainError> {
|
||||||
|
if asset.owner_user_id == self.caller_id {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all share scopes that cover this asset
|
||||||
|
let scopes = self
|
||||||
|
.share_repo
|
||||||
|
.find_scopes_for_resource(&asset.asset_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if scopes.is_empty() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all share targets that name this caller
|
||||||
|
let caller_targets = self
|
||||||
|
.share_repo
|
||||||
|
.find_targets_for_user(&self.caller_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// The caller has access if any of their targets reference a scope
|
||||||
|
// that covers this asset.
|
||||||
|
for scope in &scopes {
|
||||||
|
if scope.is_expired() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if caller_targets.iter().any(|t| t.scope_id == scope.scope_id) {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AssetRepository for VisibilityFilteredAssetRepository {
|
||||||
|
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Asset>, DomainError> {
|
||||||
|
let asset = self.inner.find_by_id(id).await?;
|
||||||
|
match asset {
|
||||||
|
Some(a) if self.caller_can_access(&a).await? => Ok(Some(a)),
|
||||||
|
_ => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_checksum(&self, checksum: &Checksum) -> Result<Vec<Asset>, DomainError> {
|
||||||
|
let assets = self.inner.find_by_checksum(checksum).await?;
|
||||||
|
let mut visible = Vec::with_capacity(assets.len());
|
||||||
|
for asset in assets {
|
||||||
|
if self.caller_can_access(&asset).await? {
|
||||||
|
visible.push(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(visible)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_owner(
|
||||||
|
&self,
|
||||||
|
owner_id: &SystemId,
|
||||||
|
limit: u32,
|
||||||
|
offset: u32,
|
||||||
|
) -> Result<Vec<Asset>, DomainError> {
|
||||||
|
if owner_id == &self.caller_id {
|
||||||
|
// Querying own assets — no filtering needed.
|
||||||
|
return self.inner.find_by_owner(owner_id, limit, offset).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let assets = self.inner.find_by_owner(owner_id, limit, offset).await?;
|
||||||
|
let mut visible = Vec::with_capacity(assets.len());
|
||||||
|
for asset in assets {
|
||||||
|
if self.caller_can_access(&asset).await? {
|
||||||
|
visible.push(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(visible)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save(&self, asset: &Asset) -> Result<(), DomainError> {
|
||||||
|
self.inner.save(asset).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, id: &SystemId) -> Result<(), DomainError> {
|
||||||
|
self.inner.delete(id).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::testing::{InMemoryAssetRepository, InMemoryShareRepository};
|
||||||
|
use domain::{
|
||||||
|
catalog::entities::{AssetType, SourceReference},
|
||||||
|
sharing::entities::{ScopeType, ShareScope, ShareTarget, ShareableType, TargetType},
|
||||||
|
value_objects::{Checksum, SystemId},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn make_asset(owner: SystemId) -> Asset {
|
||||||
|
Asset::new(
|
||||||
|
SourceReference {
|
||||||
|
volume_id: SystemId::new(),
|
||||||
|
relative_path: "test/photo.jpg".to_string(),
|
||||||
|
checksum: Checksum::new("a".repeat(64)).unwrap(),
|
||||||
|
},
|
||||||
|
AssetType::Image,
|
||||||
|
"image/jpeg",
|
||||||
|
1024,
|
||||||
|
owner,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn share_asset(asset_id: SystemId, granter: SystemId) -> ShareScope {
|
||||||
|
ShareScope::new(ScopeType::User, ShareableType::Asset, asset_id, granter)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn target_user(scope_id: SystemId, user_id: SystemId) -> ShareTarget {
|
||||||
|
ShareTarget::new(scope_id, TargetType::User, user_id, SystemId::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn owner_can_always_see_own_asset() {
|
||||||
|
let owner_id = SystemId::new();
|
||||||
|
let asset = make_asset(owner_id);
|
||||||
|
|
||||||
|
let inner = Arc::new(InMemoryAssetRepository::new());
|
||||||
|
inner.save(&asset).await.unwrap();
|
||||||
|
|
||||||
|
let share_repo = Arc::new(InMemoryShareRepository::new());
|
||||||
|
let filtered = VisibilityFilteredAssetRepository::new(
|
||||||
|
inner.clone(),
|
||||||
|
share_repo.clone(),
|
||||||
|
owner_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
let found = filtered.find_by_id(&asset.asset_id).await.unwrap();
|
||||||
|
assert!(found.is_some());
|
||||||
|
assert_eq!(found.unwrap().asset_id, asset.asset_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn stranger_cannot_see_unshared_asset() {
|
||||||
|
let owner_id = SystemId::new();
|
||||||
|
let stranger_id = SystemId::new();
|
||||||
|
let asset = make_asset(owner_id);
|
||||||
|
|
||||||
|
let inner = Arc::new(InMemoryAssetRepository::new());
|
||||||
|
inner.save(&asset).await.unwrap();
|
||||||
|
|
||||||
|
let share_repo = Arc::new(InMemoryShareRepository::new());
|
||||||
|
let filtered = VisibilityFilteredAssetRepository::new(
|
||||||
|
inner.clone(),
|
||||||
|
share_repo.clone(),
|
||||||
|
stranger_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
let found = filtered.find_by_id(&asset.asset_id).await.unwrap();
|
||||||
|
assert!(found.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn shared_user_can_see_asset() {
|
||||||
|
let owner_id = SystemId::new();
|
||||||
|
let friend_id = SystemId::new();
|
||||||
|
let asset = make_asset(owner_id);
|
||||||
|
|
||||||
|
let inner = Arc::new(InMemoryAssetRepository::new());
|
||||||
|
inner.save(&asset).await.unwrap();
|
||||||
|
|
||||||
|
let share_repo = Arc::new(InMemoryShareRepository::new());
|
||||||
|
|
||||||
|
// Create a share scope on the asset and target the friend
|
||||||
|
let scope = share_asset(asset.asset_id, owner_id);
|
||||||
|
share_repo.save_scope(&scope).await.unwrap();
|
||||||
|
|
||||||
|
let target = target_user(scope.scope_id, friend_id);
|
||||||
|
share_repo.save_target(&target).await.unwrap();
|
||||||
|
|
||||||
|
let filtered = VisibilityFilteredAssetRepository::new(
|
||||||
|
inner.clone(),
|
||||||
|
share_repo.clone(),
|
||||||
|
friend_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
let found = filtered.find_by_id(&asset.asset_id).await.unwrap();
|
||||||
|
assert!(found.is_some());
|
||||||
|
assert_eq!(found.unwrap().asset_id, asset.asset_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn find_by_checksum_filters_inaccessible() {
|
||||||
|
let owner_id = SystemId::new();
|
||||||
|
let stranger_id = SystemId::new();
|
||||||
|
|
||||||
|
let asset_a = make_asset(owner_id);
|
||||||
|
let mut asset_b = make_asset(stranger_id);
|
||||||
|
// Give asset_b the same checksum as asset_a
|
||||||
|
asset_b.source_reference.checksum = asset_a.source_reference.checksum.clone();
|
||||||
|
|
||||||
|
let inner = Arc::new(InMemoryAssetRepository::new());
|
||||||
|
inner.save(&asset_a).await.unwrap();
|
||||||
|
inner.save(&asset_b).await.unwrap();
|
||||||
|
|
||||||
|
let share_repo = Arc::new(InMemoryShareRepository::new());
|
||||||
|
|
||||||
|
// Stranger queries by checksum — should only see their own
|
||||||
|
let filtered = VisibilityFilteredAssetRepository::new(
|
||||||
|
inner.clone(),
|
||||||
|
share_repo.clone(),
|
||||||
|
stranger_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
let results = filtered
|
||||||
|
.find_by_checksum(&asset_a.source_reference.checksum)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(results.len(), 1);
|
||||||
|
assert_eq!(results[0].owner_user_id, stranger_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn find_by_owner_skips_filter_for_own_assets() {
|
||||||
|
let owner_id = SystemId::new();
|
||||||
|
let asset = make_asset(owner_id);
|
||||||
|
|
||||||
|
let inner = Arc::new(InMemoryAssetRepository::new());
|
||||||
|
inner.save(&asset).await.unwrap();
|
||||||
|
|
||||||
|
let share_repo = Arc::new(InMemoryShareRepository::new());
|
||||||
|
let filtered = VisibilityFilteredAssetRepository::new(
|
||||||
|
inner.clone(),
|
||||||
|
share_repo.clone(),
|
||||||
|
owner_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
let results = filtered.find_by_owner(&owner_id, 10, 0).await.unwrap();
|
||||||
|
assert_eq!(results.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn find_by_owner_filters_others_assets() {
|
||||||
|
let owner_id = SystemId::new();
|
||||||
|
let stranger_id = SystemId::new();
|
||||||
|
let asset = make_asset(owner_id);
|
||||||
|
|
||||||
|
let inner = Arc::new(InMemoryAssetRepository::new());
|
||||||
|
inner.save(&asset).await.unwrap();
|
||||||
|
|
||||||
|
let share_repo = Arc::new(InMemoryShareRepository::new());
|
||||||
|
|
||||||
|
// Stranger queries owner's assets without a share — should get nothing
|
||||||
|
let filtered = VisibilityFilteredAssetRepository::new(
|
||||||
|
inner.clone(),
|
||||||
|
share_repo.clone(),
|
||||||
|
stranger_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
let results = filtered.find_by_owner(&owner_id, 10, 0).await.unwrap();
|
||||||
|
assert!(results.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ async fn returns_paginated_assets() {
|
|||||||
let page = handler
|
let page = handler
|
||||||
.execute(GetTimelineQuery {
|
.execute(GetTimelineQuery {
|
||||||
owner_id: owner,
|
owner_id: owner,
|
||||||
|
caller_id: None,
|
||||||
limit: 3,
|
limit: 3,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
})
|
})
|
||||||
@@ -50,6 +51,7 @@ async fn returns_empty_for_no_assets() {
|
|||||||
let page = handler
|
let page = handler
|
||||||
.execute(GetTimelineQuery {
|
.execute(GetTimelineQuery {
|
||||||
owner_id: SystemId::new(),
|
owner_id: SystemId::new(),
|
||||||
|
caller_id: None,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ pub async fn timeline(
|
|||||||
) -> Result<Json<TimelineResponse>, AppError> {
|
) -> Result<Json<TimelineResponse>, AppError> {
|
||||||
let query = GetTimelineQuery {
|
let query = GetTimelineQuery {
|
||||||
owner_id: claims.user_id,
|
owner_id: claims.user_id,
|
||||||
|
caller_id: None,
|
||||||
limit: params.limit.unwrap_or(DEFAULT_PAGE_SIZE).min(MAX_PAGE_SIZE),
|
limit: params.limit.unwrap_or(DEFAULT_PAGE_SIZE).min(MAX_PAGE_SIZE),
|
||||||
offset: params.offset.unwrap_or(0),
|
offset: params.offset.unwrap_or(0),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user