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
+14
View File
@@ -8,6 +8,7 @@ pub use super::entity_errors::ShareError;
pub struct Share {
id: String,
item_id: String,
item_name: Option<String>,
item_type: ShareItemType,
token: String,
password_hash: Option<String>,
@@ -34,6 +35,7 @@ pub enum ShareItemType {
impl Share {
pub fn new(
item_id: String,
item_name: Option<String>,
item_type: ShareItemType,
created_by: String,
permissions: Option<SharePermissions>,
@@ -69,6 +71,7 @@ impl Share {
Ok(Self {
id: Uuid::new_v4().to_string(),
item_id,
item_name,
item_type,
token: Uuid::new_v4().to_string(),
password_hash,
@@ -88,6 +91,7 @@ impl Share {
pub fn from_raw(
id: String,
item_id: String,
item_name: Option<String>,
item_type: ShareItemType,
token: String,
password_hash: Option<String>,
@@ -100,6 +104,7 @@ impl Share {
Self {
id,
item_id,
item_name,
item_type,
token,
password_hash,
@@ -121,6 +126,10 @@ impl Share {
&self.item_id
}
pub fn item_name(&self) -> Option<&str> {
self.item_name.as_deref()
}
pub fn item_type(&self) -> &ShareItemType {
&self.item_type
}
@@ -257,6 +266,7 @@ mod tests {
fn test_create_share() {
let share = Share::new(
"test_file_id".to_string(),
None,
ShareItemType::File,
"user123".to_string(),
None,
@@ -287,6 +297,7 @@ mod tests {
let future = now + 3600; // 1 hour in the future
let share = Share::new(
"test_file_id".to_string(),
None,
ShareItemType::File,
"user123".to_string(),
None,
@@ -301,6 +312,7 @@ mod tests {
let past = now - 3600; // 1 hour in the past
let share_result = Share::new(
"test_file_id".to_string(),
None,
ShareItemType::File,
"user123".to_string(),
None,
@@ -335,6 +347,7 @@ mod tests {
fn test_has_password_with_hash() {
let share = Share::new(
"test_file_id".to_string(),
None,
ShareItemType::File,
"user123".to_string(),
None,
@@ -351,6 +364,7 @@ mod tests {
fn test_has_password_without_hash() {
let share = Share::new(
"test_file_id".to_string(),
None,
ShareItemType::File,
"user123".to_string(),
None,