feat: folder ownership scoping, batch operations integration, frontend audit fixes

Backend:
- Add owner_id to Folder entity + FolderDto (DB user_id column)
- Add list_folders_by_owner to FolderRepository trait + PG impl
- Add list_folders_for_owner to FolderUseCase + FolderService
- Rewrite FolderHandler: all endpoints now scope by AuthUser
- Remove dead handler methods (list_folders_inner, list_folders_for_user, is_user_home_folder, folder_belongs_to_user)
- Add ownership check in get_folder (returns 404 on mismatch)

Batch operations:
- Add trash_service + zip_service to BatchOperationService
- New methods: trash_files, trash_folders, move_folders, download_zip
- New handlers: trash_batch, move_folders_batch, download_batch
- New routes: POST /api/batch/trash, /api/batch/folders/move, /api/batch/download

Frontend:
- Replace findUserHomeFolder (~130 lines) with resolveHomeFolder (~35 lines)
- Remove client-side folder filtering in loadFiles (backend now scopes)
- Rewrite batchDelete: N requests -> 1 POST /api/batch/trash
- Rewrite batchMove: N requests -> 2 POST max (files + folders)
- Rewrite batchDownload: N requests -> 1 POST /api/batch/download (ZIP)
- Search moved to backend, share system uses backend API
- Dark mode fixes, frontend audit improvements
This commit is contained in:
Dionisio
2026-02-15 23:45:11 +01:00
parent 6e1b77f244
commit 7737ed90c7
33 changed files with 3078 additions and 1958 deletions
+6
View File
@@ -40,6 +40,10 @@ pub struct FolderDto {
/// Parent folder ID
pub parent_id: Option<String>,
/// Owner user ID (scopes visibility per user)
#[serde(skip_serializing_if = "Option::is_none")]
pub owner_id: Option<String>,
/// Creation timestamp
pub created_at: u64,
@@ -59,6 +63,7 @@ impl From<Folder> for FolderDto {
name: folder.name().to_string(),
path: folder.path_string().to_string(),
parent_id: folder.parent_id().map(String::from),
owner_id: folder.owner_id().map(String::from),
created_at: folder.created_at(),
modified_at: folder.modified_at(),
is_root,
@@ -90,6 +95,7 @@ impl FolderDto {
name: "stub-folder".to_string(),
path: "/stub/path".to_string(),
parent_id: None,
owner_id: None,
created_at: 0,
modified_at: 0,
is_root: true,
+106 -7
View File
@@ -56,6 +56,10 @@ pub struct SearchCriteriaDto {
/// Offset for pagination
#[serde(default)]
pub offset: usize,
/// Sort order for results: "relevance", "name", "name_desc", "date", "date_desc", "size", "size_desc"
#[serde(default = "default_sort_by")]
pub sort_by: String,
}
/// Default value for recursive search (true)
@@ -68,6 +72,11 @@ fn default_limit() -> usize {
100
}
/// Default sort_by value
fn default_sort_by() -> String {
"relevance".to_string()
}
impl Default for SearchCriteriaDto {
fn default() -> Self {
Self {
@@ -83,23 +92,75 @@ impl Default for SearchCriteriaDto {
recursive: default_recursive(),
limit: default_limit(),
offset: 0,
sort_by: default_sort_by(),
}
}
}
/// A file search result enriched with server-computed metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchFileResultDto {
/// File ID
pub id: String,
/// File name
pub name: String,
/// Path to the file (relative)
pub path: String,
/// Size in bytes
pub size: u64,
/// MIME type
pub mime_type: String,
/// Parent folder ID
pub folder_id: Option<String>,
/// Creation timestamp
pub created_at: u64,
/// Last modification timestamp
pub modified_at: u64,
/// Relevance score (0-100) computed server-side
pub relevance_score: u32,
/// Human-readable file size (e.g., "2.5 MB")
pub size_formatted: String,
/// CSS icon class for the file type (e.g., "fas fa-file-pdf")
pub icon_class: String,
/// Content category: "document", "image", "video", "audio", "archive", "code", "other"
pub category: String,
}
/// A folder search result enriched with server-computed metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchFolderResultDto {
/// Folder ID
pub id: String,
/// Folder name
pub name: String,
/// Path to the folder (relative)
pub path: String,
/// Parent folder ID
pub parent_id: Option<String>,
/// Creation timestamp
pub created_at: u64,
/// Last modification timestamp
pub modified_at: u64,
/// Whether it is a root folder
pub is_root: bool,
/// Relevance score (0-100) computed server-side
pub relevance_score: u32,
}
/**
* Data Transfer Object for search results.
*
* This structure encapsulates the results of a search operation, including
* both files and folders that match the search criteria, along with pagination information.
* both files and folders that match the search criteria, along with pagination
* information and server-computed metadata.
*/
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResultsDto {
/// Files matching the search criteria
pub files: Vec<crate::application::dtos::file_dto::FileDto>,
/// Files matching the search criteria (enriched with metadata)
pub files: Vec<SearchFileResultDto>,
/// Folders matching the search criteria
pub folders: Vec<crate::application::dtos::folder_dto::FolderDto>,
/// Folders matching the search criteria (enriched with metadata)
pub folders: Vec<SearchFolderResultDto>,
/// Total count of matching items (for pagination)
pub total_count: Option<usize>,
@@ -112,6 +173,12 @@ pub struct SearchResultsDto {
/// Whether there are more results available
pub has_more: bool,
/// Query execution time in milliseconds (server-side)
pub query_time_ms: u64,
/// Sort order used
pub sort_by: String,
}
impl SearchResultsDto {
@@ -124,16 +191,20 @@ impl SearchResultsDto {
limit: 0,
offset: 0,
has_more: false,
query_time_ms: 0,
sort_by: "relevance".to_string(),
}
}
/// Creates a new search results object from files and folders
pub fn new(
files: Vec<crate::application::dtos::file_dto::FileDto>,
folders: Vec<crate::application::dtos::folder_dto::FolderDto>,
files: Vec<SearchFileResultDto>,
folders: Vec<SearchFolderResultDto>,
limit: usize,
offset: usize,
total_count: Option<usize>,
query_time_ms: u64,
sort_by: String,
) -> Self {
let has_more = match total_count {
Some(total) => (offset + files.len() + folders.len()) < total,
@@ -147,6 +218,34 @@ impl SearchResultsDto {
limit,
offset,
has_more,
query_time_ms,
sort_by,
}
}
}
/// DTO for search suggestion results (quick prefix search)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchSuggestionsDto {
/// Suggested file/folder names matching the query prefix
pub suggestions: Vec<SearchSuggestionItem>,
/// Query execution time in milliseconds
pub query_time_ms: u64,
}
/// Individual search suggestion item
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchSuggestionItem {
/// The suggested name
pub name: String,
/// Type: "file" or "folder"
pub item_type: String,
/// Item ID for navigation
pub id: String,
/// Path for context
pub path: String,
/// CSS icon class
pub icon_class: String,
/// Relevance score
pub relevance_score: u32,
}
+3
View File
@@ -6,6 +6,7 @@ use crate::domain::entities::share::{Share, SharePermissions};
pub struct ShareDto {
pub id: String,
pub item_id: String,
pub item_name: Option<String>,
pub item_type: String,
pub token: String,
pub url: String,
@@ -27,6 +28,7 @@ pub struct SharePermissionsDto {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateShareDto {
pub item_id: String,
pub item_name: Option<String>,
pub item_type: String,
pub password: Option<String>,
pub expires_at: Option<u64>,
@@ -48,6 +50,7 @@ impl ShareDto {
Self {
id: share.id().to_string(),
item_id: share.item_id().to_string(),
item_name: share.item_name().map(|s| s.to_string()),
item_type: share.item_type().to_string(),
token: share.token().to_string(),
url,