Merge pull request #188 from zjean/fix/ci-clippy-fmt
fix: resolve clippy and rustfmt CI failures
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,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<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,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<Email> = 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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,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 => {
|
||||
|
||||
@@ -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,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)>(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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<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() {
|
||||
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<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 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<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,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();
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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