fix: resolve clippy warnings and rustfmt issues for CI compliance
Fix all clippy lints (collapsible if, clone on Copy, needless borrow, redundant bindings, unused params) and apply rustfmt across the codebase. Update test mocks to match Uuid-based trait signatures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Generated
+1
-1
@@ -2527,7 +2527,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxicloud"
|
||||
version = "0.5.0"
|
||||
version = "0.5.2"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"async-compression",
|
||||
|
||||
@@ -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<String, String> {
|
||||
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<String, String> {
|
||||
}
|
||||
|
||||
/// 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<String, String> {
|
||||
}
|
||||
|
||||
/// 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,16 +361,18 @@ 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) {
|
||||
if p.extension().is_some_and(|e| e == "json")
|
||||
&& let Ok(src) = fs::read_to_string(&p)
|
||||
{
|
||||
let _ = fs::write(&p, json_minify(&src));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Strip insignificant whitespace outside JSON strings.
|
||||
fn json_minify(source: &str) -> String {
|
||||
@@ -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<String> = 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 <link>s with single bundle ────────────────
|
||||
if t.starts_with("<link") && t.contains("stylesheet") && t.contains("href=\"/css/") {
|
||||
if !css_done {
|
||||
out.push(format!(
|
||||
" <link rel=\"stylesheet\" href=\"{css_path}\">"
|
||||
));
|
||||
out.push(format!(" <link rel=\"stylesheet\" href=\"{css_path}\">"));
|
||||
css_done = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// ── Replace sync theme-init.js with inline <script> ─────────────────
|
||||
if t.starts_with("<script")
|
||||
&& !t.contains("defer")
|
||||
&& t.contains("theme-init")
|
||||
{
|
||||
if t.starts_with("<script") && !t.contains("defer") && t.contains("theme-init") {
|
||||
out.push(format!(" <script>{inline_theme_js}</script>"));
|
||||
continue;
|
||||
}
|
||||
@@ -443,9 +437,7 @@ fn rewrite_index_html(
|
||||
// ── Replace all defer <script>s with single bundle ──────────────────
|
||||
if t.starts_with("<script") && t.contains("defer") && t.contains("src=\"") {
|
||||
if !defer_done {
|
||||
out.push(format!(
|
||||
" <script defer src=\"{js_path}\"></script>"
|
||||
));
|
||||
out.push(format!(" <script defer src=\"{js_path}\"></script>"));
|
||||
defer_done = true;
|
||||
}
|
||||
continue;
|
||||
|
||||
@@ -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<Bytes>;
|
||||
async fn get_cached_thumbnail(&self, file_id: &str, size: ThumbnailSize) -> Option<Bytes>;
|
||||
|
||||
/// Store an externally-generated thumbnail (e.g. client-side video frame).
|
||||
///
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,8 +529,11 @@ 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"))?)
|
||||
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
|
||||
@@ -604,8 +610,11 @@ 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"))?)
|
||||
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
|
||||
@@ -819,8 +828,11 @@ 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"))?)
|
||||
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,8 +857,11 @@ 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"))?)
|
||||
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
|
||||
|
||||
@@ -178,11 +178,11 @@ 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 {
|
||||
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,11 +223,11 @@ 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 {
|
||||
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);
|
||||
|
||||
Ok(false) // permanently deleted
|
||||
|
||||
@@ -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<HashMap<String, (File, String)>>,
|
||||
files: Mutex<HashMap<String, (File, Uuid)>>,
|
||||
}
|
||||
|
||||
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<File, DomainError> {
|
||||
async fn get_file_for_owner(&self, id: &str, owner_id: Uuid) -> Result<File, DomainError> {
|
||||
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<File>, 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<usize, DomainError> {
|
||||
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(),
|
||||
|
||||
@@ -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<crate::domain::entities::file::File>, 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<usize, DomainError> {
|
||||
Ok(0)
|
||||
}
|
||||
@@ -839,7 +839,7 @@ mod tests {
|
||||
async fn get_file_for_owner(
|
||||
&self,
|
||||
id: &str,
|
||||
_owner_id: &str,
|
||||
_owner_id: Uuid,
|
||||
) -> Result<crate::domain::entities::file::File, DomainError> {
|
||||
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<Vec<crate::domain::entities::folder::Folder>, 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<crate::domain::entities::folder::Folder, DomainError> {
|
||||
unimplemented!()
|
||||
|
||||
@@ -566,12 +566,12 @@ 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 {
|
||||
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 => {
|
||||
// Permanently delete the folder
|
||||
let folder_id = item.original_id().to_string();
|
||||
|
||||
@@ -61,10 +61,8 @@ where
|
||||
FW: FileWritePort,
|
||||
FoR: FolderRepository,
|
||||
{
|
||||
async fn get_trash_items(&self, user_id: &str) -> Result<Vec<TrashedItemDto>> {
|
||||
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<Vec<TrashedItemDto>> {
|
||||
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<File>, 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<usize, DomainError> {
|
||||
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<File, DomainError> {
|
||||
// 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<Vec<Folder>, 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<Folder, DomainError> {
|
||||
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);
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -283,7 +283,8 @@ fn split_sql_statements(sql: &str) -> Vec<String> {
|
||||
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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -21,16 +21,7 @@ use crate::domain::services::path_service::StoragePath;
|
||||
type FolderRow = (String, String, String, Option<String>, Uuid, i64, i64);
|
||||
|
||||
/// Type alias for paginated folder rows (includes total_count).
|
||||
type FolderRowPaginated = (
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
Option<String>,
|
||||
Uuid,
|
||||
i64,
|
||||
i64,
|
||||
i64,
|
||||
);
|
||||
type FolderRowPaginated = (String, String, String, Option<String>, Uuid, i64, i64, i64);
|
||||
|
||||
/// Type alias for folder rows with optional user_id.
|
||||
type FolderRowOptUser = (
|
||||
@@ -108,13 +99,13 @@ 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",
|
||||
)
|
||||
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}")))?
|
||||
.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(
|
||||
@@ -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)>(
|
||||
|
||||
@@ -195,8 +195,7 @@ 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")
|
||||
let result = sqlx::query("DELETE FROM storage.shares WHERE id = $1 AND created_by = $2")
|
||||
.bind(id)
|
||||
.bind(user_id)
|
||||
.execute(&*self.db_pool)
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use bytes::Bytes;
|
||||
use image::imageops::FilterType;
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
use image::imageops::FilterType;
|
||||
/**
|
||||
* Thumbnail Generation Service
|
||||
*
|
||||
@@ -236,21 +236,17 @@ 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<Bytes> {
|
||||
pub async fn get_cached_thumbnail(&self, file_id: &str, size: ThumbnailSize) -> Option<Bytes> {
|
||||
// 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() {
|
||||
if let Some(bytes) = self.cache.get(&cache_key).await
|
||||
&& !bytes.is_empty()
|
||||
{
|
||||
return Some(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check disk
|
||||
let thumb_path = self.get_thumbnail_path(file_id, size);
|
||||
@@ -285,18 +281,18 @@ impl ThumbnailService {
|
||||
let jpeg_bytes = tokio::task::spawn_blocking(move || -> Result<Vec<u8>, 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 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
|
||||
{
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Slow path: decode, resize, re-encode to JPEG ─────────
|
||||
let img = image::load_from_memory(&data)
|
||||
@@ -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<Bytes> {
|
||||
async fn get_cached_thumbnail(&self, file_id: &str, size: PortThumbnailSize) -> Option<Bytes> {
|
||||
self.get_cached_thumbnail(file_id, size.into()).await
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -299,8 +299,7 @@ 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
|
||||
let calendars = calendar_service
|
||||
.list_my_calendars(user.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -203,7 +203,8 @@ async fn revoke_device(
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
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)
|
||||
|
||||
@@ -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,9 +340,10 @@ 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 == "*" {
|
||||
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)
|
||||
@@ -347,8 +352,6 @@ impl FileHandler {
|
||||
.unwrap()
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cache-first path (Solution A) ────────────────────────────
|
||||
// Try moka (RAM) → disk before touching the database.
|
||||
@@ -409,8 +412,7 @@ impl FileHandler {
|
||||
.get_thumbnail(&id, thumb_size.into(), &file_path)
|
||||
.await
|
||||
{
|
||||
Ok(data) => {
|
||||
Response::builder()
|
||||
Ok(data) => Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "image/jpeg")
|
||||
.header(header::CONTENT_LENGTH, data.len())
|
||||
@@ -418,12 +420,9 @@ impl FileHandler {
|
||||
.header(header::ETAG, &etag)
|
||||
.body(Body::from(data))
|
||||
.unwrap()
|
||||
.into_response()
|
||||
}
|
||||
Err(err) => {
|
||||
AppError::internal_error(format!("Thumbnail generation failed: {}", err))
|
||||
.into_response()
|
||||
}
|
||||
.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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -459,11 +459,12 @@ 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)
|
||||
{
|
||||
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);
|
||||
|
||||
@@ -145,7 +145,10 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
.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());
|
||||
|
||||
@@ -91,16 +91,11 @@ where
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
Ok(OptionalUserId(
|
||||
parts
|
||||
.extensions
|
||||
.get::<Arc<CurrentUser>>()
|
||||
.map(|cu| cu.id),
|
||||
parts.extensions.get::<Arc<CurrentUser>>().map(|cu| cu.id),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Error for authentication operations
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AuthError {
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
@@ -20,7 +20,11 @@ pub fn create_web_routes() -> Router<Arc<AppState>> {
|
||||
.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()
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user