feat(trash): move trash API to normalized version (with cursor, orderBy) + normalize Trash section to existing components
normalize also component to format badges (expiry, role, etc)
This commit is contained in:
@@ -1,20 +1,28 @@
|
||||
use axum::Json;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use serde_json::json;
|
||||
use tracing::{debug, error, instrument, warn};
|
||||
|
||||
use crate::application::dtos::trash_dto::{TrashResourcesDto, TrashResourcesQuery};
|
||||
use crate::application::ports::trash_ports::TrashUseCase;
|
||||
use crate::common::di::AppState;
|
||||
use crate::interfaces::errors::AppError;
|
||||
use crate::interfaces::middleware::auth::AuthUser;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Gets all items in the trash for the current user
|
||||
/// Gets all items in the trash for the current user.
|
||||
///
|
||||
/// # Deprecated
|
||||
/// Use `GET /api/trash/resources` instead. This endpoint is kept for
|
||||
/// backwards compatibility but will be removed in a future release.
|
||||
#[deprecated = "Use GET /api/trash/resources instead"]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/trash",
|
||||
responses(
|
||||
(status = 200, description = "List of trashed items"),
|
||||
(status = 200, description = "List of trashed items (deprecated — use /api/trash/resources)"),
|
||||
(status = 501, description = "Trash feature not enabled")
|
||||
),
|
||||
security(("bearerAuth" = [])),
|
||||
@@ -30,6 +38,10 @@ pub async fn get_trash_items(
|
||||
// privilege escalation attacks.
|
||||
let effective_user = auth_user.id;
|
||||
|
||||
warn!(
|
||||
"Deprecated endpoint called: GET /api/trash — use GET /api/trash/resources instead (user {effective_user})"
|
||||
);
|
||||
|
||||
debug!("Request to list trash items for user {}", effective_user);
|
||||
|
||||
let trash_service = match state.trash_service.as_ref() {
|
||||
@@ -63,6 +75,74 @@ pub async fn get_trash_items(
|
||||
}
|
||||
}
|
||||
|
||||
/// Cursor-paginated list of a user's trashed resources.
|
||||
///
|
||||
/// Sorts by `deletion_date` (default — soonest expiry first), `trashed_at`
|
||||
/// (most recently trashed first), `name`, `type`, or `size`. Filter on
|
||||
/// `resource_types=file` or `resource_types=folder` to narrow to one kind.
|
||||
/// Items implicitly trashed as descendants of a trashed parent are excluded
|
||||
/// (only top-level trashed items appear).
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/trash/resources",
|
||||
params(TrashResourcesQuery),
|
||||
responses(
|
||||
(status = 200, description = "Paginated list of trashed resources",
|
||||
body = crate::application::dtos::trash_dto::TrashResourcesDto),
|
||||
(status = 400, description = "Invalid cursor or query parameters"),
|
||||
(status = 501, description = "Trash feature not enabled"),
|
||||
),
|
||||
security(("bearerAuth" = [])),
|
||||
tag = "trash"
|
||||
)]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_trash_resources(
|
||||
State(state): State<Arc<AppState>>,
|
||||
auth_user: AuthUser,
|
||||
Query(q): Query<TrashResourcesQuery>,
|
||||
) -> axum::response::Response {
|
||||
let user_id = auth_user.id;
|
||||
|
||||
let trash_service = match state.trash_service.as_ref() {
|
||||
Some(service) => service,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::NOT_IMPLEMENTED,
|
||||
Json(json!({ "error": "Trash feature is not enabled" })),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let order_by = q.order_by.as_deref().unwrap_or("deletion_date").to_owned();
|
||||
|
||||
// Discard cursor if sort dimension or direction changed between pages.
|
||||
let cursor = q
|
||||
.decode_cursor()
|
||||
.filter(|c| c.order_by == order_by && c.reverse == q.reverse);
|
||||
|
||||
let kinds = q.resource_kinds();
|
||||
|
||||
match trash_service
|
||||
.list_resources_paged(
|
||||
user_id,
|
||||
q.limit_clamped(),
|
||||
cursor,
|
||||
&order_by,
|
||||
kinds.as_deref(),
|
||||
q.reverse,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((items, next_cursor)) => (
|
||||
StatusCode::OK,
|
||||
Json(TrashResourcesDto::with_cursor(items, next_cursor)),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => AppError::from(e).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves a file to the trash
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
|
||||
@@ -36,7 +36,8 @@ use crate::application::dtos::search_dto::{
|
||||
};
|
||||
use crate::application::dtos::share_dto::{CreateShareDto, ShareDto, UpdateShareDto};
|
||||
use crate::application::dtos::trash_dto::{
|
||||
DeletePermanentlyRequest, MoveToTrashRequest, RestoreFromTrashRequest, TrashedItemDto,
|
||||
DeletePermanentlyRequest, MoveToTrashRequest, RestoreFromTrashRequest, TrashResourceItemDto,
|
||||
TrashResourcesDto, TrashedItemDto,
|
||||
};
|
||||
use crate::application::dtos::user_dto::{
|
||||
AuthResponseDto, ChangePasswordDto, LoginDto, OidcExchangeDto, OidcProviderInfoDto,
|
||||
@@ -121,6 +122,7 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload;
|
||||
handlers::dedup_handler::recalculate_stats,
|
||||
// Trash handlers (free functions)
|
||||
handlers::trash_handler::get_trash_items,
|
||||
handlers::trash_handler::get_trash_resources,
|
||||
handlers::trash_handler::move_file_to_trash,
|
||||
handlers::trash_handler::move_folder_to_trash,
|
||||
handlers::trash_handler::restore_from_trash,
|
||||
@@ -259,6 +261,8 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload;
|
||||
UpdateShareDto,
|
||||
// Trash schemas
|
||||
TrashedItemDto,
|
||||
TrashResourceItemDto,
|
||||
TrashResourcesDto,
|
||||
MoveToTrashRequest,
|
||||
RestoreFromTrashRequest,
|
||||
DeletePermanentlyRequest,
|
||||
|
||||
@@ -431,13 +431,17 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
if let Some(_trash_service_ref) = trash_service.clone() {
|
||||
tracing::info!("Setting up trash routes for trash view");
|
||||
|
||||
#[allow(deprecated)]
|
||||
let trash_router = Router::new()
|
||||
.route("/", get(trash_handler::get_trash_items))
|
||||
// Literal paths first — order matters for axum overlap handling
|
||||
// when a wildcard like /{id} could otherwise capture them.
|
||||
.route("/", get(trash_handler::get_trash_items)) // deprecated — kept for external compat
|
||||
.route("/resources", get(trash_handler::get_trash_resources))
|
||||
.route("/empty", delete(trash_handler::empty_trash))
|
||||
.route("/files/{id}", delete(trash_handler::move_file_to_trash))
|
||||
.route("/folders/{id}", delete(trash_handler::move_folder_to_trash))
|
||||
.route("/{id}/restore", post(trash_handler::restore_from_trash))
|
||||
.route("/{id}", delete(trash_handler::delete_permanently))
|
||||
.route("/empty", delete(trash_handler::empty_trash))
|
||||
.with_state(app_state.clone());
|
||||
|
||||
router = router.nest("/trash", trash_router);
|
||||
|
||||
Reference in New Issue
Block a user