diff --git a/Cargo.lock b/Cargo.lock index ada4a018..76996ad8 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -2527,7 +2527,7 @@ dependencies = [ [[package]] name = "oxicloud" -version = "0.5.0" +version = "0.5.2" dependencies = [ "argon2", "async-compression", diff --git a/build.rs b/build.rs index 0eddd050..553952c4 100644 --- a/build.rs +++ b/build.rs @@ -109,7 +109,7 @@ fn process_release(manifest_dir: &Path, static_dir: &Path, out_dir: &Path) { fs::write(dist_dir.join("css").join(&css_name), &css_bundle).expect("write css bundle"); // ── 4. Minify ALL individual CSS in static-dist/ ───────────────────────── - minify_tree_css(&dist_dir.join("css"), &css_name); + minify_tree_css(&dist_dir.join("css")); // ── 5. Build JS bundle for index.html ──────────────────────────────────── let index_html = fs::read_to_string(static_dir.join("index.html")).expect("read index.html"); @@ -120,11 +120,11 @@ fn process_release(manifest_dir: &Path, static_dir: &Path, out_dir: &Path) { fs::write(dist_dir.join("js").join(&js_name), &js_bundle).expect("write js bundle"); // ── 6. Minify ALL individual JS in static-dist/ ────────────────────────── - minify_tree_js(&dist_dir.join("js"), &js_name); + minify_tree_js(&dist_dir.join("js")); // ── 7. Inline theme-init.js & rewrite index.html ────────────────────── - let theme_init = fs::read_to_string(static_dir.join("js/core/theme-init.js")) - .unwrap_or_default(); + let theme_init = + fs::read_to_string(static_dir.join("js/core/theme-init.js")).unwrap_or_default(); let theme_init_min = js_minify_safe(&theme_init); let rewritten_index = rewrite_index_html( &index_html, @@ -153,9 +153,7 @@ fn process_release(manifest_dir: &Path, static_dir: &Path, out_dir: &Path) { // index.html too (future use / embedded route) fs::write(out_dir.join("index.html"), &rewritten_index).expect("write out index.html"); - eprintln!( - "cargo:warning=OxiCloud static-dist built ✓ CSS: {css_name} JS: {js_name}" - ); + eprintln!("cargo:warning=OxiCloud static-dist built ✓ CSS: {css_name} JS: {js_name}"); } // ═══════════════════════════════════════════════════════════════════════════════ @@ -206,8 +204,8 @@ fn css_minify_safe(source: &str) -> String { fn css_minify(source: &str) -> Result { use lightningcss::stylesheet::{ParserOptions, PrinterOptions, StyleSheet}; - let mut sheet = StyleSheet::parse(source, ParserOptions::default()) - .map_err(|e| format!("{e}"))?; + let mut sheet = + StyleSheet::parse(source, ParserOptions::default()).map_err(|e| format!("{e}"))?; sheet .minify(Default::default()) @@ -224,12 +222,14 @@ fn css_minify(source: &str) -> Result { } /// Walk a directory and minify every `.css` in-place (skips generated bundles). -fn minify_tree_css(dir: &Path, skip_prefix: &str) { - let Ok(entries) = fs::read_dir(dir) else { return }; +fn minify_tree_css(dir: &Path) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; for entry in entries.flatten() { let p = entry.path(); if p.is_dir() { - minify_tree_css(&p, skip_prefix); + minify_tree_css(&p); } else if p.extension().is_some_and(|e| e == "css") { let fname = p.file_name().unwrap().to_string_lossy(); // Skip the generated bundle and already-processed main.css @@ -336,12 +336,14 @@ fn js_minify(source: &str) -> Result { } /// Walk a directory and minify every `.js` in-place (skips generated bundles). -fn minify_tree_js(dir: &Path, skip_prefix: &str) { - let Ok(entries) = fs::read_dir(dir) else { return }; +fn minify_tree_js(dir: &Path) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; for entry in entries.flatten() { let p = entry.path(); if p.is_dir() { - minify_tree_js(&p, skip_prefix); + minify_tree_js(&p); } else if p.extension().is_some_and(|e| e == "js") { let fname = p.file_name().unwrap().to_string_lossy(); if fname.starts_with("app.") { @@ -359,13 +361,15 @@ fn minify_tree_js(dir: &Path, skip_prefix: &str) { // ═══════════════════════════════════════════════════════════════════════════════ fn minify_tree_json(dir: &Path) { - let Ok(entries) = fs::read_dir(dir) else { return }; + let Ok(entries) = fs::read_dir(dir) else { + return; + }; for entry in entries.flatten() { let p = entry.path(); - if p.extension().is_some_and(|e| e == "json") { - if let Ok(src) = fs::read_to_string(&p) { - let _ = fs::write(&p, json_minify(&src)); - } + if p.extension().is_some_and(|e| e == "json") + && let Ok(src) = fs::read_to_string(&p) + { + let _ = fs::write(&p, json_minify(&src)); } } } @@ -407,12 +411,7 @@ fn json_minify(source: &str) -> String { // ═══════════════════════════════════════════════════════════════════════════════ /// Rewrite index.html: single CSS bundle, inline theme-init, single JS bundle. -fn rewrite_index_html( - html: &str, - css_path: &str, - js_path: &str, - inline_theme_js: &str, -) -> String { +fn rewrite_index_html(html: &str, css_path: &str, js_path: &str, inline_theme_js: &str) -> String { let mut out: Vec = Vec::with_capacity(html.lines().count()); let mut css_done = false; let mut defer_done = false; @@ -423,19 +422,14 @@ fn rewrite_index_html( // ── Replace all stylesheet s with single bundle ──────────────── if t.starts_with("" - )); + out.push(format!(" ")); css_done = true; } continue; } // ── Replace sync theme-init.js with inline ")); continue; } @@ -443,9 +437,7 @@ fn rewrite_index_html( // ── Replace all defer " - )); + out.push(format!(" ")); defer_done = true; } continue; diff --git a/src/application/ports/thumbnail_ports.rs b/src/application/ports/thumbnail_ports.rs index 8dcc04b7..5d5b5fa7 100755 --- a/src/application/ports/thumbnail_ports.rs +++ b/src/application/ports/thumbnail_ports.rs @@ -89,11 +89,7 @@ pub trait ThumbnailPort: Send + Sync + 'static { /// Returns `None` if no cached thumbnail exists on disk or in memory. /// Used for non-image file types (videos) where thumbnails are /// generated client-side and uploaded. - async fn get_cached_thumbnail( - &self, - file_id: &str, - size: ThumbnailSize, - ) -> Option; + async fn get_cached_thumbnail(&self, file_id: &str, size: ThumbnailSize) -> Option; /// Store an externally-generated thumbnail (e.g. client-side video frame). /// diff --git a/src/application/services/auth_application_service.rs b/src/application/services/auth_application_service.rs index 7c77b104..6b888e06 100755 --- a/src/application/services/auth_application_service.rs +++ b/src/application/services/auth_application_service.rs @@ -17,11 +17,11 @@ use crate::infrastructure::services::jwt_service::JwtTokenService; use crate::infrastructure::services::oidc_service::OidcService; use crate::infrastructure::services::password_hasher::Argon2PasswordHasher; use moka::sync::Cache; -use uuid::Uuid; use std::path::PathBuf; use std::sync::Arc; use std::sync::RwLock; use std::time::Duration; +use uuid::Uuid; /// Result of a successful OIDC callback. The handler layer inspects this to /// decide whether to redirect to the regular frontend or complete a Nextcloud diff --git a/src/application/services/batch_operations.rs b/src/application/services/batch_operations.rs index 9881bc63..a149ff6c 100755 --- a/src/application/services/batch_operations.rs +++ b/src/application/services/batch_operations.rs @@ -448,11 +448,9 @@ impl BatchOperationService { }, }; - let uid = user_id; - let mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| { let trash = trash_service.clone(); - let uid = uid; + let uid = user_id; async move { let trash_result = trash.move_to_trash(&file_id, "file", uid).await; @@ -513,11 +511,9 @@ impl BatchOperationService { }, }; - let uid = user_id; - let mut operation_stream = stream::iter(folder_ids.into_iter().map(|folder_id| { let trash = trash_service.clone(); - let uid = uid; + let uid = user_id; async move { let trash_result = trash.move_to_trash(&folder_id, "folder", uid).await; diff --git a/src/application/services/contact_service.rs b/src/application/services/contact_service.rs index 0a7904a7..f95e9406 100755 --- a/src/application/services/contact_service.rs +++ b/src/application/services/contact_service.rs @@ -1,6 +1,6 @@ use chrono::Utc; -use uuid::Uuid; use std::sync::Arc; +use uuid::Uuid; use crate::application::dtos::address_book_dto::{ AddressBookDto, CreateAddressBookDto, ShareAddressBookDto, UnshareAddressBookDto, @@ -296,8 +296,11 @@ impl AddressBookUseCase for ContactService { // Check if user has write access to the address book let address_book = self - .check_address_book_write_access(&id, &Uuid::parse_str(&update.user_id) - .map_err(|_| DomainError::validation_error("Invalid user ID format"))?) + .check_address_book_write_access( + &id, + &Uuid::parse_str(&update.user_id) + .map_err(|_| DomainError::validation_error("Invalid user ID format"))?, + ) .await?; // Apply updates @@ -526,9 +529,12 @@ impl ContactUseCase for ContactService { .map_err(|_| DomainError::validation_error("Invalid address book ID format"))?; // Check if user has write access to the address book - self.check_address_book_write_access(&address_book_id, &Uuid::parse_str(&dto.user_id) - .map_err(|_| DomainError::validation_error("Invalid user ID format"))?) - .await?; + self.check_address_book_write_access( + &address_book_id, + &Uuid::parse_str(&dto.user_id) + .map_err(|_| DomainError::validation_error("Invalid user ID format"))?, + ) + .await?; // Convert DTOs to domain entities let email: Vec = dto @@ -604,9 +610,12 @@ impl ContactUseCase for ContactService { .map_err(|_| DomainError::validation_error("Invalid address book ID format"))?; // Check if user has write access to the address book - self.check_address_book_write_access(&address_book_id, &Uuid::parse_str(&dto.user_id) - .map_err(|_| DomainError::validation_error("Invalid user ID format"))?) - .await?; + self.check_address_book_write_access( + &address_book_id, + &Uuid::parse_str(&dto.user_id) + .map_err(|_| DomainError::validation_error("Invalid user ID format"))?, + ) + .await?; // Parse vCard data let mut contact = self.parse_vcard(&dto.vcard)?; @@ -819,9 +828,12 @@ impl ContactUseCase for ContactService { .map_err(|_| DomainError::validation_error("Invalid address book ID format"))?; // Check if user has write access to the address book - self.check_address_book_write_access(&address_book_id, &Uuid::parse_str(&dto.user_id) - .map_err(|_| DomainError::validation_error("Invalid user ID format"))?) - .await?; + self.check_address_book_write_access( + &address_book_id, + &Uuid::parse_str(&dto.user_id) + .map_err(|_| DomainError::validation_error("Invalid user ID format"))?, + ) + .await?; let group = ContactGroup::new(address_book_id, dto.name); @@ -845,9 +857,12 @@ impl ContactUseCase for ContactService { .ok_or_else(|| DomainError::not_found("Contact group", "not found"))?; // Check if user has write access to the address book - self.check_address_book_write_access(group.address_book_id(), &Uuid::parse_str(&update.user_id) - .map_err(|_| DomainError::validation_error("Invalid user ID format"))?) - .await?; + self.check_address_book_write_access( + group.address_book_id(), + &Uuid::parse_str(&update.user_id) + .map_err(|_| DomainError::validation_error("Invalid user ID format"))?, + ) + .await?; // Update the group let updated_group = ContactGroup::from_raw( diff --git a/src/application/services/file_management_service.rs b/src/application/services/file_management_service.rs index 06ba7ff5..c08ff9ce 100755 --- a/src/application/services/file_management_service.rs +++ b/src/application/services/file_management_service.rs @@ -178,10 +178,10 @@ impl FileManagementUseCase for FileManagementService { async fn delete_file(&self, id: &str) -> Result<(), DomainError> { self.file_repository.delete_file(id).await?; // Best-effort thumbnail cleanup - if let Some(thumb) = &self.thumbnail_service { - if let Err(e) = thumb.delete_thumbnails(id).await { - warn!("Failed to delete thumbnails for file {}: {}", id, e); - } + if let Some(thumb) = &self.thumbnail_service + && let Err(e) = thumb.delete_thumbnails(id).await + { + warn!("Failed to delete thumbnails for file {}: {}", id, e); } Ok(()) } @@ -223,10 +223,10 @@ impl FileManagementUseCase for FileManagementService { warn!("Permanently deleting file: {}", id); self.file_repository.delete_file(id).await?; // Best-effort thumbnail cleanup - if let Some(thumb) = &self.thumbnail_service { - if let Err(e) = thumb.delete_thumbnails(id).await { - warn!("Failed to delete thumbnails for file {}: {}", id, e); - } + if let Some(thumb) = &self.thumbnail_service + && let Err(e) = thumb.delete_thumbnails(id).await + { + warn!("Failed to delete thumbnails for file {}: {}", id, e); } info!("File permanently deleted: {}", id); diff --git a/src/application/services/idor_protection_test.rs b/src/application/services/idor_protection_test.rs index fef54a17..a221e5b2 100755 --- a/src/application/services/idor_protection_test.rs +++ b/src/application/services/idor_protection_test.rs @@ -9,6 +9,7 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::pin::Pin; use std::sync::Mutex; +use uuid::Uuid; use crate::application::ports::storage_ports::{FileReadPort, FileWritePort}; use crate::common::errors::DomainError; @@ -22,7 +23,7 @@ use crate::domain::services::path_service::StoragePath; /// A simple in-memory mock that maps (file_id → (File, owner_id)). struct MockFileReadPort { /// file_id → (File, owner_id) - files: Mutex>, + files: Mutex>, } impl MockFileReadPort { @@ -33,7 +34,7 @@ impl MockFileReadPort { } /// Insert a test file owned by `owner_id`. - fn insert(&self, id: &str, name: &str, owner_id: &str) { + fn insert(&self, id: &str, name: &str, owner_id: Uuid) { let file = File::new( id.to_string(), name.to_string(), @@ -46,7 +47,7 @@ impl MockFileReadPort { self.files .lock() .unwrap() - .insert(id.to_string(), (file, owner_id.to_string())); + .insert(id.to_string(), (file, owner_id)); } } @@ -59,10 +60,10 @@ impl FileReadPort for MockFileReadPort { .ok_or_else(|| DomainError::not_found("File", id.to_string())) } - async fn get_file_for_owner(&self, id: &str, owner_id: &str) -> Result { + async fn get_file_for_owner(&self, id: &str, owner_id: Uuid) -> Result { let files = self.files.lock().unwrap(); match files.get(id) { - Some((file, actual_owner)) if actual_owner == owner_id => Ok(file.clone()), + Some((file, actual_owner)) if *actual_owner == owner_id => Ok(file.clone()), // Return NotFound regardless — do not leak existence _ => Err(DomainError::not_found("File", id.to_string())), } @@ -104,7 +105,7 @@ impl FileReadPort for MockFileReadPort { &self, _folder_id: Option<&str>, _criteria: &crate::application::dtos::search_dto::SearchCriteriaDto, - _user_id: &str, + _user_id: Uuid, ) -> Result<(Vec, usize), DomainError> { Ok((Vec::new(), 0)) } @@ -113,7 +114,7 @@ impl FileReadPort for MockFileReadPort { &self, _folder_id: Option<&str>, _criteria: &crate::application::dtos::search_dto::SearchCriteriaDto, - _user_id: &str, + _user_id: Uuid, ) -> Result { Ok(0) } @@ -248,20 +249,23 @@ impl FileWritePort for MockFileWritePort { #[tokio::test] async fn get_file_for_owner_returns_file_for_correct_owner() { + let alice_id = Uuid::new_v4(); let repo = MockFileReadPort::new(); - repo.insert("file-1", "secret.txt", "alice"); + repo.insert("file-1", "secret.txt", alice_id); - let result = repo.get_file_for_owner("file-1", "alice").await; + let result = repo.get_file_for_owner("file-1", alice_id).await; assert!(result.is_ok(), "owner should be able to read own file"); assert_eq!(result.unwrap().id(), "file-1"); } #[tokio::test] async fn get_file_for_owner_rejects_wrong_owner() { + let alice_id = Uuid::new_v4(); + let bob_id = Uuid::new_v4(); let repo = MockFileReadPort::new(); - repo.insert("file-1", "secret.txt", "alice"); + repo.insert("file-1", "secret.txt", alice_id); - let result = repo.get_file_for_owner("file-1", "bob").await; + let result = repo.get_file_for_owner("file-1", bob_id).await; assert!(result.is_err(), "non-owner should be rejected"); // Must be NotFound, NOT Forbidden — avoids leaking existence @@ -276,20 +280,23 @@ async fn get_file_for_owner_rejects_wrong_owner() { #[tokio::test] async fn get_file_for_owner_returns_not_found_for_missing_file() { + let alice_id = Uuid::new_v4(); let repo = MockFileReadPort::new(); - let result = repo.get_file_for_owner("nonexistent", "alice").await; + let result = repo.get_file_for_owner("nonexistent", alice_id).await; assert!(result.is_err()); } #[tokio::test] async fn verify_file_owner_uses_default_impl() { + let alice_id = Uuid::new_v4(); + let bob_id = Uuid::new_v4(); let repo = MockFileReadPort::new(); - repo.insert("file-1", "secret.txt", "alice"); + repo.insert("file-1", "secret.txt", alice_id); // Default impl delegates to get_file_for_owner and maps to () - assert!(repo.verify_file_owner("file-1", "alice").await.is_ok()); - assert!(repo.verify_file_owner("file-1", "bob").await.is_err()); + assert!(repo.verify_file_owner("file-1", alice_id).await.is_ok()); + assert!(repo.verify_file_owner("file-1", bob_id).await.is_err()); } // ═══════════════════════════════════════════════════════════════════════════ @@ -310,15 +317,17 @@ async fn verify_file_owner_uses_default_impl() { async fn verify_file_owner_delegates_to_read_port() { // This test verifies the FileReadPort contract that verify_file_owner // returns Ok for the correct owner and Err for others. + let user_id = Uuid::new_v4(); + let attacker_id = Uuid::new_v4(); let read = MockFileReadPort::new(); - read.insert("abc-123", "report.pdf", "user-42"); + read.insert("abc-123", "report.pdf", user_id); // Same user → Ok - let ok = read.verify_file_owner("abc-123", "user-42").await; + let ok = read.verify_file_owner("abc-123", user_id).await; assert!(ok.is_ok(), "correct owner should pass verify_file_owner"); // Different user → Err - let err = read.verify_file_owner("abc-123", "attacker-99").await; + let err = read.verify_file_owner("abc-123", attacker_id).await; assert!(err.is_err(), "wrong owner should fail verify_file_owner"); } @@ -326,15 +335,17 @@ async fn verify_file_owner_delegates_to_read_port() { async fn owned_methods_require_ownership_check_first() { // Simulate what the _owned methods do: verify_owner then delegate. // We test with the mock read port to prove the sequence. + let owner_id = Uuid::new_v4(); + let attacker_id = Uuid::new_v4(); let read = MockFileReadPort::new(); - read.insert("file-1", "data.csv", "owner-a"); + read.insert("file-1", "data.csv", owner_id); // Step 1: verify_owner for correct owner → Ok - let step1 = read.verify_file_owner("file-1", "owner-a").await; + let step1 = read.verify_file_owner("file-1", owner_id).await; assert!(step1.is_ok()); // Step 2: verify_owner for attacker → Err, so the move/rename never executes - let step2 = read.verify_file_owner("file-1", "attacker").await; + let step2 = read.verify_file_owner("file-1", attacker_id).await; assert!(step2.is_err()); } @@ -347,18 +358,20 @@ use crate::common::stubs::StubFileManagementUseCase; #[tokio::test] async fn stub_move_file_owned_returns_ok() { + let user_id = Uuid::new_v4(); let stub = StubFileManagementUseCase; let result = stub - .move_file_owned("file-1", "user-1", Some("folder-2".to_string())) + .move_file_owned("file-1", user_id, Some("folder-2".to_string())) .await; assert!(result.is_ok(), "stub should return Ok for move_file_owned"); } #[tokio::test] async fn stub_rename_file_owned_returns_ok() { + let user_id = Uuid::new_v4(); let stub = StubFileManagementUseCase; let result = stub - .rename_file_owned("file-1", "user-1", "new-name.txt") + .rename_file_owned("file-1", user_id, "new-name.txt") .await; assert!( result.is_ok(), @@ -371,16 +384,18 @@ use crate::common::stubs::StubFileRetrievalUseCase; #[tokio::test] async fn stub_get_file_owned_returns_ok() { + let user_id = Uuid::new_v4(); let stub = StubFileRetrievalUseCase; - let result = stub.get_file_owned("file-1", "user-1").await; + let result = stub.get_file_owned("file-1", user_id).await; assert!(result.is_ok(), "stub should return Ok for get_file_owned"); } #[tokio::test] async fn stub_get_file_optimized_owned_returns_ok() { + let user_id = Uuid::new_v4(); let stub = StubFileRetrievalUseCase; let result = stub - .get_file_optimized_owned("file-1", "user-1", true, false) + .get_file_optimized_owned("file-1", user_id, true, false) .await; assert!( result.is_ok(), diff --git a/src/application/services/share_service.rs b/src/application/services/share_service.rs index 1d88f22f..14e25800 100755 --- a/src/application/services/share_service.rs +++ b/src/application/services/share_service.rs @@ -806,7 +806,7 @@ mod tests { &self, _folder_id: Option<&str>, _criteria: &crate::application::dtos::search_dto::SearchCriteriaDto, - _user_id: &str, + _user_id: Uuid, ) -> Result<(Vec, usize), DomainError> { Ok((Vec::new(), 0)) } @@ -815,7 +815,7 @@ mod tests { &self, _folder_id: Option<&str>, _criteria: &crate::application::dtos::search_dto::SearchCriteriaDto, - _user_id: &str, + _user_id: Uuid, ) -> Result { Ok(0) } @@ -839,7 +839,7 @@ mod tests { async fn get_file_for_owner( &self, id: &str, - _owner_id: &str, + _owner_id: Uuid, ) -> Result { self.get_file(id).await } @@ -891,7 +891,7 @@ mod tests { async fn list_folders_by_owner( &self, _parent_id: Option<&str>, - _owner_id: &str, + _owner_id: Uuid, ) -> Result, DomainError> { unimplemented!() } @@ -910,7 +910,7 @@ mod tests { async fn list_folders_by_owner_paginated( &self, _parent_id: Option<&str>, - _owner_id: &str, + _owner_id: Uuid, _offset: usize, _limit: usize, _include_total: bool, @@ -971,7 +971,7 @@ mod tests { async fn create_home_folder( &self, - _user_id: &str, + _user_id: Uuid, _name: String, ) -> Result { unimplemented!() diff --git a/src/application/services/trash_service.rs b/src/application/services/trash_service.rs index 881bd4e5..c3323e23 100755 --- a/src/application/services/trash_service.rs +++ b/src/application/services/trash_service.rs @@ -566,10 +566,10 @@ impl TrashUseCase for TrashService { // Best-effort thumbnail cleanup — thumbnails are cache // artifacts, so failure must not block file deletion. - if let Some(thumb) = &self.thumbnail_service { - if let Err(e) = thumb.delete_thumbnails(&file_id).await { - warn!("Failed to delete thumbnails for file {}: {}", file_id, e); - } + if let Some(thumb) = &self.thumbnail_service + && let Err(e) = thumb.delete_thumbnails(&file_id).await + { + warn!("Failed to delete thumbnails for file {}: {}", file_id, e); } } TrashedItemType::Folder => { diff --git a/src/application/services/trash_service_test.rs b/src/application/services/trash_service_test.rs index cd20fded..620c2828 100755 --- a/src/application/services/trash_service_test.rs +++ b/src/application/services/trash_service_test.rs @@ -61,10 +61,8 @@ where FW: FileWritePort, FoR: FolderRepository, { - async fn get_trash_items(&self, user_id: &str) -> Result> { - let user_uuid = Uuid::parse_str(user_id) - .map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?; - let items = self.trash_repository.get_trash_items(&user_uuid).await?; + async fn get_trash_items(&self, user_id: Uuid) -> Result> { + let items = self.trash_repository.get_trash_items(&user_id).await?; Ok(items .into_iter() .map(|item| { @@ -85,11 +83,9 @@ where .collect()) } - async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: &str) -> Result<()> { + async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: Uuid) -> Result<()> { let item_uuid = Uuid::parse_str(item_id) .map_err(|e| DomainError::validation_error(format!("Invalid item ID: {}", e)))?; - let user_uuid = Uuid::parse_str(user_id) - .map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?; match item_type { "file" => { @@ -103,7 +99,7 @@ where let original_path = file.storage_path().to_string(); let trashed_item = TrashedItem::new( item_uuid, - user_uuid, + user_id, TrashedItemType::File, file.name().to_string(), original_path, @@ -145,7 +141,7 @@ where let original_path = folder.storage_path().to_string(); let trashed_item = TrashedItem::new( item_uuid, - user_uuid, + user_id, TrashedItemType::Folder, folder.name().to_string(), original_path, @@ -179,15 +175,13 @@ where } } - async fn restore_item(&self, trash_id: &str, user_id: &str) -> Result<()> { + async fn restore_item(&self, trash_id: &str, user_id: Uuid) -> Result<()> { let trash_uuid = Uuid::parse_str(trash_id) .map_err(|e| DomainError::validation_error(format!("Invalid trash ID: {}", e)))?; - let user_uuid = Uuid::parse_str(user_id) - .map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?; let item = self .trash_repository - .get_trash_item(&trash_uuid, &user_uuid) + .get_trash_item(&trash_uuid, &user_id) .await?; match item { Some(item) => { @@ -228,7 +222,7 @@ where } } self.trash_repository - .restore_from_trash(&trash_uuid, &user_uuid) + .restore_from_trash(&trash_uuid, &user_id) .await .map_err(|e| { DomainError::new( @@ -243,15 +237,13 @@ where } } - async fn delete_permanently(&self, trash_id: &str, user_id: &str) -> Result<()> { + async fn delete_permanently(&self, trash_id: &str, user_id: Uuid) -> Result<()> { let trash_uuid = Uuid::parse_str(trash_id) .map_err(|e| DomainError::validation_error(format!("Invalid trash ID: {}", e)))?; - let user_uuid = Uuid::parse_str(user_id) - .map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?; let item = self .trash_repository - .get_trash_item(&trash_uuid, &user_uuid) + .get_trash_item(&trash_uuid, &user_id) .await?; match item { Some(item) => { @@ -287,7 +279,7 @@ where } } self.trash_repository - .delete_permanently(&trash_uuid, &user_uuid) + .delete_permanently(&trash_uuid, &user_id) .await .map_err(|e| { DomainError::new( @@ -302,10 +294,8 @@ where } } - async fn empty_trash(&self, user_id: &str) -> Result<()> { - let user_uuid = Uuid::parse_str(user_id) - .map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?; - self.trash_repository.clear_trash(&user_uuid).await + async fn empty_trash(&self, user_id: Uuid) -> Result<()> { + self.trash_repository.clear_trash(&user_id).await } } @@ -492,7 +482,7 @@ impl FileReadPort for MockFileRepository { &self, _folder_id: Option<&str>, _criteria: &crate::application::dtos::search_dto::SearchCriteriaDto, - _user_id: &str, + _user_id: Uuid, ) -> std::result::Result<(Vec, usize), DomainError> { Ok((Vec::new(), 0)) } @@ -501,7 +491,7 @@ impl FileReadPort for MockFileRepository { &self, _folder_id: Option<&str>, _criteria: &crate::application::dtos::search_dto::SearchCriteriaDto, - _user_id: &str, + _user_id: Uuid, ) -> std::result::Result { Ok(0) } @@ -519,7 +509,7 @@ impl FileReadPort for MockFileRepository { async fn get_file_for_owner( &self, id: &str, - _owner_id: &str, + _owner_id: Uuid, ) -> std::result::Result { // In this mock, ignore ownership — trash tests don't focus on ownership self.get_file(id).await @@ -697,7 +687,7 @@ impl FolderRepository for MockFolderRepository { async fn list_folders_by_owner( &self, _parent_id: Option<&str>, - _owner_id: &str, + _owner_id: Uuid, ) -> std::result::Result, DomainError> { Ok(vec![]) } @@ -715,7 +705,7 @@ impl FolderRepository for MockFolderRepository { async fn list_folders_by_owner_paginated( &self, _parent_id: Option<&str>, - _owner_id: &str, + _owner_id: Uuid, _offset: usize, _limit: usize, _include_total: bool, @@ -799,7 +789,7 @@ impl FolderRepository for MockFolderRepository { async fn create_home_folder( &self, - _user_id: &str, + _user_id: Uuid, _name: String, ) -> std::result::Result { Ok(Folder::default()) @@ -839,18 +829,18 @@ mod tests { let file_id = "550e8400-e29b-41d4-a716-446655440000"; let user_id = "550e8400-e29b-41d4-a716-446655440001"; + let user_uuid = Uuid::parse_str(user_id).unwrap(); // Add a test file to the repository file_repo.add_test_file(file_id, "test.txt", "/test/path/test.txt"); // Act - let result = service.move_to_trash(file_id, "file", user_id).await; + let result = service.move_to_trash(file_id, "file", user_uuid).await; // Assert assert!(result.is_ok(), "Moving file to trash failed: {:?}", result); // Verify the file is in trash - let user_uuid = Uuid::parse_str(user_id).unwrap(); let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); assert_eq!( @@ -913,12 +903,13 @@ mod tests { let folder_id = "550e8400-e29b-41d4-a716-446655440002"; let user_id = "550e8400-e29b-41d4-a716-446655440001"; + let user_uuid = Uuid::parse_str(user_id).unwrap(); // Add a test folder to the repository folder_repo.add_test_folder(folder_id, "test_folder", "/test/path/test_folder"); // Act - let result = service.move_to_trash(folder_id, "folder", user_id).await; + let result = service.move_to_trash(folder_id, "folder", user_uuid).await; // Assert assert!( @@ -928,7 +919,6 @@ mod tests { ); // Verify the folder is in trash - let user_uuid = Uuid::parse_str(user_id).unwrap(); let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); assert_eq!( @@ -978,22 +968,22 @@ mod tests { let file_id = "550e8400-e29b-41d4-a716-446655440000"; let user_id = "550e8400-e29b-41d4-a716-446655440001"; + let user_uuid = Uuid::parse_str(user_id).unwrap(); let file_path = "/test/path/test.txt"; // Add a test file and move it to trash file_repo.add_test_file(file_id, "test.txt", file_path); service - .move_to_trash(file_id, "file", user_id) + .move_to_trash(file_id, "file", user_uuid) .await .unwrap(); // Get the trash item ID - let user_uuid = Uuid::parse_str(user_id).unwrap(); let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); let trash_id = trash_items[0].id().to_string(); // Act - let result = service.restore_item(&trash_id, user_id).await; + let result = service.restore_item(&trash_id, user_uuid).await; // Assert assert!( @@ -1048,21 +1038,21 @@ mod tests { let file_id = "550e8400-e29b-41d4-a716-446655440000"; let user_id = "550e8400-e29b-41d4-a716-446655440001"; + let user_uuid = Uuid::parse_str(user_id).unwrap(); // Add a test file and move it to trash file_repo.add_test_file(file_id, "test.txt", "/test/path/test.txt"); service - .move_to_trash(file_id, "file", user_id) + .move_to_trash(file_id, "file", user_uuid) .await .unwrap(); // Get the trash item ID - let user_uuid = Uuid::parse_str(user_id).unwrap(); let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); let trash_id = trash_items[0].id().to_string(); // Act - let result = service.delete_permanently(&trash_id, user_id).await; + let result = service.delete_permanently(&trash_id, user_uuid).await; // Assert assert!( @@ -1116,6 +1106,7 @@ mod tests { ); let user_id = "550e8400-e29b-41d4-a716-446655440001"; + let user_uuid = Uuid::parse_str(user_id).unwrap(); // Add multiple files and folders to trash let file_ids = [ @@ -1136,7 +1127,7 @@ mod tests { &format!("/test/path/test{}.txt", i), ); service - .move_to_trash(file_id, "file", user_id) + .move_to_trash(file_id, "file", user_uuid) .await .unwrap(); } @@ -1148,18 +1139,17 @@ mod tests { &format!("/test/path/folder{}", i), ); service - .move_to_trash(folder_id, "folder", user_id) + .move_to_trash(folder_id, "folder", user_uuid) .await .unwrap(); } // Verify items are in trash - let user_uuid = Uuid::parse_str(user_id).unwrap(); let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); assert_eq!(trash_items.len(), 4, "Should have 4 items in trash"); // Act - let result = service.empty_trash(user_id).await; + let result = service.empty_trash(user_uuid).await; // Assert assert!(result.is_ok(), "Emptying trash failed: {:?}", result); diff --git a/src/domain/entities/file.rs b/src/domain/entities/file.rs index 5dac1fc5..6129969e 100755 --- a/src/domain/entities/file.rs +++ b/src/domain/entities/file.rs @@ -310,7 +310,7 @@ impl File { folder_id: self.folder_id.clone(), created_at: self.created_at, modified_at: now, - owner_id: self.owner_id.clone(), + owner_id: self.owner_id, }) } @@ -344,7 +344,7 @@ impl File { folder_id, created_at: self.created_at, modified_at: now, - owner_id: self.owner_id.clone(), + owner_id: self.owner_id, }) } @@ -365,7 +365,7 @@ impl File { folder_id: self.folder_id.clone(), created_at: self.created_at, modified_at: now, - owner_id: self.owner_id.clone(), + owner_id: self.owner_id, } } } diff --git a/src/domain/entities/folder.rs b/src/domain/entities/folder.rs index 67d08035..fdf82bb4 100755 --- a/src/domain/entities/folder.rs +++ b/src/domain/entities/folder.rs @@ -234,7 +234,7 @@ impl Folder { storage_path: new_storage_path, path_string: new_path_string, parent_id: self.parent_id.clone(), - owner_id: self.owner_id.clone(), + owner_id: self.owner_id, created_at: self.created_at, modified_at: now, }) @@ -266,7 +266,7 @@ impl Folder { storage_path: new_storage_path, path_string: new_path_string, parent_id, - owner_id: self.owner_id.clone(), + owner_id: self.owner_id, created_at: self.created_at, modified_at: now, }) diff --git a/src/infrastructure/adapters/contact_storage_adapter.rs b/src/infrastructure/adapters/contact_storage_adapter.rs index b80f3b89..d629be2c 100755 --- a/src/infrastructure/adapters/contact_storage_adapter.rs +++ b/src/infrastructure/adapters/contact_storage_adapter.rs @@ -86,7 +86,10 @@ impl ContactStorageAdapter { .address_book_repository .get_address_book_shares(address_book_id) .await?; - if shares.iter().any(|(shared_user, _)| shared_user == &user_id.to_string()) { + if shares + .iter() + .any(|(shared_user, _)| shared_user == &user_id.to_string()) + { return Ok(address_book); } @@ -248,7 +251,11 @@ impl AddressBookUseCase for ContactStorageAdapter { // Check write access let user_id = Uuid::parse_str(&update.user_id).map_err(|_| { - DomainError::new(ErrorKind::InvalidInput, "AddressBook", "Invalid user ID format") + DomainError::new( + ErrorKind::InvalidInput, + "AddressBook", + "Invalid user ID format", + ) })?; let mut address_book = self.check_write_access(&uuid, user_id).await?; @@ -364,7 +371,11 @@ impl AddressBookUseCase for ContactStorageAdapter { } let target_user_id = Uuid::parse_str(&dto.user_id).map_err(|_| { - DomainError::new(ErrorKind::InvalidInput, "AddressBook", "Invalid target user ID format") + DomainError::new( + ErrorKind::InvalidInput, + "AddressBook", + "Invalid target user ID format", + ) })?; self.address_book_repository @@ -397,7 +408,11 @@ impl AddressBookUseCase for ContactStorageAdapter { } let target_user_id = Uuid::parse_str(&dto.user_id).map_err(|_| { - DomainError::new(ErrorKind::InvalidInput, "AddressBook", "Invalid target user ID format") + DomainError::new( + ErrorKind::InvalidInput, + "AddressBook", + "Invalid target user ID format", + ) })?; self.address_book_repository @@ -443,8 +458,7 @@ impl ContactUseCase for ContactStorageAdapter { let user_id = Uuid::parse_str(&dto.user_id).map_err(|_| { DomainError::new(ErrorKind::InvalidInput, "Contact", "Invalid user ID format") })?; - self.check_write_access(&address_book_id, user_id) - .await?; + self.check_write_access(&address_book_id, user_id).await?; let now = chrono::Utc::now(); let mut contact = Contact::from_raw( @@ -488,8 +502,7 @@ impl ContactUseCase for ContactStorageAdapter { let user_id = Uuid::parse_str(&dto.user_id).map_err(|_| { DomainError::new(ErrorKind::InvalidInput, "Contact", "Invalid user ID format") })?; - self.check_write_access(&address_book_id, user_id) - .await?; + self.check_write_access(&address_book_id, user_id).await?; // Parse vCard fields let now = chrono::Utc::now(); @@ -742,10 +755,13 @@ impl ContactUseCase for ContactStorageAdapter { // Check write access let user_id = Uuid::parse_str(&dto.user_id).map_err(|_| { - DomainError::new(ErrorKind::InvalidInput, "ContactGroup", "Invalid user ID format") + DomainError::new( + ErrorKind::InvalidInput, + "ContactGroup", + "Invalid user ID format", + ) })?; - self.check_write_access(&address_book_id, user_id) - .await?; + self.check_write_access(&address_book_id, user_id).await?; let group = ContactGroup::new(address_book_id, dto.name); @@ -770,7 +786,11 @@ impl ContactUseCase for ContactStorageAdapter { // Check write access let user_id = Uuid::parse_str(&update.user_id).map_err(|_| { - DomainError::new(ErrorKind::InvalidInput, "ContactGroup", "Invalid user ID format") + DomainError::new( + ErrorKind::InvalidInput, + "ContactGroup", + "Invalid user ID format", + ) })?; self.check_write_access(group.address_book_id(), user_id) .await?; diff --git a/src/infrastructure/db.rs b/src/infrastructure/db.rs index 1b2ee68d..b7d1c1d5 100755 --- a/src/infrastructure/db.rs +++ b/src/infrastructure/db.rs @@ -283,7 +283,8 @@ fn split_sql_statements(sql: &str) -> Vec { if i >= len { break; } - if bytes[i] == b'$' && i + tag_bytes.len() <= len + if bytes[i] == b'$' + && i + tag_bytes.len() <= len && &bytes[i..i + tag_bytes.len()] == tag_bytes { current.push_str(&tag); diff --git a/src/infrastructure/repositories/pg/app_password_pg_repository.rs b/src/infrastructure/repositories/pg/app_password_pg_repository.rs index 8c1b2635..15fffa73 100755 --- a/src/infrastructure/repositories/pg/app_password_pg_repository.rs +++ b/src/infrastructure/repositories/pg/app_password_pg_repository.rs @@ -32,8 +32,8 @@ impl AppPasswordStoragePort for AppPasswordPgRepository { VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) "#, ) - .bind(&ap.id) - .bind(&ap.user_id) + .bind(ap.id) + .bind(ap.user_id) .bind(&ap.label) .bind(&ap.password_hash) .bind(&ap.prefix) diff --git a/src/infrastructure/repositories/pg/folder_db_repository.rs b/src/infrastructure/repositories/pg/folder_db_repository.rs index 7bf76a85..ba346322 100755 --- a/src/infrastructure/repositories/pg/folder_db_repository.rs +++ b/src/infrastructure/repositories/pg/folder_db_repository.rs @@ -21,16 +21,7 @@ use crate::domain::services::path_service::StoragePath; type FolderRow = (String, String, String, Option, Uuid, i64, i64); /// Type alias for paginated folder rows (includes total_count). -type FolderRowPaginated = ( - String, - String, - String, - Option, - Uuid, - i64, - i64, - i64, -); +type FolderRowPaginated = (String, String, String, Option, Uuid, i64, i64, i64); /// Type alias for folder rows with optional user_id. type FolderRowOptUser = ( @@ -108,14 +99,14 @@ impl FolderRepository for FolderDbRepository { // caller to have set up the home folder beforehand (done during user // registration). let user_id: Uuid = if let Some(ref pid) = parent_id { - sqlx::query_scalar::<_, Uuid>( - "SELECT user_id FROM storage.folders WHERE id = $1::uuid", - ) - .bind(pid) - .fetch_optional(self.pool()) - .await - .map_err(|e| DomainError::internal_error("FolderDb", format!("parent lookup: {e}")))? - .ok_or_else(|| DomainError::not_found("Folder", pid))? + sqlx::query_scalar::<_, Uuid>("SELECT user_id FROM storage.folders WHERE id = $1::uuid") + .bind(pid) + .fetch_optional(self.pool()) + .await + .map_err(|e| { + DomainError::internal_error("FolderDb", format!("parent lookup: {e}")) + })? + .ok_or_else(|| DomainError::not_found("Folder", pid))? } else { return Err(DomainError::internal_error( "FolderDb", @@ -647,15 +638,9 @@ impl FolderRepository for FolderDbRepository { .map_err(|e| DomainError::internal_error("FolderDb", format!("home folder: {e}")))?; match row { - Some((id, path, ca, ma)) => Self::row_to_folder( - id, - name.clone(), - path, - None, - Some(user_id), - ca, - ma, - ), + Some((id, path, ca, ma)) => { + Self::row_to_folder(id, name.clone(), path, None, Some(user_id), ca, ma) + } None => { // Already exists — fetch it let existing = sqlx::query_as::<_, (String, String, i64, i64)>( diff --git a/src/infrastructure/repositories/pg/share_pg_repository.rs b/src/infrastructure/repositories/pg/share_pg_repository.rs index bfdff9ba..592ae040 100755 --- a/src/infrastructure/repositories/pg/share_pg_repository.rs +++ b/src/infrastructure/repositories/pg/share_pg_repository.rs @@ -195,16 +195,15 @@ impl ShareStoragePort for SharePgRepository { } async fn delete_share_for_user(&self, id: Uuid, user_id: Uuid) -> Result<(), DomainError> { - let result = - sqlx::query("DELETE FROM storage.shares WHERE id = $1 AND created_by = $2") - .bind(id) - .bind(user_id) - .execute(&*self.db_pool) - .await - .map_err(|e| { - tracing::error!("Database error deleting share for user: {}", e); - DomainError::internal_error("Share", format!("Failed to delete share: {e}")) - })?; + let result = sqlx::query("DELETE FROM storage.shares WHERE id = $1 AND created_by = $2") + .bind(id) + .bind(user_id) + .execute(&*self.db_pool) + .await + .map_err(|e| { + tracing::error!("Database error deleting share for user: {}", e); + DomainError::internal_error("Share", format!("Failed to delete share: {e}")) + })?; if result.rows_affected() == 0 { // SECURITY: could be non-existent or owned by another user — same 404 diff --git a/src/infrastructure/repositories/pg/trash_db_repository.rs b/src/infrastructure/repositories/pg/trash_db_repository.rs index e8cc3ec9..9c0ed8a0 100755 --- a/src/infrastructure/repositories/pg/trash_db_repository.rs +++ b/src/infrastructure/repositories/pg/trash_db_repository.rs @@ -64,9 +64,9 @@ impl TrashDbRepository { // In the soft-delete model, the trash entry ID is the same as the // original item ID since there is no separate trash table. TrashedItem::from_raw( - id, // trash entry id (same as original) - id, // original item id - user_id, // owner + id, // trash entry id (same as original) + id, // original item id + user_id, // owner item_type_enum, name.clone(), String::new(), // original_path — not stored separately in soft-delete model diff --git a/src/infrastructure/services/jwt_service.rs b/src/infrastructure/services/jwt_service.rs index 930c2ba2..7570c70f 100755 --- a/src/infrastructure/services/jwt_service.rs +++ b/src/infrastructure/services/jwt_service.rs @@ -262,10 +262,11 @@ impl TokenServicePort for JwtTokenService { mod tests { use super::*; use crate::domain::entities::user::{User, UserRole}; + use uuid::Uuid; fn create_test_user() -> User { User::from_data( - "test-user-id".to_string(), + Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(), "testuser".to_string(), "test@example.com".to_string(), "hashed_password".to_string(), @@ -295,7 +296,7 @@ mod tests { let claims = service .validate_token(&token) .expect("Should validate token"); - assert_eq!(claims.sub, user.id()); + assert_eq!(claims.sub, user.id().to_string()); assert_eq!(claims.username, user.username()); assert_eq!(claims.email, user.email()); } diff --git a/src/infrastructure/services/thumbnail_service.rs b/src/infrastructure/services/thumbnail_service.rs index 8de8610b..fa696295 100755 --- a/src/infrastructure/services/thumbnail_service.rs +++ b/src/infrastructure/services/thumbnail_service.rs @@ -1,6 +1,6 @@ use bytes::Bytes; -use image::imageops::FilterType; use image::codecs::jpeg::JpegEncoder; +use image::imageops::FilterType; /** * Thumbnail Generation Service * @@ -236,20 +236,16 @@ impl ThumbnailService { /// Unlike `get_thumbnail`, this does **not** generate a new thumbnail. /// Useful for non-image file types (videos) where a client-generated /// thumbnail may have been uploaded previously. - pub async fn get_cached_thumbnail( - &self, - file_id: &str, - size: ThumbnailSize, - ) -> Option { + pub async fn get_cached_thumbnail(&self, file_id: &str, size: ThumbnailSize) -> Option { // 1. Check in-memory cache let cache_key = ThumbnailCacheKey { file_id: file_id.to_string(), size, }; - if let Some(bytes) = self.cache.get(&cache_key).await { - if !bytes.is_empty() { - return Some(bytes); - } + if let Some(bytes) = self.cache.get(&cache_key).await + && !bytes.is_empty() + { + return Some(bytes); } // 2. Check disk @@ -285,17 +281,17 @@ impl ThumbnailService { let jpeg_bytes = tokio::task::spawn_blocking(move || -> Result, ThumbnailError> { // ── Fast path: already a correctly-sized JPEG ───────────── // JPEG files start with SOI marker 0xFF 0xD8. - if data.len() >= 2 && data[0] == 0xFF && data[1] == 0xD8 { - if let Ok(reader) = image::ImageReader::new(std::io::Cursor::new(&data)) - .with_guessed_format() - { - if let Ok((w, h)) = reader.into_dimensions() { - if w <= max_dim && h <= max_dim { - // Already JPEG at correct size — zero-copy store - return Ok(data.to_vec()); - } - } - } + if data.len() >= 2 + && data[0] == 0xFF + && data[1] == 0xD8 + && let Ok(reader) = + image::ImageReader::new(std::io::Cursor::new(&data)).with_guessed_format() + && let Ok((w, h)) = reader.into_dimensions() + && w <= max_dim + && h <= max_dim + { + // Already JPEG at correct size — zero-copy store + return Ok(data.to_vec()); } // ── Slow path: decode, resize, re-encode to JPEG ───────── @@ -637,11 +633,7 @@ impl ThumbnailPort for ThumbnailService { .map_err(|e| DomainError::new(ErrorKind::InternalError, "Thumbnail", e.to_string())) } - async fn get_cached_thumbnail( - &self, - file_id: &str, - size: PortThumbnailSize, - ) -> Option { + async fn get_cached_thumbnail(&self, file_id: &str, size: PortThumbnailSize) -> Option { self.get_cached_thumbnail(file_id, size.into()).await } diff --git a/src/interfaces/api/handlers/admin_handler.rs b/src/interfaces/api/handlers/admin_handler.rs index 7f37ed0c..6cf5a458 100755 --- a/src/interfaces/api/handlers/admin_handler.rs +++ b/src/interfaces/api/handlers/admin_handler.rs @@ -73,7 +73,11 @@ async fn admin_guard(state: &AppState, headers: &HeaderMap) -> Result<(Uuid, Str )); } - Ok((Uuid::parse_str(&claims.sub).map_err(|_| AppError::internal_error("Invalid user ID in token"))?, claims.role)) + Ok(( + Uuid::parse_str(&claims.sub) + .map_err(|_| AppError::internal_error("Invalid user ID in token"))?, + claims.role, + )) } /// GET /api/admin/settings/oidc — get OIDC settings for the admin panel diff --git a/src/interfaces/api/handlers/app_password_handler.rs b/src/interfaces/api/handlers/app_password_handler.rs index 91ac4a8c..c3bc702f 100755 --- a/src/interfaces/api/handlers/app_password_handler.rs +++ b/src/interfaces/api/handlers/app_password_handler.rs @@ -75,10 +75,7 @@ async fn revoke_app_password( let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?; - let response = service - .revoke(user.id, id) - .await - .map_err(AppError::from)?; + let response = service.revoke(user.id, id).await.map_err(AppError::from)?; Ok(Json(response)) } diff --git a/src/interfaces/api/handlers/caldav_handler.rs b/src/interfaces/api/handlers/caldav_handler.rs index 8d2c35fc..ac18fac3 100755 --- a/src/interfaces/api/handlers/caldav_handler.rs +++ b/src/interfaces/api/handlers/caldav_handler.rs @@ -299,13 +299,12 @@ async fn handle_propfind( } else { // Not a calendar ID — treat as user calendar home (e.g. /caldav/{username}/) // List all calendars for this user - let calendars = - calendar_service - .list_my_calendars(user.id) - .await - .map_err(|e| { - AppError::internal_error(format!("Failed to list calendars: {}", e)) - })?; + let calendars = calendar_service + .list_my_calendars(user.id) + .await + .map_err(|e| { + AppError::internal_error(format!("Failed to list calendars: {}", e)) + })?; let base_href = &format!("/caldav/{}/", first_segment); let mut response_body = Vec::new(); diff --git a/src/interfaces/api/handlers/chunked_upload_handler.rs b/src/interfaces/api/handlers/chunked_upload_handler.rs index afbf387a..8fce8b93 100755 --- a/src/interfaces/api/handlers/chunked_upload_handler.rs +++ b/src/interfaces/api/handlers/chunked_upload_handler.rs @@ -190,13 +190,7 @@ impl ChunkedUploadHandler { }); match chunked_service - .upload_chunk( - &upload_id, - auth_user.id, - params.chunk_index, - body, - checksum, - ) + .upload_chunk(&upload_id, auth_user.id, params.chunk_index, body, checksum) .await { Ok(response) => { diff --git a/src/interfaces/api/handlers/dedup_handler.rs b/src/interfaces/api/handlers/dedup_handler.rs index aaa52bb2..d7def5b3 100755 --- a/src/interfaces/api/handlers/dedup_handler.rs +++ b/src/interfaces/api/handlers/dedup_handler.rs @@ -99,7 +99,9 @@ impl DedupHandler { } // Only reveal whether THIS user has the blob — no global oracle - let user_has_it = dedup.user_owns_blob_reference(&hash, &auth_user.id.to_string()).await; + let user_has_it = dedup + .user_owns_blob_reference(&hash, &auth_user.id.to_string()) + .await; if user_has_it { // Fetch size from metadata (safe — user owns a reference) @@ -346,7 +348,10 @@ impl DedupHandler { } // Verify the user owns at least one file referencing this blob - if !dedup.user_owns_blob_reference(&hash, &auth_user.id.to_string()).await { + if !dedup + .user_owns_blob_reference(&hash, &auth_user.id.to_string()) + .await + { return Response::builder() .status(StatusCode::NOT_FOUND) .header(header::CONTENT_TYPE, "application/json") diff --git a/src/interfaces/api/handlers/device_auth_handler.rs b/src/interfaces/api/handlers/device_auth_handler.rs index c83b1d7d..1544ae61 100755 --- a/src/interfaces/api/handlers/device_auth_handler.rs +++ b/src/interfaces/api/handlers/device_auth_handler.rs @@ -203,7 +203,8 @@ async fn revoke_device( ) -> Result { let device_service = get_device_service(&state)?; - let device_id = Uuid::parse_str(&device_id).map_err(|_| AppError::bad_request("Invalid device ID"))?; + let device_id = + Uuid::parse_str(&device_id).map_err(|_| AppError::bad_request("Invalid device ID"))?; device_service .revoke_device(device_id, auth_user.id) diff --git a/src/interfaces/api/handlers/file_handler.rs b/src/interfaces/api/handlers/file_handler.rs index c5c3ecea..cc010bbc 100755 --- a/src/interfaces/api/handlers/file_handler.rs +++ b/src/interfaces/api/handlers/file_handler.rs @@ -109,7 +109,11 @@ impl FileHandler { if let Some(ref fid) = folder_id { use crate::application::ports::inbound::FolderUseCase; let folder_service = &state.applications.folder_service; - if folder_service.get_folder_owned(fid, auth_user.id).await.is_err() { + if folder_service + .get_folder_owned(fid, auth_user.id) + .await + .is_err() + { tracing::warn!( "⛔ UPLOAD REJECTED (IDOR): user='{}' attempted upload to folder '{}' owned by another user", auth_user.username, @@ -336,18 +340,17 @@ impl FileHandler { // (file_id, size) pair. If the browser already has it, return 304 // with zero I/O or DB work. let etag = format!("\"thumb-{}-{:?}\"", id, thumb_size); - if let Some(if_none_match) = headers.get(header::IF_NONE_MATCH) { - if let Ok(val) = if_none_match.to_str() { - if val == etag || val == "*" { - return Response::builder() - .status(StatusCode::NOT_MODIFIED) - .header(header::ETAG, &etag) - .header(header::CACHE_CONTROL, "public, max-age=31536000, immutable") - .body(Body::empty()) - .unwrap() - .into_response(); - } - } + if let Some(if_none_match) = headers.get(header::IF_NONE_MATCH) + && let Ok(val) = if_none_match.to_str() + && (val == etag || val == "*") + { + return Response::builder() + .status(StatusCode::NOT_MODIFIED) + .header(header::ETAG, &etag) + .header(header::CACHE_CONTROL, "public, max-age=31536000, immutable") + .body(Body::empty()) + .unwrap() + .into_response(); } // ── Cache-first path (Solution A) ──────────────────────────── @@ -409,21 +412,17 @@ impl FileHandler { .get_thumbnail(&id, thumb_size.into(), &file_path) .await { - Ok(data) => { - Response::builder() - .status(StatusCode::OK) - .header(header::CONTENT_TYPE, "image/jpeg") - .header(header::CONTENT_LENGTH, data.len()) - .header(header::CACHE_CONTROL, "public, max-age=31536000, immutable") - .header(header::ETAG, &etag) - .body(Body::from(data)) - .unwrap() - .into_response() - } - Err(err) => { - AppError::internal_error(format!("Thumbnail generation failed: {}", err)) - .into_response() - } + Ok(data) => Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "image/jpeg") + .header(header::CONTENT_LENGTH, data.len()) + .header(header::CACHE_CONTROL, "public, max-age=31536000, immutable") + .header(header::ETAG, &etag) + .body(Body::from(data)) + .unwrap() + .into_response(), + Err(err) => AppError::internal_error(format!("Thumbnail generation failed: {}", err)) + .into_response(), } } @@ -487,10 +486,8 @@ impl FileHandler { .await { Ok(_) => StatusCode::CREATED.into_response(), - Err(err) => { - AppError::internal_error(format!("Failed to store thumbnail: {}", err)) - .into_response() - } + Err(err) => AppError::internal_error(format!("Failed to store thumbnail: {}", err)) + .into_response(), } } @@ -663,9 +660,7 @@ impl FileHandler { .unwrap() .into_response(), }, - Err(err) => { - AppError::from(err).into_response() - } + Err(err) => AppError::from(err).into_response(), } } @@ -715,9 +710,7 @@ impl FileHandler { .insert(header::ETAG, header::HeaderValue::from_str(&etag).unwrap()); resp } - Err(err) => { - AppError::from(err).into_response() - } + Err(err) => AppError::from(err).into_response(), } } @@ -750,8 +743,7 @@ impl FileHandler { tokio::spawn(async move { tracing::info!("🖼️ Generating thumbnails for: {}", file_id); - thumbnail_service - .generate_all_sizes_background(file_id, file_path); + thumbnail_service.generate_all_sizes_background(file_id, file_path); }); } @@ -832,7 +824,7 @@ impl FileHandler { match result { Ok(_) => StatusCode::NO_CONTENT.into_response(), - Err(err) => AppError::from(err).into_response() + Err(err) => AppError::from(err).into_response(), } } @@ -864,7 +856,7 @@ impl FileHandler { let mgmt = &state.applications.file_management_service; match mgmt.rename_file_owned(&id, auth_user.id, &new_name).await { Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(), - Err(err) => AppError::from(err).into_response() + Err(err) => AppError::from(err).into_response(), } } @@ -884,7 +876,7 @@ impl FileHandler { .await { Ok(file) => (StatusCode::OK, Json(file)).into_response(), - Err(err) => AppError::from(err).into_response() + Err(err) => AppError::from(err).into_response(), } } @@ -903,7 +895,7 @@ impl FileHandler { let mgmt = &state.applications.file_management_service; match mgmt.move_file_owned(&id, auth_user.id, folder_id).await { Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(), - Err(err) => AppError::from(err).into_response() + Err(err) => AppError::from(err).into_response(), } } @@ -962,9 +954,7 @@ impl FileHandler { }) .collect(); - format!( - "{disposition}; filename=\"{ascii_safe}\"; filename*=UTF-8''{encoded}" - ) + format!("{disposition}; filename=\"{ascii_safe}\"; filename*=UTF-8''{encoded}") } /// Build a 201 Created JSON response. diff --git a/src/interfaces/api/handlers/webdav_handler.rs b/src/interfaces/api/handlers/webdav_handler.rs index 9d95df22..4fd31229 100755 --- a/src/interfaces/api/handlers/webdav_handler.rs +++ b/src/interfaces/api/handlers/webdav_handler.rs @@ -1251,7 +1251,11 @@ async fn handle_move( && let Ok(parent) = folder_service.get_folder_by_path(dest_parent_path).await { - assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?; + assert_owner( + parent.owner_id.as_deref(), + &user.id.to_string(), + dest_parent_path, + )?; } file_management_service .move_file(&file.id, Some(dest_parent_path.to_string())) @@ -1281,7 +1285,11 @@ async fn handle_move( let folder_result = folder_service.get_folder_by_path(&source_path).await; if let Ok(folder) = folder_result { - assert_owner(folder.owner_id.as_deref(), &user.id.to_string(), &source_path)?; + assert_owner( + folder.owner_id.as_deref(), + &user.id.to_string(), + &source_path, + )?; let dest_folder_name = destination_path .split('/') .next_back() @@ -1299,7 +1307,11 @@ async fn handle_move( match folder_service.get_folder_by_path(dest_parent_path).await { Ok(parent) => { // SECURITY: verify destination parent belongs to caller (V-08) - assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?; + assert_owner( + parent.owner_id.as_deref(), + &user.id.to_string(), + dest_parent_path, + )?; Some(parent.id) } Err(_) => None, @@ -1352,7 +1364,11 @@ async fn handle_move( if !dest_parent_path.is_empty() && let Ok(parent) = folder_service.get_folder_by_path(dest_parent_path).await { - assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?; + assert_owner( + parent.owner_id.as_deref(), + &user.id.to_string(), + dest_parent_path, + )?; } file_management_service .move_file(&file.id, Some(dest_parent_path.to_string())) @@ -1480,7 +1496,11 @@ async fn handle_copy( match folder_service.get_folder_by_path(dest_parent_path).await { Ok(parent) => { // SECURITY: verify destination parent belongs to caller (V-08) - assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?; + assert_owner( + parent.owner_id.as_deref(), + &user.id.to_string(), + dest_parent_path, + )?; Some(parent.id) } Err(_) => None, @@ -1528,7 +1548,11 @@ async fn handle_copy( match folder_service.get_folder_by_path(dest_parent_path).await { Ok(parent) => { // SECURITY: verify destination parent belongs to caller (V-08) - assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?; + assert_owner( + parent.owner_id.as_deref(), + &user.id.to_string(), + dest_parent_path, + )?; Some(parent.id) } Err(_) => None, @@ -1553,7 +1577,11 @@ async fn handle_copy( let folder_result = folder_service.get_folder_by_path(&source_path).await; if let Ok(folder) = folder_result { - assert_owner(folder.owner_id.as_deref(), &user.id.to_string(), &source_path)?; + assert_owner( + folder.owner_id.as_deref(), + &user.id.to_string(), + &source_path, + )?; let recursive = depth != "0"; let dest_folder_name = destination_path @@ -1572,7 +1600,11 @@ async fn handle_copy( match folder_service.get_folder_by_path(dest_parent_path).await { Ok(parent) => { // SECURITY: verify destination parent belongs to caller (V-08) - assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?; + assert_owner( + parent.owner_id.as_deref(), + &user.id.to_string(), + dest_parent_path, + )?; Some(parent.id) } Err(_) => None, @@ -1627,7 +1659,11 @@ async fn handle_copy( match folder_service.get_folder_by_path(dest_parent_path).await { Ok(parent) => { // SECURITY: verify destination parent belongs to caller (V-08) - assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?; + assert_owner( + parent.owner_id.as_deref(), + &user.id.to_string(), + dest_parent_path, + )?; Some(parent.id) } Err(_) => None, diff --git a/src/interfaces/api/handlers/wopi_handler.rs b/src/interfaces/api/handlers/wopi_handler.rs index b3dcd7fc..97403a05 100755 --- a/src/interfaces/api/handlers/wopi_handler.rs +++ b/src/interfaces/api/handlers/wopi_handler.rs @@ -459,17 +459,18 @@ pub async fn get_editor_url( }; // Generate WOPI access token - let (access_token, access_token_ttl) = - match state - .token_service - .generate_token(¶ms.file_id, &user_id.to_string(), &username, can_write) - { - Ok(t) => t, - Err(e) => { - tracing::error!("Failed to generate WOPI token: {}", e); - return StatusCode::INTERNAL_SERVER_ERROR.into_response(); - } - }; + let (access_token, access_token_ttl) = match state.token_service.generate_token( + ¶ms.file_id, + &user_id.to_string(), + username, + can_write, + ) { + Ok(t) => t, + Err(e) => { + tracing::error!("Failed to generate WOPI token: {}", e); + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + }; axum::Json(EditorUrlResponse { editor_url, diff --git a/src/interfaces/api/routes.rs b/src/interfaces/api/routes.rs index aa10a6dd..1c9cc287 100755 --- a/src/interfaces/api/routes.rs +++ b/src/interfaces/api/routes.rs @@ -145,7 +145,10 @@ pub fn create_api_routes(app_state: &Arc) -> Router> { .route("/", get(FileHandler::list_files_query)) .route("/upload", post(FileHandler::upload_file_with_thumbnails)) .route("/{id}", get(FileHandler::download_file)) - .route("/{id}/thumbnail/{size}", get(FileHandler::get_thumbnail).put(FileHandler::upload_thumbnail)) + .route( + "/{id}/thumbnail/{size}", + get(FileHandler::get_thumbnail).put(FileHandler::upload_thumbnail), + ) .route("/{id}/metadata", get(FileHandler::get_file_metadata)) .layer(DefaultBodyLimit::max(10 * 1024 * 1024 * 1024)) // 10 GB for file uploads .with_state(app_state.clone()); diff --git a/src/interfaces/middleware/auth.rs b/src/interfaces/middleware/auth.rs index 2a72ae00..90f56f68 100755 --- a/src/interfaces/middleware/auth.rs +++ b/src/interfaces/middleware/auth.rs @@ -91,16 +91,11 @@ where async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { Ok(OptionalUserId( - parts - .extensions - .get::>() - .map(|cu| cu.id), + parts.extensions.get::>().map(|cu| cu.id), )) } } - - // Error for authentication operations #[derive(Debug, thiserror::Error)] pub enum AuthError { diff --git a/src/interfaces/nextcloud/ocs_handler.rs b/src/interfaces/nextcloud/ocs_handler.rs index a576de58..4cfadada 100755 --- a/src/interfaces/nextcloud/ocs_handler.rs +++ b/src/interfaces/nextcloud/ocs_handler.rs @@ -151,7 +151,10 @@ async fn user_provisioning_response( // Fetch quota from storage usage service let quota: (i64, i64) = match state.storage_usage_service.as_ref() { - Some(service) => match service.get_user_storage_info(uuid::Uuid::parse_str(&user_dto.id).unwrap_or_default()).await { + Some(service) => match service + .get_user_storage_info(uuid::Uuid::parse_str(&user_dto.id).unwrap_or_default()) + .await + { Ok((used, total)) => (used, total), Err(_) => (0, 0), }, diff --git a/src/interfaces/web/mod.rs b/src/interfaces/web/mod.rs index a31377c0..c3f43369 100755 --- a/src/interfaces/web/mod.rs +++ b/src/interfaces/web/mod.rs @@ -20,7 +20,11 @@ pub fn create_web_routes() -> Router> { .parent() .unwrap_or(std::path::Path::new(".")) .join("static-dist"); - if dist.exists() { dist } else { config.static_path.clone() } + if dist.exists() { + dist + } else { + config.static_path.clone() + } } else { config.static_path.clone() };