diff --git a/docs/architecture/resource-listing.md b/docs/architecture/resource-listing.md index 7aaf7497..3933d31d 100644 --- a/docs/architecture/resource-listing.md +++ b/docs/architecture/resource-listing.md @@ -301,17 +301,17 @@ avoids a breaking API change when a second sort is added later. | `GET /api/photos` | ⚠️ `before` header | ❌ | Non-standard — migrate to body cursor | | `GET /api/search` | ❌ offset | ✅ | Migrate cursor | | `GET /api/folders/paginated` | ❌ page | ❌ | Migrate cursor | -| `GET /api/folders/{id}/contents/paginated` | ❌ page | ❌ | Migrate cursor | +| `GET /api/folders/{id}/contents/paginated` | — | — | ✅ **Removed** — use `/api/folders/{id}/resources` | | `GET /api/admin/users` | ❌ offset | ❌ | Migrate cursor | | `GET /api/address-books/{id}/contacts` | ❌ offset | ❌ | Migrate cursor | | `GET /api/shares` | ❌ page | ❌ | Migrate cursor | | `GET /api/playlists` | ❌ offset | ❌ | Migrate cursor | -| `GET /api/recent` | ❌ limit only | ❌ | Migrate cursor | +| `GET /api/recent` | — | — | ✅ **Removed** — use `/api/recent/resources` | | `GET /api/files` | ❌ **none** | ❌ | Unbounded — **urgent** | | `GET /api/folders` | ❌ **none** | ❌ | Unbounded — **urgent** | | `GET /api/folders/{id}/listing` | ❌ **none** | ❌ | Unbounded — **urgent** | -| `GET /api/favorites` | ❌ **none** | ❌ | Unbounded — **urgent** | -| `GET /api/trash` | ❌ **none** | ❌ | Unbounded — **urgent** | +| `GET /api/favorites` | — | — | ✅ **Removed** — use `/api/favorites/resources` | +| `GET /api/trash` | — | — | ✅ **Removed** — use `/api/trash/resources` | | `GET /api/grants/incoming` | ❌ **none** | ❌ | Unbounded | | `GET /api/grants/outgoing` | ❌ **none** | ❌ | Unbounded | diff --git a/docs/plan/benchmake-and-performance-tracking.md b/docs/plan/benchmake-and-performance-tracking.md index 2038a2c5..46f40f17 100644 --- a/docs/plan/benchmake-and-performance-tracking.md +++ b/docs/plan/benchmake-and-performance-tracking.md @@ -71,7 +71,7 @@ All scenarios load `baseline.json` at startup and set `thresholds` dynamically f **Endpoint contracts (confirmed by exploration):** - Login: `POST /api/auth/login` body `{username, password}` → `{access_token, ...}`. -- Folder ops: `POST /api/folders`, `GET /api/folders/{id}/contents` (or `/contents/paginated` at high depth), `PUT /api/folders/{id}/move`, `POST /api/batch/folders/copy`, `DELETE /api/folders/{id}`. Handlers in `src/interfaces/api/handlers/folder_handler.rs` and `batch_handler.rs`. +- Folder ops: `POST /api/folders`, `GET /api/folders/{id}/resources` (cursor-paginated; replaced the removed `/contents` + `/contents/paginated`), `PUT /api/folders/{id}/move`, `POST /api/batch/folders/copy`, `DELETE /api/folders/{id}`. Handlers in `src/interfaces/api/handlers/folder_handler.rs` and `batch_handler.rs`. - Grants: `POST /api/grants` (subject `{type, id|email}`, resource `{type, id}`, `role` or `permissions[]`), `GET /api/grants?resource_type=folder&resource_id=…`. Handler `src/interfaces/api/handlers/grant_handler.rs`. - Groups: `POST /api/groups` (admin), `POST /api/groups/{id}/members` body `{user_id}` or `{group_id}`. Handler `src/interfaces/api/handlers/subject_group_handler.rs`. Nesting limit is 8 — `--group-depth 3` is well inside. - Files: multipart `POST /api/files/upload` with fields `folder_id` + `file`. diff --git a/src/interfaces/api/handlers/favorites_handler.rs b/src/interfaces/api/handlers/favorites_handler.rs index 08450a96..0a4ebfec 100644 --- a/src/interfaces/api/handlers/favorites_handler.rs +++ b/src/interfaces/api/handlers/favorites_handler.rs @@ -6,7 +6,7 @@ use axum::{ }; use serde::Deserialize; use std::sync::Arc; -use tracing::{error, info, warn}; +use tracing::{error, info}; use utoipa::ToSchema; use crate::application::dtos::display_helpers::{ @@ -37,52 +37,6 @@ pub struct BatchFavoritesRequest { pub items: Vec, } -/// Handler for favorite-related API endpoints -/// -/// # Deprecated -/// Use `GET /api/favorites/resources` instead. This endpoint is kept for -/// backwards compatibility but will be removed in a future release. -#[deprecated = "Use GET /api/favorites/resources instead"] -#[utoipa::path( - get, - path = "/api/favorites", - responses( - (status = 200, description = "List of favorites (deprecated — use /api/favorites/resources)", body = Vec) - ), - security(("bearerAuth" = [])), - tag = "favorites" -)] -pub async fn get_favorites( - State(favorites_service): State>, - auth_user: AuthUser, -) -> impl IntoResponse { - let user_id = auth_user.id; - warn!( - "Deprecated endpoint called: GET /api/favorites — use GET /api/favorites/resources instead" - ); - - match favorites_service.get_favorites(user_id).await { - Ok(favorites) => { - info!( - "Retrieved {} favorites for user {}", - favorites.len(), - auth_user.id - ); - (StatusCode::OK, Json(serde_json::json!(favorites))).into_response() - } - Err(err) => { - error!("Error retrieving favorites: {}", err); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "error": "Failed to retrieve favorites" - })), - ) - .into_response() - } - } -} - /// Add an item to user's favorites #[utoipa::path( post, diff --git a/src/interfaces/api/handlers/folder_handler.rs b/src/interfaces/api/handlers/folder_handler.rs index b868ae16..0ffc3e6a 100644 --- a/src/interfaces/api/handlers/folder_handler.rs +++ b/src/interfaces/api/handlers/folder_handler.rs @@ -111,16 +111,6 @@ impl FolderHandler { Self::list_folders_scoped(service, None, &auth_user).await } - /// Lists contents of a specific folder by its ID. - /// Scoped to the authenticated user's folders. - pub(super) async fn list_folder_contents_impl( - State(service): State, - auth_user: AuthUser, - Path(id): Path, - ) -> axum::response::Response { - Self::list_folders_scoped(service, Some(&id), &auth_user).await - } - /// Lists root folders with pagination. /// Scoped to the authenticated user — only returns folders owned by this user. pub(super) async fn list_root_folders_paginated_impl( @@ -131,22 +121,6 @@ impl FolderHandler { Self::list_folders_scoped(service, None, &auth_user).await } - /// Lists sub-folders inside a folder with pagination. - pub(super) async fn list_folder_contents_paginated_impl( - State(service): State, - auth_user: AuthUser, - Path(id): Path, - pagination: Query, - ) -> axum::response::Response { - match service - .list_folders_paginated_with_perms(Some(&id), auth_user.id, &pagination) - .await - { - Ok(paginated_result) => (StatusCode::OK, Json(paginated_result)).into_response(), - Err(err) => AppError::from(err).into_response(), - } - } - /// Internal helper: lists folders scoped to the authenticated user. /// Uses `list_folders_for_owner` — the DB query filters by `user_id`, /// so no data from other users ever leaves the database. @@ -525,30 +499,6 @@ pub async fn list_root_folders( FolderHandler::list_root_folders_impl(state, auth_user).await } -#[deprecated = "Use /api/folders/{id}/resources instead"] -#[utoipa::path( - get, - path = "/api/folders/{id}/contents", - params(("id" = String, Path, description = "Folder ID")), - responses( - (status = 200, description = "List of sub-folders", body = Vec), - (status = 404, description = "Folder not found"), - ), - security(("bearerAuth" = [])), - tag = "folders" -)] -#[allow(deprecated)] -pub async fn list_folder_contents( - state: State, - auth_user: AuthUser, - path: Path, -) -> axum::response::Response { - tracing::warn!( - "Deprecated endpoint called: GET /api/folders/{{id}}/contents — use GET /api/folders/{{id}}/resources?resource_types=folder instead" - ); - FolderHandler::list_folder_contents_impl(state, auth_user, path).await -} - #[utoipa::path( get, path = "/api/folders/paginated", @@ -567,34 +517,6 @@ pub async fn list_root_folders_paginated( FolderHandler::list_root_folders_paginated_impl(state, auth_user, pagination).await } -#[deprecated = "Use /api/folders/{id}/resources instead"] -#[utoipa::path( - get, - path = "/api/folders/{id}/contents/paginated", - params( - ("id" = String, Path, description = "Folder ID"), - PaginationRequestDto, - ), - responses( - (status = 200, description = "Paginated list of sub-folders"), - (status = 404, description = "Folder not found"), - ), - security(("bearerAuth" = [])), - tag = "folders" -)] -#[allow(deprecated)] -pub async fn list_folder_contents_paginated( - state: State, - auth_user: AuthUser, - path: Path, - pagination: Query, -) -> axum::response::Response { - tracing::warn!( - "Deprecated endpoint called: GET /api/folders/{{id}}/contents/paginated — use GET /api/folders/{{id}}/resources instead" - ); - FolderHandler::list_folder_contents_paginated_impl(state, auth_user, path, pagination).await -} - #[deprecated = "Use /api/folders/{id}/resources instead"] #[utoipa::path( get, diff --git a/src/interfaces/api/handlers/recent_handler.rs b/src/interfaces/api/handlers/recent_handler.rs index eb313b40..592fdbc6 100644 --- a/src/interfaces/api/handlers/recent_handler.rs +++ b/src/interfaces/api/handlers/recent_handler.rs @@ -4,9 +4,8 @@ use axum::{ http::StatusCode, response::IntoResponse, }; -use serde::Deserialize; use std::sync::Arc; -use tracing::{error, info, warn}; +use tracing::{error, info}; use crate::application::dtos::display_helpers::{ category_for, format_file_size, icon_class_for, icon_special_class_for, @@ -24,50 +23,6 @@ use crate::interfaces::errors::AppError; use crate::interfaces::middleware::auth::AuthUser; use uuid::Uuid; -/// Query parameters for getting recent items -#[derive(Deserialize)] -pub struct GetRecentParams { - #[serde(default)] - limit: Option, -} - -/// Get user's recent items (deprecated — use `GET /api/recent/resources` instead) -#[deprecated = "Use GET /api/recent/resources instead"] -#[utoipa::path( - get, - path = "/api/recent", - responses( - (status = 200, description = "List of recent items", body = Vec) - ), - security(("bearerAuth" = [])), - tag = "recent" -)] -pub async fn get_recent_items( - State(recent_service): State>, - auth_user: AuthUser, - Query(params): Query, -) -> impl IntoResponse { - let user_id = auth_user.id; - warn!("Deprecated endpoint called: GET /api/recent — use GET /api/recent/resources instead"); - - match recent_service.get_recent_items(user_id, params.limit).await { - Ok(items) => { - info!("Retrieved {} recent items for user", items.len()); - (StatusCode::OK, Json(items)).into_response() - } - Err(err) => { - error!("Error retrieving recent items: {}", err); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "error": "Failed to retrieve recent items" - })), - ) - .into_response() - } - } -} - /// Record access to an item #[utoipa::path( post, diff --git a/src/interfaces/api/handlers/trash_handler.rs b/src/interfaces/api/handlers/trash_handler.rs index 52bf3b9d..eaa899e3 100644 --- a/src/interfaces/api/handlers/trash_handler.rs +++ b/src/interfaces/api/handlers/trash_handler.rs @@ -12,69 +12,6 @@ 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. -/// -/// # 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 (deprecated — use /api/trash/resources)"), - (status = 501, description = "Trash feature not enabled") - ), - security(("bearerAuth" = [])), - tag = "trash" -)] -#[instrument(skip_all)] -pub async fn get_trash_items( - State(state): State>, - auth_user: AuthUser, -) -> (StatusCode, Json) { - // SECURITY: Always use the authenticated user's ID from the JWT token. - // Never allow user ID override via query parameters to prevent - // 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() { - Some(service) => service, - None => { - return ( - StatusCode::NOT_IMPLEMENTED, - Json(json!({ - "error": "Trash feature is not enabled" - })), - ); - } - }; - - let result = trash_service.get_trash_items(effective_user).await; - - match result { - Ok(items) => { - debug!("Found {} items in trash", items.len()); - (StatusCode::OK, Json(json!(items))) - } - Err(e) => { - error!("Error retrieving trash items: {:?}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(json!({ - "error": "Error retrieving trash items" - })), - ) - } - } -} - /// Cursor-paginated list of a user's trashed resources. /// /// Sorts by `deletion_date` (default — soonest expiry first), `trashed_at` diff --git a/src/interfaces/api/mod.rs b/src/interfaces/api/mod.rs index 047beab6..afabd464 100644 --- a/src/interfaces/api/mod.rs +++ b/src/interfaces/api/mod.rs @@ -98,9 +98,7 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload; handlers::folder_handler::create_folder, handlers::folder_handler::get_folder, handlers::folder_handler::list_root_folders, - handlers::folder_handler::list_folder_contents, handlers::folder_handler::list_root_folders_paginated, - handlers::folder_handler::list_folder_contents_paginated, handlers::folder_handler::list_folder_resources, handlers::folder_handler::list_folder_listing, handlers::folder_handler::rename_folder, @@ -130,7 +128,6 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload; handlers::dedup_handler::get_blob, 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, @@ -152,13 +149,11 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload; handlers::share_handler::download_share_zip_root, handlers::share_handler::download_share_zip_subfolder, // Favorites handlers (free functions) - handlers::favorites_handler::get_favorites, handlers::favorites_handler::list_favorites_resources, handlers::favorites_handler::add_favorite, handlers::favorites_handler::remove_favorite, handlers::favorites_handler::batch_add_favorites, // Recent handlers (free functions) - handlers::recent_handler::get_recent_items, handlers::recent_handler::list_recent_resources, handlers::recent_handler::record_item_access, handlers::recent_handler::remove_from_recent, @@ -433,18 +428,23 @@ mod tests { "expected at least 10 paths, got {}", paths.paths.len() ); - assert!(paths.paths.contains_key("/api/trash"), "missing /api/trash"); + // The old grouped list endpoints (/api/trash, /api/favorites, /api/recent) + // were removed in favour of the normalized cursor-paginated /resources API. + assert!( + paths.paths.contains_key("/api/trash/resources"), + "missing /api/trash/resources" + ); assert!( paths.paths.contains_key("/api/shares"), "missing /api/shares" ); assert!( - paths.paths.contains_key("/api/favorites"), - "missing /api/favorites" + paths.paths.contains_key("/api/favorites/resources"), + "missing /api/favorites/resources" ); assert!( - paths.paths.contains_key("/api/recent"), - "missing /api/recent" + paths.paths.contains_key("/api/recent/resources"), + "missing /api/recent/resources" ); let schemas = &spec diff --git a/src/interfaces/api/routes.rs b/src/interfaces/api/routes.rs index 6310709a..9da0b5d5 100644 --- a/src/interfaces/api/routes.rs +++ b/src/interfaces/api/routes.rs @@ -64,9 +64,9 @@ use crate::interfaces::api::handlers::file_handler::{ }; #[allow(deprecated)] use crate::interfaces::api::handlers::folder_handler::{ - create_folder, delete_folder_with_trash, download_folder_zip, get_folder, list_folder_contents, - list_folder_contents_paginated, list_folder_listing, list_folder_resources, list_root_folders, - list_root_folders_paginated, move_folder, rename_folder, + create_folder, delete_folder_with_trash, download_folder_zip, get_folder, list_folder_listing, + list_folder_resources, list_root_folders, list_root_folders_paginated, move_folder, + rename_folder, }; use crate::interfaces::api::handlers::i18n_handler::{ get_locales, get_translations_by_locale, translate, @@ -198,11 +198,6 @@ pub fn create_api_routes(app_state: &Arc) -> Router> { .route("/", get(list_root_folders)) .route("/paginated", get(list_root_folders_paginated)) .route("/{id}", get(get_folder)) - .route("/{id}/contents", get(list_folder_contents)) - .route( - "/{id}/contents/paginated", - get(list_folder_contents_paginated), - ) .route("/{id}/resources", get(list_folder_resources)) .route("/{id}/rename", put(rename_folder)) .route("/{id}/move", put(move_folder)) @@ -343,13 +338,9 @@ pub fn create_api_routes(app_state: &Arc) -> Router> { // Create a router without the i18n routes // Create routes for favorites if the service is available let favorites_router = if let Some(favorites_service) = favorites_service.clone() { - #[allow(deprecated)] - use crate::interfaces::api::handlers::favorites_handler::{ - self, get_favorites, list_favorites_resources, - }; + use crate::interfaces::api::handlers::favorites_handler::{self, list_favorites_resources}; Router::new() - .route("/", get(get_favorites)) // deprecated — kept for external compat .route("/resources", get(list_favorites_resources)) .route("/batch", post(favorites_handler::batch_add_favorites)) .route( @@ -367,11 +358,9 @@ pub fn create_api_routes(app_state: &Arc) -> Router> { // Create routes for recent items if the service is available let recent_router = if let Some(recent_service) = recent_service.clone() { - #[allow(deprecated)] use crate::interfaces::api::handlers::recent_handler; Router::new() - .route("/", get(recent_handler::get_recent_items)) // deprecated — kept for external compat .route("/resources", get(recent_handler::list_recent_resources)) .route( "/{item_type}/{item_id}", @@ -475,11 +464,9 @@ pub fn create_api_routes(app_state: &Arc) -> Router> { 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() // 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))