diff --git a/Dockerfile b/Dockerfile index 72583812..f72e542a 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Stage 1: Cache dependencies -FROM rust:1.93.0-alpine3.23 AS cacher +FROM rust:1.94.0-alpine3.23 AS cacher WORKDIR /app RUN apk --no-cache upgrade && \ apk add --no-cache musl-dev pkgconfig postgresql-dev gcc perl make @@ -10,7 +10,7 @@ RUN mkdir -p src && \ RUSTFLAGS="-C target-cpu=native" cargo build --release && \ rm -rf src target/release/deps/oxicloud* # Stage 2: Build the application -FROM rust:1.93.0-alpine3.23 AS builder +FROM rust:1.94.0-alpine3.23 AS builder WORKDIR /app RUN apk --no-cache upgrade && \ apk add --no-cache musl-dev pkgconfig postgresql-dev gcc perl make diff --git a/db/schema.sql b/db/schema.sql index 9aaa5c4e..3c462d8c 100755 --- a/db/schema.sql +++ b/db/schema.sql @@ -34,7 +34,7 @@ END $BODY$; -- Users table CREATE TABLE IF NOT EXISTS auth.users ( - id VARCHAR(36) PRIMARY KEY, + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), username TEXT UNIQUE NOT NULL, email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, @@ -52,8 +52,8 @@ CREATE INDEX IF NOT EXISTS idx_users_email ON auth.users(email); -- Sessions table for refresh tokens CREATE TABLE IF NOT EXISTS auth.sessions ( - id VARCHAR(36) PRIMARY KEY, - user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, refresh_token TEXT NOT NULL UNIQUE, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, ip_address TEXT, @@ -80,7 +80,7 @@ WHERE NOT revoked AND auth.is_session_active(expires_at); -- File ownership tracking CREATE TABLE IF NOT EXISTS auth.user_files ( id SERIAL PRIMARY KEY, - user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, file_path TEXT NOT NULL, file_id TEXT NOT NULL, size_bytes BIGINT NOT NULL DEFAULT 0, @@ -95,7 +95,7 @@ CREATE INDEX IF NOT EXISTS idx_user_files_file_id ON auth.user_files(file_id); -- User favorites CREATE TABLE IF NOT EXISTS auth.user_favorites ( id SERIAL PRIMARY KEY, - user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, item_id TEXT NOT NULL, item_type TEXT NOT NULL, -- 'file' or 'folder' created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -111,7 +111,7 @@ CREATE INDEX IF NOT EXISTS idx_user_favorites_user_type ON auth.user_favorites(u -- Recent files CREATE TABLE IF NOT EXISTS auth.user_recent_files ( id SERIAL PRIMARY KEY, - user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, item_id TEXT NOT NULL, item_type TEXT NOT NULL, -- 'file' or 'folder' accessed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -137,7 +137,7 @@ CREATE TABLE IF NOT EXISTS auth.admin_settings ( category VARCHAR(50) NOT NULL DEFAULT 'general', is_secret BOOLEAN NOT NULL DEFAULT FALSE, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_by VARCHAR(36) + updated_by UUID ); CREATE INDEX IF NOT EXISTS idx_admin_settings_category ON auth.admin_settings(category); @@ -172,13 +172,13 @@ BEGIN END $BODY$; CREATE TABLE IF NOT EXISTS auth.device_codes ( - id VARCHAR(36) PRIMARY KEY, + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), device_code VARCHAR(128) UNIQUE NOT NULL, user_code VARCHAR(16) UNIQUE NOT NULL, client_name VARCHAR(255) NOT NULL DEFAULT 'Unknown Client', scopes VARCHAR(512) NOT NULL DEFAULT 'webdav,caldav,carddav', status auth.device_code_status NOT NULL DEFAULT 'pending', - user_id VARCHAR(36) REFERENCES auth.users(id) ON DELETE CASCADE, + user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, access_token TEXT, refresh_token TEXT, verification_uri TEXT NOT NULL, @@ -203,8 +203,8 @@ COMMENT ON TABLE auth.device_codes IS 'OAuth 2.0 Device Authorization Grant (RFC -- App Passwords (application-specific passwords for DAV clients with HTTP Basic Auth) CREATE TABLE IF NOT EXISTS auth.app_passwords ( - id VARCHAR(36) PRIMARY KEY, - user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, label VARCHAR(255) NOT NULL, password_hash TEXT NOT NULL, prefix VARCHAR(50) NOT NULL, @@ -231,7 +231,7 @@ CREATE SCHEMA IF NOT EXISTS caldav; CREATE TABLE IF NOT EXISTS caldav.calendars ( id UUID PRIMARY KEY, name TEXT NOT NULL, - owner_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + owner_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, description TEXT, color VARCHAR(9), -- #RRGGBB or #RRGGBBAA is_public BOOLEAN NOT NULL DEFAULT FALSE, @@ -271,7 +271,7 @@ CREATE INDEX IF NOT EXISTS idx_calendar_events_summary_trgm CREATE TABLE IF NOT EXISTS caldav.calendar_shares ( id SERIAL PRIMARY KEY, calendar_id UUID NOT NULL REFERENCES caldav.calendars(id) ON DELETE CASCADE, - user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, access_level VARCHAR(10) NOT NULL DEFAULT 'read', -- read, write, owner created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(calendar_id, user_id) @@ -305,7 +305,7 @@ CREATE SCHEMA IF NOT EXISTS carddav; CREATE TABLE IF NOT EXISTS carddav.address_books ( id UUID PRIMARY KEY, name TEXT NOT NULL, - owner_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + owner_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, description TEXT, color VARCHAR(9), is_public BOOLEAN NOT NULL DEFAULT FALSE, @@ -364,7 +364,7 @@ CREATE INDEX IF NOT EXISTS idx_contacts_phone_text_trgm CREATE TABLE IF NOT EXISTS carddav.address_book_shares ( id SERIAL PRIMARY KEY, address_book_id UUID NOT NULL REFERENCES carddav.address_books(id) ON DELETE CASCADE, - user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, can_write BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(address_book_id, user_id) @@ -444,7 +444,7 @@ CREATE TABLE IF NOT EXISTS storage.folders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, parent_id UUID REFERENCES storage.folders(id) ON DELETE CASCADE, - user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, path TEXT NOT NULL DEFAULT '', lpath ltree NOT NULL DEFAULT '', is_trashed BOOLEAN NOT NULL DEFAULT FALSE, @@ -545,7 +545,7 @@ CREATE TABLE IF NOT EXISTS storage.files ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, folder_id UUID REFERENCES storage.folders(id) ON DELETE CASCADE, - user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, blob_hash VARCHAR(64) NOT NULL, size BIGINT NOT NULL DEFAULT 0, mime_type TEXT NOT NULL DEFAULT 'application/octet-stream', @@ -636,7 +636,7 @@ CREATE TABLE IF NOT EXISTS storage.shares ( permissions_write BOOLEAN NOT NULL DEFAULT FALSE, permissions_reshare BOOLEAN NOT NULL DEFAULT FALSE, created_at BIGINT NOT NULL, -- unix epoch seconds - created_by VARCHAR(36) NOT NULL, + created_by UUID NOT NULL, access_count BIGINT NOT NULL DEFAULT 0 ); diff --git a/doc/PERFORMANCE-ANALYSIS.md b/doc/PERFORMANCE-ANALYSIS.md deleted file mode 100644 index 8b337198..00000000 --- a/doc/PERFORMANCE-ANALYSIS.md +++ /dev/null @@ -1,735 +0,0 @@ -# OxiCloud — Deep Performance Analysis - -> Extreme‑optimization audit of every hot path, allocation pattern, and -> concurrency strategy across 22 source files. - ---- - -## Executive Summary - -OxiCloud is **already well‑architected** for performance: moka lock‑free caches -everywhere, BLAKE3 hashing, dedicated rayon pool for image work, ltree GiST -indexes for subtree queries, streaming I/O, and zero‑copy `Bytes` clones. The -findings below target the **remaining ~15–25 % of allocatable overhead** that -separates "good" from "extreme." - -**Impact tiers:** -- 🔴 **High** — measurable latency or throughput regression on every request -- 🟡 **Medium** — wasteful but amortised across many requests -- 🟢 **Low** — micro‑optimisation, only matters at ≥ 10 k req/s - ---- - -## 1. Avoidable `.clone()` calls - -### 🔴 1a. `CurrentUser` cloned on every authenticated request - -**File:** `src/interfaces/middleware/auth.rs` - -The middleware extracts a `CurrentUser` (4 owned `String` fields) into Axum's -request extensions. Every handler that reads it clones the struct: - -```rust -// auth.rs — CurrentUser has 4 String fields -pub struct CurrentUser { - pub id: String, - pub username: String, - pub email: String, - pub role: String, -} -``` - -**Fix:** Replace with `Arc` in request extensions. All downstream -handlers receive `Arc::clone()` (8‑byte refcount bump) instead of 4 heap -allocations: - -```rust -request.extensions_mut().insert(Arc::new(current_user)); -// handlers: Extension(user): Extension> -``` - -**Estimated saving:** ~160–320 ns per request (4 × String clone of ~20‑byte -UUIDs/emails). - ---- - -### 🟡 1b. `config.clone()` during `CoreServices` construction - -**File:** `src/common/di.rs` - -```rust -// di.rs — CoreServices creation -let core = CoreServices { - config: config.clone(), // AppConfig is large: ~60 fields, many Strings - ... -}; -``` - -`AppConfig` contains ~60 fields including nested structs with owned `String`s. -This only runs at startup, so impact is negligible — but it leaks into any -service that receives `AppConfig` by value instead of `Arc`. - -**Fix:** Pass `Arc` everywhere. Most services already take -`Arc`; unify the remaining call sites. - ---- - -### 🟡 1c. `mime_type.clone()` in file retrieval return paths - -**File:** `src/application/services/file_retrieval_service.rs` - -```rust -// file_retrieval_service.rs — return path -Ok(FileContentDto { - content, - mime_type: mime_type.clone(), // repeated in match arms - ... -}) -``` - -Mime type strings are typically < 30 bytes (`"image/jpeg"`) so each clone is -cheap, but this happens per‑download. Using `Arc` or keeping the MIME as -`&'static str` (from a lookup table of the ~30 common types) would eliminate -the allocation entirely. - ---- - -### 🟡 1d. `file.clone()` in search suggest - -**File:** `src/application/services/search_service.rs` - -```rust -// search_service.rs — suggest() -results.iter().map(|file| { - FileDto::from(file.clone()) // full File entity clone per suggestion -}).collect() -``` - -**Fix:** `FileDto::from(&file)` — take by reference, build DTO fields directly. - ---- - -### 🟡 1e. `target_folder.map(|s| s.to_string())` in batch operations - -**File:** `src/application/services/batch_operations.rs` - -```rust -// batch_operations.rs — copy_files/move_files -let target_folder: Option> = target_folder_id.map(|s| Arc::from(s.as_str())); -// ...per-item: -target_folder.map(|s| s.to_string()) // re-allocates a String from Arc per item -``` - -`Arc` is correctly used to avoid N clones, but the inner closure converts -it back to `String` on each iteration — allocating N identical Strings. - -**Fix:** Accept `Option<&str>` in the downstream service method, or if it -requires `String`, store `Arc` and call `.as_ref()`. - ---- - -## 2. String allocations replaceable by `&str` / `Cow` / `&'static str` - -### 🔴 2a. `DomainError` allocates on every construction - -**File:** `src/domain/errors.rs` - -```rust -// errors.rs -pub struct DomainError { - pub entity_id: Option, // heap alloc - pub message: String, // heap alloc - pub source: Option>, // heap alloc - ... -} - -pub fn not_found(entity_type: &'static str, id: &str) -> Self { - Self { - entity_id: Some(id.to_string()), // alloc - message: format!("{} not found", entity_type), // alloc + format - ... - } -} -``` - -Error paths are not usually "hot," but in OxiCloud many operations pattern- -match on errors to decide control flow (e.g. trash service checks `"not found"` -in error messages via string matching): - -```rust -// trash_service.rs -if format!("{}", e).contains("not found") { ... } -``` - -This is both a performance issue (formatting the error + string search) and a -correctness risk. The `ErrorKind` enum already exists — use it: - -```rust -if matches!(e.kind, ErrorKind::NotFound) { ... } -``` - -**Fix for DomainError allocations:** -- Use `Cow<'static, str>` for `message` (most messages are literals) -- Use `Cow<'_, str>` for `entity_id` (most IDs are passed as `&str`) -- Only allocate when the error crosses an async boundary - ---- - -### 🔴 2b. `compute_relevance` allocates per result - -**File:** `src/application/services/search_service.rs` - -```rust -// search_service.rs -fn compute_relevance(name: &str, query: &str) -> f64 { - let name_lower = name.to_lowercase(); // alloc - let query_lower = query.to_lowercase(); // alloc (same query, every iteration!) - ... -} -``` - -For a search returning 100 results, this creates 200 temporary `String`s. - -**Fix:** Pre-lowercase the query once before the loop; for file names use -`eq_ignore_ascii_case` / `to_ascii_lowercase` (in-place capable) or -`unicase::UniCase`. - ---- - -### 🟡 2c. `enrich_file`/`enrich_folder` in search service - -**File:** `src/application/services/search_service.rs` - -```rust -// search_service.rs — enrich_file per result -enriched.formatted_size = format_bytes(file.size as u64); // format!() alloc -enriched.icon_class = get_icon_class(&file.mime_type); // returns String -enriched.icon_special_class = get_icon_special_class(&file.mime_type); // String -enriched.category = get_category(&file.mime_type); // String -``` - -4 × String allocation per search result. If `get_icon_class` etc. return from -a fixed set, they should return `&'static str`. - ---- - -### 🟡 2d. `target_format.mime_type().to_string()` in transcode service - -**File:** `src/infrastructure/services/image_transcode_service.rs` - -```rust -// image_transcode_service.rs -Ok((transcoded, target_format.mime_type().to_string(), true)) -// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -// mime_type() returns &'static str ("image/webp"), .to_string() allocates -``` - -**Fix:** Change return type to `&'static str` or `Cow<'static, str>`. - ---- - -### 🟡 2e. `file_id.to_string()` in cache key construction (thumbnails) - -**File:** `src/infrastructure/services/thumbnail_service.rs` - -```rust -let cache_key = ThumbnailCacheKey { - file_id: file_id.to_string(), // alloc on each lookup - size: *size, -}; -``` - -Moka's `get()` takes `&K` and hashes it. If `file_id` is already a `String`, -this clone is unnecessary — store `Arc` as key or borrow via `Borrow` -trait. - ---- - -## 3. Vec allocations - -### 🟢 3a. Generally well pre‑sized - -Most `Vec` allocations use `with_capacity()` or rely on `collect()` from -known-size iterators. **No major issues found.** Notable good patterns: - -```rust -// zip_service.rs -let mut files_by_folder: HashMap> = - HashMap::with_capacity(all_folders.len()); - -// batch_operations.rs — uses buffer_unordered, no Vec needed -``` - -### 🟡 3b. `BatchResult` vectors not pre-sized - -```rust -// batch_operations.rs -let mut result = BatchResult { - successful: Vec::new(), // could be Vec::with_capacity(total) - failed: Vec::new(), - ... -}; -``` - -Minor: `Vec::with_capacity(file_ids.len())` for `successful` avoids -reallocations when most operations succeed. - ---- - -## 4. Blocking operations inside async contexts - -### 🔴 4a. `std::env::var()` on every request in rate limiter - -**File:** `src/interfaces/middleware/rate_limit.rs` - -```rust -// rate_limit.rs — extract_client_ip() -fn extract_client_ip(req: &Request) -> String { - let trust_proxy = std::env::var("OXICLOUD_TRUST_PROXY_HEADERS") - .unwrap_or_default(); // BLOCKING SYSCALL per request - ... -} -``` - -`std::env::var()` takes a global lock on glibc's environ and is a blocking -syscall. Called on **every single HTTP request**. - -**Fix:** Read the env var once at startup into `AppConfig` (it already exists -there as `trust_proxy_headers: bool`). Pass the config to the middleware: - -```rust -fn extract_client_ip(req: &Request, trust_proxy: bool) -> String { ... } -``` - ---- - -### 🟡 4b. `ip.to_string()` called twice in rate limiter - -**File:** `src/interfaces/middleware/rate_limit.rs` - -```rust -// rate_limit.rs — check_and_increment -pub fn check_and_increment(&self, ip: &str) -> bool { - let current = self.requests.get(ip); // hashes ip — String lookup OK - // ... later: - self.requests.insert(ip.to_string(), ...); // re-allocates String for key -} -``` - -The `ip` is already a `String` at the call site (`ip.to_string()` in -`extract_client_ip`). This means 2 allocations of the same IP string per -request. - -**Fix:** Take `ip: String` by value, reuse it for insertion. - ---- - -### 🟡 4c. `moka::sync::Cache` in JWT service (sync ops on async path) - -**File:** `src/infrastructure/services/jwt_service.rs` - -```rust -// jwt_service.rs -validation_cache: moka::sync::Cache, -``` - -`moka::sync::Cache` performs eviction inline (not background). On hot paths -this can occasionally block the Tokio thread for µs during eviction scans. -For the JWT cache (50k entries, 30s TTL) this is borderline. - -**Fix:** Switch to `moka::future::Cache` which performs eviction in a -background async task, or keep `sync` but call `run_pending_tasks()` from a -periodic maintenance future. - ---- - -## 5. HashMap hasher opportunities - -### 🟡 5a. `DefaultHasher` in search cache key - -**File:** `src/application/services/search_service.rs` - -```rust -// search_service.rs -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; - -fn cache_key(folder_id: Option<&str>, query: &str, ...) -> u64 { - let mut hasher = DefaultHasher::new(); // SipHash-2-4 - ... - hasher.finish() -} -``` - -SipHash provides HashDoS resistance which is unnecessary for an internal cache -key derived from trusted inputs. Switching to `ahash::AHasher` or `fxhash` -saves ~5 ns per hash (relevant when search results are cached aggressively). - ---- - -### 🟢 5b. Moka caches use their own optimised hasher - -Moka internally uses a fast hasher. No action needed for moka-backed caches. - ---- - -## 6. Lock contention patterns - -### 🟢 Mostly eliminated - -The codebase **correctly** uses: -- `moka` (lock-free segmented map) for all caches -- `tokio::sync::Semaphore` for bounded concurrency (Argon2, thumbnail decode) -- `AtomicU64` for hit/miss counters -- No `RwLock>` patterns - -**One minor note:** The Argon2 semaphore is set to `MAX_CONCURRENT_HASHES = 2`: - -```rust -// share_service.rs -const MAX_CONCURRENT_HASHES: usize = 2; -let hash_semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_HASHES)); -``` - -This is correct for memory safety (~19 MB/hash) but could be a throughput -bottleneck if many users set/verify share passwords concurrently. Consider -making this configurable. - ---- - -## 7. Unnecessary serialization/deserialization - -### 🟢 No major issues found - -DTOs are converted with hand-written `from_entity()` and `From` impls, not -round-tripped through serde. The only serde usage is at the HTTP boundary -(axum's `Json`) which is unavoidable and correct. - ---- - -## 8. Memory copies that could be zero‑copy - -### 🟡 8a. File upload hashes in‑memory content after writing to disk - -**File:** `src/application/services/file_upload_service.rs` - -```rust -// file_upload_service.rs — create_file -let hash = blake3::hash(content); // hashes full &[u8] in memory -// content is also written to temp file... -``` - -For files that fit in memory (the `content: &[u8]` path), the content exists -as a slice and is hashed directly — this is fine. But the same content is then -written to a temp file for dedup, meaning the data is traversed twice (hash + -write). - -**Fix:** Use `blake3::Hasher` as an `io::Write` adapter — hash while writing -to disk in a single pass: - -```rust -let mut hasher = blake3::Hasher::new(); -let mut file = File::create(&temp_path)?; -let mut tee = TeeWriter::new(&mut file, &mut hasher); -tee.write_all(content)?; -let hash = hasher.finalize(); -``` - ---- - -### 🟡 8b. File retrieval accumulates stream into `BytesMut` for cache - -**File:** `src/application/services/file_retrieval_service.rs` - -```rust -// file_retrieval_service.rs — cache miss for files < 10MB -let mut buf = BytesMut::new(); -while let Some(chunk) = stream.next().await { - buf.extend_from_slice(&chunk?); -} -let content = buf.freeze(); // Bytes (O(1) clone) -``` - -This is the expected pattern for building a `Bytes` from a stream. The -`BytesMut` will reallocate as it grows. Pre-sizing from the known file size -would avoid reallocations: - -```rust -let mut buf = BytesMut::with_capacity(file.size as usize); -``` - ---- - -## 9. Database query patterns - -### 🟢 9a. No N+1 queries found - -All multi-entity operations use: -- JOINs (`get_file` joins `storage.files` with `storage.blobs`) -- `COUNT(*) OVER()` window functions for paginated counts (single query) -- ltree `<@` for subtree operations (single indexed scan) -- Bulk SQL (`DELETE ... WHERE folder_id IN (SELECT ...)` for trash/delete) -- CTEs for atomic read-modify (`swap_blob_hash`, `copy_file`) - -This is excellently designed. - ---- - -### 🟡 9b. Dynamic SQL building in search (not prepared) - -**File:** `src/infrastructure/repositories/pg/file_blob_read_repository.rs` - -```rust -// file_blob_read_repository.rs — search_files_paginated -let mut sql = String::with_capacity(512); -sql.push_str("SELECT ... FROM storage.files f JOIN storage.blobs b ..."); -if let Some(_) = criteria.name_contains { sql.push_str(" AND f.name ILIKE ..."); } -if let Some(_) = criteria.mime_type { sql.push_str(" AND f.mime_type = ..."); } -// ... etc -``` - -Dynamic SQL cannot benefit from PostgreSQL's prepared statement cache (each -unique SQL text is parsed/planned separately). For the ~8 common combinations, -consider pre-building the queries or using PG's `PREPARE`/`EXECUTE`. - ---- - -### 🟡 9c. `hash_cache` uses `String` keys - -**File:** `src/infrastructure/repositories/pg/file_blob_read_repository.rs` - -```rust -// file_blob_read_repository.rs -hash_cache: Cache, // file_id → blob_hash -``` - -Both file IDs and blob hashes are UUIDs/hex strings (~36 bytes). Using -`Arc` or a 128-bit UUID type as key would reduce per-entry heap overhead. - ---- - -## 10. Inefficient iteration patterns - -### 🟡 10a. Search results: map then sort (two passes) - -**File:** `src/application/services/search_service.rs` - -```rust -// search_service.rs -let enriched: Vec<_> = results.iter().map(|f| enrich_file(f, query)).collect(); -enriched.sort_by(|a, b| b.relevance.total_cmp(&a.relevance)); -``` - -Two passes: one to enrich (allocating N `EnrichedFileDto`s), another to sort. -Could be combined into a single pass that computes relevance inline and uses -`sort_unstable_by` (avoids allocation for equal-comparison temporaries): - -```rust -let mut enriched: Vec<_> = results.iter().map(|f| enrich_file(f, query)).collect(); -enriched.sort_unstable_by(|a, b| b.relevance.total_cmp(&a.relevance)); -``` - -`sort_unstable_by` is ~20% faster than `sort_by` for non-trivial N. - ---- - -### 🟡 10b. `Uuid::parse_str` called multiple times per operation (trash) - -**File:** `src/application/services/trash_service.rs` - -```rust -// trash_service.rs — restore_from_trash -let trash_uuid = Uuid::parse_str(trash_id)?; -let user_uuid = Uuid::parse_str(user_id)?; -// ... later in delete_permanently, same two parse calls -``` - -UUIDs are parsed from `&str` in every trash method. If the caller already has -validated UUIDs (e.g., from the auth middleware), accept `Uuid` directly to -skip re-parsing. - ---- - -### 🟡 10c. `generic_batch_operation` clones every item for error reporting - -**File:** `src/application/services/batch_operations.rs` - -```rust -// batch_operations.rs -items.into_iter().map(|item| { - let op = operation.clone(); - async move { - let op_result = op(item.clone()).await; // clone just for the error arm - (item, op_result) - } -}) -``` - -`item.clone()` is only needed if the operation fails (to report which item -failed). For success paths this is wasted work. Consider using an index-based -approach or `Arc`. - ---- - -## 11. Dynamic dispatch in hot paths - -### 🟡 11a. `Arc` in `ApplicationServices` - -**File:** `src/common/di.rs` - -```rust -pub struct ApplicationServices { - pub file_use_case_factory: Arc, - ... -} -``` - -Every file operation goes through a `dyn` trait dispatch. The vtable indirect -call costs ~2 ns but — more importantly — prevents inlining and LTO across -the boundary. Since there is only one concrete implementation, using a concrete -type wrapped in `Arc` would allow the compiler to -devirtualise and inline. - ---- - -### 🟡 11b. `Box` in every `DomainError` - -**File:** `src/domain/errors.rs` - -```rust -pub source: Option>, -``` - -Every error with a source allocates a `Box`. In hot error paths (e.g., "file -not found" during cache-miss-then-load), this adds ~30 ns of heap allocation. - -**Fix:** Use a concrete error enum or `anyhow::Error` (which uses a thin -pointer and avoids the double indirection). - ---- - -## 12. Additional findings - -### 🔴 12a. `format!("{}", e).contains("not found")` for error matching - -**File:** `src/application/services/trash_service.rs` - -```rust -// trash_service.rs -Err(e) => { - if format!("{}", e).contains("not found") { ... } -} -``` - -This allocates a `String`, formats the error into it, then does a substring -search. Happens on every trash restore/delete for missing items. The -`DomainError` already has `ErrorKind::NotFound`: - -```rust -if matches!(e.kind(), ErrorKind::NotFound) { ... } -``` - ---- - -### 🟡 12b. Excessive `info!()` logging in trash service - -**File:** `src/application/services/trash_service.rs` - -The trash service has **14 `info!()` calls** per single `restore_from_trash` -operation and **12** per `delete_permanently`. Each `info!` allocates -`format_args!` and traverses the tracing subscriber pipeline. - -**Fix:** Downgrade most to `debug!()` or `trace!()`. Keep one `info!` at the -entry point and one at the exit. - ---- - -### 🟡 12c. `AppConfig::from_env()` reads ~60 env vars sequentially - -**File:** `src/common/config.rs` - -Each `std::env::var()` call acquires a global lock. At startup this is fine, -but if this function were ever called more than once it would be a bottleneck. -Currently only called once — **no action needed** unless hot-reloading is added. - ---- - -### 🟢 12d. `BatchOperationService` takes `AppConfig` by value - -**File:** `src/application/services/batch_operations.rs` - -```rust -pub struct BatchOperationService { - config: AppConfig, // owned, not Arc - ... -} -``` - -At construction, the entire `AppConfig` is cloned. Since this happens once at -startup, impact is negligible, but it's inconsistent with other services that -use `Arc`. - ---- - -## Summary table - -| # | Finding | Severity | Per-request cost | Fix complexity | -|---|---------|----------|-----------------|----------------| -| 1a | `CurrentUser` clone per request | 🔴 High | ~200 ns | Low | -| 2a | `DomainError` heap allocs | 🔴 High | ~60 ns × errors | Medium | -| 2b | `compute_relevance` double lowercase | 🔴 High | ~2 µs × N results | Low | -| 4a | `std::env::var()` per request | 🔴 High | ~500 ns | Low | -| 12a | `format!().contains()` error matching | 🔴 High | ~200 ns | Low | -| 1c | `mime_type.clone()` in retrieval | 🟡 Medium | ~30 ns | Low | -| 1d | `file.clone()` in search suggest | 🟡 Medium | ~100 ns × N | Low | -| 1e | `Arc` → `String` in batch ops | 🟡 Medium | ~30 ns × N items | Low | -| 2c | `enrich_file` 4× String allocs | 🟡 Medium | ~120 ns × N | Medium | -| 2d | `.to_string()` on `&'static str` | 🟡 Medium | ~15 ns | Low | -| 2e | `file_id.to_string()` thumbnail key | 🟡 Medium | ~15 ns | Low | -| 4b | IP string double-alloc in rate limiter | 🟡 Medium | ~30 ns | Low | -| 4c | `moka::sync::Cache` in JWT service | 🟡 Medium | occasional µs | Medium | -| 5a | SipHash for search cache key | 🟡 Medium | ~5 ns | Low | -| 8a | Double-traverse in upload hash | 🟡 Medium | ~ms for large files | Medium | -| 8b | `BytesMut` not pre-sized | 🟡 Medium | reallocations | Low | -| 9b | Dynamic SQL not prepared | 🟡 Medium | ~50 µs parse | High | -| 10a | `sort_by` → `sort_unstable_by` | 🟡 Medium | ~20% slower sort | Low | -| 10b | Repeated `Uuid::parse_str` | 🟡 Medium | ~50 ns × calls | Low | -| 10c | `item.clone()` in generic batch | 🟡 Medium | varies | Medium | -| 11a | `dyn FileUseCaseFactory` | 🟡 Medium | ~2 ns + no inline | Medium | -| 11b | `Box` per error | 🟡 Medium | ~30 ns | High | -| 12b | 14× `info!()` in trash restore | 🟡 Medium | ~1 µs total | Low | -| 3b | `BatchResult` vecs not pre-sized | 🟢 Low | rare realloc | Low | - ---- - -## Recommended priority order - -1. **`std::env::var()` in rate limiter** (4a) — 5-minute fix, blocks every request -2. **`CurrentUser` → `Arc`** (1a) — 30-minute refactor -3. **`format!().contains()` → `ErrorKind` match** (12a) — 15-minute fix -4. **Pre-lowercase query in search** (2b) — 10-minute fix -5. **`DomainError` use `Cow`** (2a) — 2-hour refactor, touches many files -6. **`BytesMut::with_capacity`** (8b) — 1-line fix -7. **Return `&'static str` from icon/mime helpers** (2c, 2d) — 30-minute refactor -8. **IP string reuse in rate limiter** (4b) — 10-minute fix -9. **`sort_unstable_by` in search** (10a) — 1-line fix -10. **Remaining items** — diminishing returns, schedule as convenient - ---- - -## What's already excellent - -The following patterns demonstrate strong performance engineering: - -- **Moka lock-free caches** everywhere (file content, JWT, search, thumbnails, transcode, blob hash) — no `RwLock` anywhere -- **BLAKE3** for content-addressable hashing (~5× faster than SHA-256) with `update_mmap_rayon` for large files -- **Dedicated rayon thread pool** for image transcoding (isolated from Tokio's blocking pool) -- **Streaming I/O** for file downloads (64 KB chunks), ZIP creation (256 KB buffer), and database cursors -- **ltree GiST indexes** for O(log N) subtree operations -- **`COUNT(*) OVER()`** window functions — single query for paginated results + total count -- **Content-addressable dedup** with write-first strategy and atomic blob reference counting -- **`HEX_PREFIXES`** compile-time lookup table avoiding `format!()` in dedup hot path -- **Semaphore-bounded** Argon2 hashing (memory safety) and image decode (back-pressure) -- **`Arc`** usage in batch operations for shared string references -- **CTE-based atomic operations** (`swap_blob_hash`, `copy_file`) — zero round-trip waste -- **PG triggers** for `ref_count` management — no Rust-side bookkeeping overhead diff --git a/src/application/dtos/user_dto.rs b/src/application/dtos/user_dto.rs index 01746b04..06fd605b 100755 --- a/src/application/dtos/user_dto.rs +++ b/src/application/dtos/user_dto.rs @@ -1,6 +1,7 @@ use crate::domain::entities::user::User; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use uuid::Uuid; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UserDto { @@ -80,7 +81,7 @@ pub struct RefreshTokenDto { /// Authenticated current user data (for use in application services) #[derive(Clone, Debug, Serialize, Deserialize)] pub struct CurrentUser { - pub id: String, + pub id: Uuid, pub username: String, pub email: String, pub role: String, diff --git a/src/application/ports/auth_ports.rs b/src/application/ports/auth_ports.rs index c584b419..55ce1549 100755 --- a/src/application/ports/auth_ports.rs +++ b/src/application/ports/auth_ports.rs @@ -3,6 +3,7 @@ use crate::domain::entities::app_password::AppPassword; use crate::domain::entities::device_code::DeviceCode; use crate::domain::entities::session::Session; use crate::domain::entities::user::User; +use uuid::Uuid; // ============================================================================ // Cryptography Ports - Extracted from Domain to maintain Clean Architecture @@ -72,7 +73,7 @@ pub trait UserStoragePort: Send + Sync + 'static { async fn create_user(&self, user: User) -> Result; /// Gets a user by ID - async fn get_user_by_id(&self, id: &str) -> Result; + async fn get_user_by_id(&self, id: Uuid) -> Result; /// Gets a user by username async fn get_user_by_username(&self, username: &str) -> Result; @@ -86,7 +87,7 @@ pub trait UserStoragePort: Send + Sync + 'static { /// Updates only the storage usage of a user async fn update_storage_usage( &self, - user_id: &str, + user_id: Uuid, usage_bytes: i64, ) -> Result<(), DomainError>; @@ -100,10 +101,10 @@ pub trait UserStoragePort: Send + Sync + 'static { async fn list_users_by_role(&self, role: &str) -> Result, DomainError>; /// Deletes a user by their ID - async fn delete_user(&self, user_id: &str) -> Result<(), DomainError>; + async fn delete_user(&self, user_id: Uuid) -> Result<(), DomainError>; /// Changes a user's password - async fn change_password(&self, user_id: &str, password_hash: &str) -> Result<(), DomainError>; + async fn change_password(&self, user_id: Uuid, password_hash: &str) -> Result<(), DomainError>; /// Finds a user by OIDC provider + subject pair async fn get_user_by_oidc_subject( @@ -113,15 +114,15 @@ pub trait UserStoragePort: Send + Sync + 'static { ) -> Result; /// Activates or deactivates a user - async fn set_user_active_status(&self, user_id: &str, active: bool) -> Result<(), DomainError>; + async fn set_user_active_status(&self, user_id: Uuid, active: bool) -> Result<(), DomainError>; /// Changes a user's role - async fn change_role(&self, user_id: &str, role: &str) -> Result<(), DomainError>; + async fn change_role(&self, user_id: Uuid, role: &str) -> Result<(), DomainError>; /// Updates a user's storage quota async fn update_storage_quota( &self, - user_id: &str, + user_id: Uuid, quota_bytes: i64, ) -> Result<(), DomainError>; @@ -197,10 +198,10 @@ pub trait SessionStoragePort: Send + Sync + 'static { ) -> Result; /// Revokes a specific session - async fn revoke_session(&self, session_id: &str) -> Result<(), DomainError>; + async fn revoke_session(&self, session_id: Uuid) -> Result<(), DomainError>; /// Revokes all sessions of a user - async fn revoke_all_user_sessions(&self, user_id: &str) -> Result; + async fn revoke_all_user_sessions(&self, user_id: Uuid) -> Result; } // ============================================================================ @@ -224,10 +225,10 @@ pub trait DeviceCodeStoragePort: Send + Sync + 'static { async fn delete_expired(&self) -> Result; /// List authorized device codes for a user (for UI management) - async fn list_by_user(&self, user_id: &str) -> Result, DomainError>; + async fn list_by_user(&self, user_id: Uuid) -> Result, DomainError>; /// Delete a specific device code by ID (revocation) - async fn delete_by_id(&self, id: &str) -> Result<(), DomainError>; + async fn delete_by_id(&self, id: Uuid) -> Result<(), DomainError>; } // ============================================================================ @@ -240,31 +241,31 @@ pub trait AppPasswordStoragePort: Send + Sync + 'static { async fn create(&self, app_password: AppPassword) -> Result; /// Get all active (non-expired) app passwords for a user. - async fn list_by_user(&self, user_id: &str) -> Result, DomainError>; + async fn list_by_user(&self, user_id: Uuid) -> Result, DomainError>; /// Get a specific app password by ID. - async fn get_by_id(&self, id: &str) -> Result; + async fn get_by_id(&self, id: Uuid) -> Result; /// Get all active app passwords for a user ID (for Basic auth verification). /// This includes the password hash for verification. - async fn get_active_by_user_id(&self, user_id: &str) -> Result, DomainError>; + async fn get_active_by_user_id(&self, user_id: Uuid) -> Result, DomainError>; /// Update the `last_used_at` timestamp after a successful authentication. - async fn touch_last_used(&self, id: &str) -> Result<(), DomainError>; + async fn touch_last_used(&self, id: Uuid) -> Result<(), DomainError>; /// Get active app passwords for a user filtered by token prefix (first 8 chars). /// More efficient than `get_active_by_user_id` when the password prefix is known. async fn get_active_by_user_prefix( &self, - user_id: &str, + user_id: Uuid, prefix: &str, ) -> Result, DomainError>; /// Deactivate (soft-delete) an app password, scoped to the owning user. - async fn revoke(&self, id: &str, user_id: &str) -> Result<(), DomainError>; + async fn revoke(&self, id: Uuid, user_id: Uuid) -> Result<(), DomainError>; /// Delete an app password owned by a specific user. Returns true if found and deleted. - async fn delete_by_user_and_id(&self, id: &str, user_id: &str) -> Result; + async fn delete_by_user_and_id(&self, id: Uuid, user_id: Uuid) -> Result; /// Hard-delete expired/revoked app passwords (cleanup). async fn delete_expired(&self) -> Result; diff --git a/src/application/ports/calendar_ports.rs b/src/application/ports/calendar_ports.rs index ca67bfde..4f8559c6 100755 --- a/src/application/ports/calendar_ports.rs +++ b/src/application/ports/calendar_ports.rs @@ -4,6 +4,7 @@ use crate::application::dtos::calendar_dto::{ }; use crate::common::errors::DomainError; use chrono::{DateTime, Utc}; +use uuid::Uuid; /// Port for external calendar storage mechanisms pub trait CalendarStoragePort: Send + Sync + 'static { @@ -11,7 +12,7 @@ pub trait CalendarStoragePort: Send + Sync + 'static { async fn create_calendar( &self, calendar: CreateCalendarDto, - owner_id: &str, + owner_id: Uuid, ) -> Result; async fn update_calendar( &self, @@ -22,11 +23,11 @@ pub trait CalendarStoragePort: Send + Sync + 'static { async fn get_calendar(&self, calendar_id: &str) -> Result; async fn list_calendars_by_owner( &self, - owner_id: &str, + owner_id: Uuid, ) -> Result, DomainError>; async fn list_calendars_shared_with_user( &self, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError>; async fn list_public_calendars( &self, @@ -36,20 +37,20 @@ pub trait CalendarStoragePort: Send + Sync + 'static { async fn check_calendar_access( &self, calendar_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result; // Calendar sharing async fn share_calendar( &self, calendar_id: &str, - user_id: &str, + user_id: Uuid, access_level: &str, ) -> Result<(), DomainError>; async fn remove_calendar_sharing( &self, calendar_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result<(), DomainError>; async fn get_calendar_shares( &self, @@ -114,22 +115,22 @@ pub trait CalendarUseCase: Send + Sync + 'static { async fn create_calendar( &self, calendar: CreateCalendarDto, - user_id: &str, + user_id: Uuid, ) -> Result; async fn update_calendar( &self, calendar_id: &str, update: UpdateCalendarDto, - user_id: &str, + user_id: Uuid, ) -> Result; - async fn delete_calendar(&self, calendar_id: &str, user_id: &str) -> Result<(), DomainError>; + async fn delete_calendar(&self, calendar_id: &str, user_id: Uuid) -> Result<(), DomainError>; async fn get_calendar( &self, calendar_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result; - async fn list_my_calendars(&self, user_id: &str) -> Result, DomainError>; - async fn list_shared_calendars(&self, user_id: &str) -> Result, DomainError>; + async fn list_my_calendars(&self, user_id: Uuid) -> Result, DomainError>; + async fn list_shared_calendars(&self, user_id: Uuid) -> Result, DomainError>; async fn list_public_calendars( &self, limit: Option, @@ -140,57 +141,57 @@ pub trait CalendarUseCase: Send + Sync + 'static { async fn share_calendar( &self, calendar_id: &str, - target_user_id: &str, + target_user_id: Uuid, access_level: &str, - caller_user_id: &str, + caller_user_id: Uuid, ) -> Result<(), DomainError>; async fn remove_calendar_sharing( &self, calendar_id: &str, - target_user_id: &str, - caller_user_id: &str, + target_user_id: Uuid, + caller_user_id: Uuid, ) -> Result<(), DomainError>; async fn get_calendar_shares( &self, calendar_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError>; // Event operations async fn create_event( &self, event: CreateEventDto, - user_id: &str, + user_id: Uuid, ) -> Result; async fn create_event_from_ical( &self, event: CreateEventICalDto, - user_id: &str, + user_id: Uuid, ) -> Result; async fn update_event( &self, event_id: &str, update: UpdateEventDto, - user_id: &str, + user_id: Uuid, ) -> Result; - async fn delete_event(&self, event_id: &str, user_id: &str) -> Result<(), DomainError>; + async fn delete_event(&self, event_id: &str, user_id: Uuid) -> Result<(), DomainError>; async fn get_event( &self, event_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result; async fn list_events( &self, calendar_id: &str, limit: Option, offset: Option, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError>; async fn get_events_in_range( &self, calendar_id: &str, start: DateTime, end: DateTime, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError>; } diff --git a/src/application/ports/carddav_ports.rs b/src/application/ports/carddav_ports.rs index c46beafc..94acb1e5 100755 --- a/src/application/ports/carddav_ports.rs +++ b/src/application/ports/carddav_ports.rs @@ -7,6 +7,7 @@ use crate::application::dtos::contact_dto::{ GroupMembershipDto, UpdateContactDto, UpdateContactGroupDto, }; use crate::common::errors::DomainError; +use uuid::Uuid; pub type CardDavRepositoryError = DomainError; @@ -24,16 +25,16 @@ pub trait AddressBookUseCase: Send + Sync + 'static { async fn delete_address_book( &self, address_book_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result<(), DomainError>; async fn get_address_book( &self, address_book_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result; async fn list_user_address_books( &self, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError>; async fn list_public_address_books(&self) -> Result, DomainError>; @@ -41,17 +42,17 @@ pub trait AddressBookUseCase: Send + Sync + 'static { async fn share_address_book( &self, dto: ShareAddressBookDto, - user_id: &str, + user_id: Uuid, ) -> Result<(), DomainError>; async fn unshare_address_book( &self, dto: UnshareAddressBookDto, - user_id: &str, + user_id: Uuid, ) -> Result<(), DomainError>; async fn get_address_book_shares( &self, address_book_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError>; } @@ -67,19 +68,19 @@ pub trait ContactUseCase: Send + Sync + 'static { contact_id: &str, update: UpdateContactDto, ) -> Result; - async fn delete_contact(&self, contact_id: &str, user_id: &str) -> Result<(), DomainError>; - async fn get_contact(&self, contact_id: &str, user_id: &str) + async fn delete_contact(&self, contact_id: &str, user_id: Uuid) -> Result<(), DomainError>; + async fn get_contact(&self, contact_id: &str, user_id: Uuid) -> Result; async fn list_contacts( &self, address_book_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError>; async fn search_contacts( &self, address_book_id: &str, query: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError>; // Contact Group operations @@ -92,49 +93,49 @@ pub trait ContactUseCase: Send + Sync + 'static { group_id: &str, update: UpdateContactGroupDto, ) -> Result; - async fn delete_group(&self, group_id: &str, user_id: &str) -> Result<(), DomainError>; + async fn delete_group(&self, group_id: &str, user_id: Uuid) -> Result<(), DomainError>; async fn get_group( &self, group_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result; async fn list_groups( &self, address_book_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError>; // Group membership async fn add_contact_to_group( &self, dto: GroupMembershipDto, - user_id: &str, + user_id: Uuid, ) -> Result<(), DomainError>; async fn remove_contact_from_group( &self, dto: GroupMembershipDto, - user_id: &str, + user_id: Uuid, ) -> Result<(), DomainError>; async fn list_contacts_in_group( &self, group_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError>; async fn list_groups_for_contact( &self, contact_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError>; // vCard operations async fn get_contact_vcard( &self, contact_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result; async fn get_contacts_as_vcards( &self, address_book_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError>; } diff --git a/src/application/ports/chunked_upload_ports.rs b/src/application/ports/chunked_upload_ports.rs index 1f3082ce..bbd40a97 100755 --- a/src/application/ports/chunked_upload_ports.rs +++ b/src/application/ports/chunked_upload_ports.rs @@ -8,6 +8,7 @@ use crate::common::errors::DomainError; use bytes::Bytes; use serde::Serialize; use std::path::PathBuf; +use uuid::Uuid; /// Default chunk size (5 MB) — optimised for parallel transfers. pub const DEFAULT_CHUNK_SIZE: usize = 5 * 1024 * 1024; @@ -59,7 +60,7 @@ pub trait ChunkedUploadPort: Send + Sync + 'static { /// total number of chunks, and expiration timestamp. async fn create_session( &self, - user_id: &str, + user_id: Uuid, filename: String, folder_id: Option, content_type: String, @@ -73,7 +74,7 @@ pub trait ChunkedUploadPort: Send + Sync + 'static { async fn upload_chunk( &self, upload_id: &str, - user_id: &str, + user_id: Uuid, chunk_index: usize, data: Bytes, checksum: Option, @@ -83,7 +84,7 @@ pub trait ChunkedUploadPort: Send + Sync + 'static { async fn get_status( &self, upload_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result; /// Assemble all chunks into the final file. @@ -94,14 +95,14 @@ pub trait ChunkedUploadPort: Send + Sync + 'static { async fn complete_upload( &self, upload_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result<(PathBuf, String, Option, String, u64, String), DomainError>; /// Finalize upload: clean up the session and temporary files. - async fn finalize_upload(&self, upload_id: &str, user_id: &str) -> Result<(), DomainError>; + async fn finalize_upload(&self, upload_id: &str, user_id: Uuid) -> Result<(), DomainError>; /// Cancel an upload and clean up all temporary data. - async fn cancel_upload(&self, upload_id: &str, user_id: &str) -> Result<(), DomainError>; + async fn cancel_upload(&self, upload_id: &str, user_id: Uuid) -> Result<(), DomainError>; /// Check if a file size qualifies for chunked upload. fn should_use_chunked(&self, size: u64) -> bool; diff --git a/src/application/ports/favorites_ports.rs b/src/application/ports/favorites_ports.rs index 11b1d17c..f3b2bcc1 100755 --- a/src/application/ports/favorites_ports.rs +++ b/src/application/ports/favorites_ports.rs @@ -1,32 +1,34 @@ use std::collections::HashSet; +use uuid::Uuid; + use crate::application::dtos::favorites_dto::{BatchFavoritesResult, FavoriteItemDto}; use crate::common::errors::Result; /// Defines operations for managing user favorites pub trait FavoritesUseCase: Send + Sync { /// Get all favorites for a user - async fn get_favorites(&self, user_id: &str) -> Result>; + async fn get_favorites(&self, user_id: Uuid) -> Result>; /// Add an item to user's favorites - async fn add_to_favorites(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>; + async fn add_to_favorites(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<()>; /// Remove an item from user's favorites async fn remove_from_favorites( &self, - user_id: &str, + user_id: Uuid, item_id: &str, item_type: &str, ) -> Result; /// Check if an item is in user's favorites - async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result; + async fn is_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result; /// Add multiple items to favorites in a single transaction. /// Returns enriched favourites list so the client can replace its cache. async fn batch_add_to_favorites( &self, - user_id: &str, + user_id: Uuid, items: &[(String, String)], ) -> Result; @@ -34,7 +36,7 @@ pub trait FavoritesUseCase: Send + Sync { /// Returns the set of item_ids that are favorites. async fn batch_check_favorites( &self, - user_id: &str, + user_id: Uuid, item_ids: &[(&str, &str)], // (item_id, item_type) pairs ) -> Result>; } @@ -50,26 +52,26 @@ pub trait FavoritesUseCase: Send + Sync { /// lives in `infrastructure::repositories::pg`. pub trait FavoritesRepositoryPort: Send + Sync + 'static { /// Gets all favorites for a user. - async fn get_favorites(&self, user_id: &str) -> Result>; + async fn get_favorites(&self, user_id: Uuid) -> Result>; /// Adds an item to favorites. Returns `Ok(())` if it already existed (idempotent). - async fn add_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>; + async fn add_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<()>; /// Removes an item from favorites. Returns `true` if it existed. - async fn remove_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result; + async fn remove_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result; /// Checks if an item is in favorites. - async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result; + async fn is_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result; /// Insert multiple items in a single transaction. /// Returns the number of rows actually inserted (ignoring duplicates). - async fn add_favorites_batch(&self, user_id: &str, items: &[(String, String)]) -> Result; + async fn add_favorites_batch(&self, user_id: Uuid, items: &[(String, String)]) -> Result; /// Check which of the given item IDs are favorites for this user. /// Returns the set of item_ids that are favorites. async fn batch_check_favorites( &self, - user_id: &str, + user_id: Uuid, item_ids: &[(&str, &str)], // (item_id, item_type) pairs ) -> Result>; } diff --git a/src/application/ports/file_ports.rs b/src/application/ports/file_ports.rs index 43228a5d..ceea8134 100755 --- a/src/application/ports/file_ports.rs +++ b/src/application/ports/file_ports.rs @@ -3,6 +3,7 @@ use futures::Stream; use std::path::Path; use std::pin::Pin; use std::sync::Arc; +use uuid::Uuid; use crate::application::dtos::file_dto::FileDto; use crate::application::ports::storage_ports::CopyFolderTreeResult; @@ -116,7 +117,7 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static { /// /// Returns `NotFound` if the file does not exist **or** belongs to /// another user. All user-facing handlers should use this method. - async fn get_file_owned(&self, id: &str, caller_id: &str) -> Result; + async fn get_file_owned(&self, id: &str, caller_id: Uuid) -> Result; /// Gets a file by its path (for WebDAV) async fn get_file_by_path(&self, path: &str) -> Result; @@ -131,7 +132,7 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static { async fn list_files_owned( &self, folder_id: Option<&str>, - owner_id: &str, + owner_id: Uuid, ) -> Result, DomainError>; /// Gets file content as a stream (for large files) @@ -144,7 +145,7 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static { async fn get_file_stream_owned( &self, id: &str, - caller_id: &str, + caller_id: Uuid, ) -> Result> + Send>, DomainError>; /// Optimized multi-tier download. @@ -166,7 +167,7 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static { async fn get_file_optimized_owned( &self, id: &str, - caller_id: &str, + caller_id: Uuid, accept_webp: bool, prefer_original: bool, ) -> Result<(FileDto, OptimizedFileContent), DomainError>; @@ -198,7 +199,7 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static { async fn get_file_range_stream_owned( &self, id: &str, - caller_id: &str, + caller_id: Uuid, start: u64, end: Option, ) -> Result> + Send>, DomainError>; @@ -238,14 +239,15 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static { async fn list_files_batch_for_owner( &self, folder_id: Option<&str>, - owner_id: &str, + owner_id: Uuid, offset: i64, limit: i64, ) -> Result, DomainError> { let all = self.list_files_batch(folder_id, offset, limit).await?; + let owner_str = owner_id.to_string(); Ok(all .into_iter() - .filter(|f| f.owner_id.as_deref().is_some_and(|o| o == owner_id)) + .filter(|f| f.owner_id.as_deref().is_some_and(|o| o == owner_str)) .collect()) } } @@ -267,7 +269,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static { async fn move_file_owned( &self, file_id: &str, - caller_id: &str, + caller_id: Uuid, folder_id: Option, ) -> Result; @@ -282,7 +284,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static { async fn copy_file_owned( &self, file_id: &str, - caller_id: &str, + caller_id: Uuid, target_folder_id: Option, ) -> Result; @@ -293,7 +295,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static { async fn rename_file_owned( &self, file_id: &str, - caller_id: &str, + caller_id: Uuid, new_name: &str, ) -> Result; @@ -301,7 +303,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static { async fn delete_file(&self, id: &str) -> Result<(), DomainError>; /// Deletes a file, enforcing that `caller_id` is the owner. - async fn delete_file_owned(&self, id: &str, caller_id: &str) -> Result<(), DomainError>; + async fn delete_file_owned(&self, id: &str, caller_id: Uuid) -> Result<(), DomainError>; /// Smart delete: trash-first with dedup reference cleanup. /// @@ -310,7 +312,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static { /// 3. Decrements the dedup reference count for the content hash. /// /// Returns `Ok(true)` when trashed, `Ok(false)` when permanently deleted. - async fn delete_with_cleanup(&self, id: &str, user_id: &str) -> Result; + async fn delete_with_cleanup(&self, id: &str, user_id: Uuid) -> Result; /// Copies an entire folder subtree atomically (WebDAV COPY Depth: infinity). /// diff --git a/src/application/ports/inbound.rs b/src/application/ports/inbound.rs index 2a04da7f..3a7380ac 100755 --- a/src/application/ports/inbound.rs +++ b/src/application/ports/inbound.rs @@ -1,5 +1,7 @@ use std::sync::Arc; +use uuid::Uuid; + use crate::application::dtos::folder_dto::{ CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto, }; @@ -20,7 +22,7 @@ pub trait FolderUseCase: Send + Sync + 'static { /// /// Returns `NotFound` if the folder does not exist **or** belongs to /// another user. All user-facing handlers should use this method. - async fn get_folder_owned(&self, id: &str, caller_id: &str) -> Result; + async fn get_folder_owned(&self, id: &str, caller_id: Uuid) -> Result; /// Gets a folder by its path async fn get_folder_by_path(&self, path: &str) -> Result; @@ -33,7 +35,7 @@ pub trait FolderUseCase: Send + Sync + 'static { async fn list_folders_for_owner( &self, parent_id: Option<&str>, - owner_id: &str, + owner_id: Uuid, ) -> Result, DomainError>; /// Lists folders with pagination @@ -47,7 +49,7 @@ pub trait FolderUseCase: Send + Sync + 'static { async fn list_folders_for_owner_paginated( &self, parent_id: Option<&str>, - owner_id: &str, + owner_id: Uuid, pagination: &crate::application::dtos::pagination::PaginationRequestDto, ) -> Result, DomainError>; @@ -56,7 +58,7 @@ pub trait FolderUseCase: Send + Sync + 'static { &self, id: &str, dto: RenameFolderDto, - caller_id: &str, + caller_id: Uuid, ) -> Result; /// Moves a folder to another parent (ownership verified against caller_id) @@ -64,16 +66,16 @@ pub trait FolderUseCase: Send + Sync + 'static { &self, id: &str, dto: MoveFolderDto, - caller_id: &str, + caller_id: Uuid, ) -> Result; /// Deletes a folder (ownership verified against caller_id) - async fn delete_folder(&self, id: &str, caller_id: &str) -> Result<(), DomainError>; + async fn delete_folder(&self, id: &str, caller_id: Uuid) -> Result<(), DomainError>; /// Creates a root-level home folder for a user during registration. async fn create_home_folder( &self, - user_id: &str, + user_id: Uuid, name: String, ) -> Result; @@ -103,7 +105,7 @@ pub trait SearchUseCase: Send + Sync + 'static { async fn search( &self, criteria: SearchCriteriaDto, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError>; /// Returns quick suggestions for autocomplete (lightweight, fast). diff --git a/src/application/ports/recent_ports.rs b/src/application/ports/recent_ports.rs index 81f3b12a..91d6cfd7 100755 --- a/src/application/ports/recent_ports.rs +++ b/src/application/ports/recent_ports.rs @@ -1,3 +1,5 @@ +use uuid::Uuid; + use crate::application::dtos::recent_dto::RecentItemDto; use crate::common::errors::Result; @@ -6,24 +8,24 @@ pub trait RecentItemsUseCase: Send + Sync { /// Get all recent items for a user async fn get_recent_items( &self, - user_id: &str, + user_id: Uuid, limit: Option, ) -> Result>; /// Record access to an item - async fn record_item_access(&self, user_id: &str, item_id: &str, item_type: &str) + async fn record_item_access(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<()>; /// Remove an item from recents async fn remove_from_recent( &self, - user_id: &str, + user_id: Uuid, item_id: &str, item_type: &str, ) -> Result; /// Clear the entire recent items list - async fn clear_recent_items(&self, user_id: &str) -> Result<()>; + async fn clear_recent_items(&self, user_id: Uuid) -> Result<()>; } // ───────────────────────────────────────────────────── @@ -36,17 +38,17 @@ pub trait RecentItemsUseCase: Send + Sync { /// `RecentService` does not depend directly on `PgPool`. pub trait RecentItemsRepositoryPort: Send + Sync + 'static { /// Gets the latest recent items for a user (ordered by date desc). - async fn get_recent_items(&self, user_id: &str, limit: i32) -> Result>; + async fn get_recent_items(&self, user_id: Uuid, limit: i32) -> Result>; /// Records/updates access to an item (upsert by user+item+type). - async fn upsert_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>; + async fn upsert_access(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<()>; /// Removes an item from recents. Returns `true` if it existed. - async fn remove_item(&self, user_id: &str, item_id: &str, item_type: &str) -> Result; + async fn remove_item(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result; /// Removes all recent items for a user. - async fn clear_all(&self, user_id: &str) -> Result<()>; + async fn clear_all(&self, user_id: Uuid) -> Result<()>; /// Removes items exceeding `max_items` (the oldest ones). - async fn prune(&self, user_id: &str, max_items: i32) -> Result<()>; + async fn prune(&self, user_id: Uuid, max_items: i32) -> Result<()>; } diff --git a/src/application/ports/share_ports.rs b/src/application/ports/share_ports.rs index 0bcc1592..f6b6d151 100755 --- a/src/application/ports/share_ports.rs +++ b/src/application/ports/share_ports.rs @@ -1,3 +1,5 @@ +use uuid::Uuid; + use crate::{ application::dtos::{ pagination::PaginatedResponseDto, @@ -11,12 +13,12 @@ pub trait ShareUseCase: Send + Sync + 'static { /// Create a new shared link for a file or folder async fn create_shared_link( &self, - user_id: &str, + user_id: Uuid, dto: CreateShareDto, ) -> Result; /// Get a shared link by its ID (ownership-verified) - async fn get_shared_link(&self, id: &str, requester_id: &str) -> Result; + async fn get_shared_link(&self, id: Uuid, requester_id: Uuid) -> Result; /// Get a shared link by its token (for access by non-users) async fn get_shared_link_by_token(&self, token: &str) -> Result; @@ -26,24 +28,24 @@ pub trait ShareUseCase: Send + Sync + 'static { &self, item_id: &str, item_type: &ShareItemType, - requester_id: &str, + requester_id: Uuid, ) -> Result, DomainError>; /// Update a shared link (ownership-verified) async fn update_shared_link( &self, - id: &str, - requester_id: &str, + id: Uuid, + requester_id: Uuid, dto: UpdateShareDto, ) -> Result; /// Delete a shared link (ownership-verified) - async fn delete_shared_link(&self, id: &str, requester_id: &str) -> Result<(), DomainError>; + async fn delete_shared_link(&self, id: Uuid, requester_id: Uuid) -> Result<(), DomainError>; /// Get all shared links created by a specific user async fn get_user_shared_links( &self, - user_id: &str, + user_id: Uuid, page: usize, per_page: usize, ) -> Result, DomainError>; @@ -77,19 +79,19 @@ pub trait ShareStoragePort: Send + Sync + 'static { /// (prevents share-ID enumeration). async fn find_share_by_id_for_user( &self, - id: &str, - user_id: &str, + id: Uuid, + user_id: Uuid, ) -> Result; /// Delete a share only if it belongs to the given user. - async fn delete_share_for_user(&self, id: &str, user_id: &str) -> Result<(), DomainError>; + async fn delete_share_for_user(&self, id: Uuid, user_id: Uuid) -> Result<(), DomainError>; /// Find shares for a specific item that belong to the given user. async fn find_shares_by_item_for_user( &self, item_id: &str, item_type: &ShareItemType, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError>; async fn update_share( @@ -99,7 +101,7 @@ pub trait ShareStoragePort: Send + Sync + 'static { async fn find_shares_by_user( &self, - user_id: &str, + user_id: Uuid, offset: usize, limit: usize, ) -> Result<(Vec, usize), DomainError>; diff --git a/src/application/ports/storage_ports.rs b/src/application/ports/storage_ports.rs index 2b6a0332..7fed9628 100755 --- a/src/application/ports/storage_ports.rs +++ b/src/application/ports/storage_ports.rs @@ -3,6 +3,7 @@ use futures::Stream; use serde_json::Value; use std::path::PathBuf; use std::pin::Pin; +use uuid::Uuid; use crate::application::dtos::search_dto::SearchCriteriaDto; use crate::common::errors::DomainError; @@ -33,13 +34,13 @@ pub trait FileReadPort: Send + Sync + 'static { /// Returns `NotFound` if the file does not exist **or** belongs to a /// different user. This is the primary IDOR-safe accessor — handlers /// serving end-user requests should always prefer this over `get_file`. - async fn get_file_for_owner(&self, id: &str, owner_id: &str) -> Result; + async fn get_file_for_owner(&self, id: &str, owner_id: Uuid) -> Result; /// Verifies that the file identified by `id` belongs to `owner_id`. /// /// Returns `Ok(())` on success or `NotFound` when the file does not /// exist or belongs to another user. - async fn verify_file_owner(&self, id: &str, owner_id: &str) -> Result<(), DomainError> { + async fn verify_file_owner(&self, id: &str, owner_id: Uuid) -> Result<(), DomainError> { self.get_file_for_owner(id, owner_id).await.map(|_| ()) } @@ -53,12 +54,13 @@ pub trait FileReadPort: Send + Sync + 'static { async fn list_files_for_owner( &self, folder_id: Option<&str>, - owner_id: &str, + owner_id: Uuid, ) -> Result, DomainError> { let all = self.list_files(folder_id).await?; + let owner_str = owner_id.to_string(); Ok(all .into_iter() - .filter(|f| f.owner_id().is_some_and(|o| o == owner_id)) + .filter(|f| f.owner_id().is_some_and(|o| o == owner_str)) .collect()) } @@ -134,15 +136,16 @@ pub trait FileReadPort: Send + Sync + 'static { async fn list_files_batch_for_owner( &self, folder_id: Option<&str>, - owner_id: &str, + owner_id: Uuid, offset: i64, limit: i64, ) -> Result, DomainError> { // Default: filter in-memory (repos should override with SQL) let all = self.list_files_batch(folder_id, offset, limit).await?; + let owner_str = owner_id.to_string(); Ok(all .into_iter() - .filter(|f| f.owner_id().is_some_and(|o| o == owner_id)) + .filter(|f| f.owner_id().is_some_and(|o| o == owner_str)) .collect()) } @@ -175,7 +178,7 @@ pub trait FileReadPort: Send + Sync + 'static { &self, folder_id: Option<&str>, criteria: &SearchCriteriaDto, - user_id: &str, + user_id: Uuid, ) -> Result<(Vec, usize), DomainError>; /// Search files recursively in a folder subtree using ltree. @@ -190,7 +193,7 @@ pub trait FileReadPort: Send + Sync + 'static { &self, root_folder_id: Option<&str>, criteria: &SearchCriteriaDto, - user_id: &str, + user_id: Uuid, ) -> Result<(Vec, usize), DomainError> { // Default: delegate to paginated search (non-recursive fallback) self.search_files_paginated(root_folder_id, criteria, user_id) @@ -204,7 +207,7 @@ pub trait FileReadPort: Send + Sync + 'static { &self, folder_id: Option<&str>, criteria: &SearchCriteriaDto, - user_id: &str, + user_id: Uuid, ) -> Result; /// Return up to `limit` files whose name contains `query` (case-insensitive). @@ -359,7 +362,7 @@ pub trait FileWritePort: Send + Sync + 'static { /// Secondary port for storage usage management pub trait StorageUsagePort: Send + Sync + 'static { /// Updates storage usage statistics for a user - async fn update_user_storage_usage(&self, user_id: &str) -> Result; + async fn update_user_storage_usage(&self, user_id: Uuid) -> Result; /// Updates storage usage statistics for a user, looked up by username async fn update_user_storage_usage_by_username( @@ -375,12 +378,12 @@ pub trait StorageUsagePort: Send + Sync + 'static { /// descriptive message otherwise. async fn check_storage_quota( &self, - user_id: &str, + user_id: Uuid, additional_bytes: u64, ) -> Result<(), DomainError>; /// Returns (used_bytes, quota_bytes) for a user. - async fn get_user_storage_info(&self, user_id: &str) -> Result<(i64, i64), DomainError>; + async fn get_user_storage_info(&self, user_id: Uuid) -> Result<(i64, i64), DomainError>; } /// Generic storage service interface for calendar and contact services diff --git a/src/application/ports/trash_ports.rs b/src/application/ports/trash_ports.rs index b459f981..60bfa07a 100755 --- a/src/application/ports/trash_ports.rs +++ b/src/application/ports/trash_ports.rs @@ -1,20 +1,22 @@ +use uuid::Uuid; + use crate::application::dtos::trash_dto::TrashedItemDto; use crate::common::errors::Result; /// Port for trash-related use cases pub trait TrashUseCase: Send + Sync { /// List items in the user's trash - async fn get_trash_items(&self, user_id: &str) -> Result>; + async fn get_trash_items(&self, user_id: Uuid) -> Result>; /// Move a file or folder to trash - 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<()>; /// Restore an item from trash to its original location - async fn restore_item(&self, trash_id: &str, user_id: &str) -> Result<()>; + async fn restore_item(&self, trash_id: &str, user_id: Uuid) -> Result<()>; /// Permanently delete an item from trash - async fn delete_permanently(&self, trash_id: &str, user_id: &str) -> Result<()>; + async fn delete_permanently(&self, trash_id: &str, user_id: Uuid) -> Result<()>; /// Empty the trash for a specific user - async fn empty_trash(&self, user_id: &str) -> Result<()>; + async fn empty_trash(&self, user_id: Uuid) -> Result<()>; } diff --git a/src/application/services/admin_settings_service.rs b/src/application/services/admin_settings_service.rs index c2accbad..c4a88932 100755 --- a/src/application/services/admin_settings_service.rs +++ b/src/application/services/admin_settings_service.rs @@ -1,4 +1,5 @@ use std::sync::Arc; +use uuid::Uuid; use crate::application::dtos::settings_dto::{ OidcSettingsDto, OidcTestResultDto, SaveOidcSettingsDto, TestOidcConnectionDto, @@ -181,7 +182,7 @@ impl AdminSettingsService { pub async fn save_oidc_settings( &self, dto: SaveOidcSettingsDto, - updated_by: &str, + updated_by: Uuid, ) -> Result<(), DomainError> { let cat = "oidc"; let by = Some(updated_by); @@ -366,7 +367,7 @@ impl AdminSettingsService { } /// Mark the system as initialized after the first admin is created. - pub async fn mark_system_initialized(&self, admin_user_id: &str) -> Result<(), DomainError> { + pub async fn mark_system_initialized(&self, admin_user_id: Uuid) -> Result<(), DomainError> { self.settings_repo .set( "system_initialized", @@ -384,7 +385,7 @@ impl AdminSettingsService { /// initialized (the caller "won" the race), or `Ok(false)` if another /// request already did it. This eliminates the race-condition window /// between `is_system_initialized()` and `mark_system_initialized()`. - pub async fn try_claim_initialization(&self, admin_user_id: &str) -> Result { + pub async fn try_claim_initialization(&self, admin_user_id: Uuid) -> Result { self.settings_repo .try_claim_initialization(admin_user_id) .await @@ -412,7 +413,7 @@ impl AdminSettingsService { pub async fn set_registration_enabled( &self, enabled: bool, - updated_by: &str, + updated_by: Uuid, ) -> Result<(), DomainError> { self.settings_repo .set( diff --git a/src/application/services/app_password_service.rs b/src/application/services/app_password_service.rs index bf157c1a..98459f63 100755 --- a/src/application/services/app_password_service.rs +++ b/src/application/services/app_password_service.rs @@ -17,6 +17,7 @@ use moka::future::Cache; use rand_core::RngCore; use std::sync::Arc; use std::time::Duration as StdDuration; +use uuid::Uuid; /// App password token length (32 random alphanumeric chars after prefix). const TOKEN_LENGTH: usize = 32; @@ -41,7 +42,7 @@ const BASIC_AUTH_CACHE_MAX_ENTRIES: u64 = 10_000; /// Cached identity returned after a successful Basic Auth verification. #[derive(Clone)] struct CachedBasicAuthResult { - user_id: String, + user_id: Uuid, username: String, email: String, role: String, @@ -118,7 +119,7 @@ impl AppPasswordService { /// Returns the response DTO that includes the plain-text password (shown only once). pub async fn create( &self, - user_id: &str, + user_id: Uuid, request: CreateAppPasswordRequestDto, ) -> Result { // Validate label @@ -147,7 +148,7 @@ impl AppPasswordService { // Create entity let app_password = AppPassword::new( - user_id.to_string(), + user_id, label.clone(), password_hash, prefix.clone(), @@ -165,7 +166,7 @@ impl AppPasswordService { ); Ok(AppPasswordCreatedResponseDto { - id: saved.id, + id: saved.id.to_string(), label, password: plain_token, username: username.clone(), @@ -200,7 +201,7 @@ impl AppPasswordService { } /// List all app passwords for a user (excludes plain-text passwords). - pub async fn list(&self, user_id: &str) -> Result { + pub async fn list(&self, user_id: Uuid) -> Result { let passwords = self.repo.list_by_user(user_id).await?; let total = passwords.len(); @@ -209,7 +210,7 @@ impl AppPasswordService { .map(|ap| { let is_active = ap.active && !ap.is_expired(); AppPasswordSummaryDto { - id: ap.id, + id: ap.id.to_string(), label: ap.label, prefix: format!("{}...", ap.prefix), scopes: ap.scopes, @@ -234,8 +235,8 @@ impl AppPasswordService { /// up to `BASIC_AUTH_CACHE_TTL_SECS`). pub async fn revoke( &self, - user_id: &str, - id: &str, + user_id: Uuid, + id: Uuid, ) -> Result { // Ownership enforced at SQL level (WHERE user_id = $2). // The get_by_id pre-check gives a clear error message when @@ -250,7 +251,7 @@ impl AppPasswordService { // Invalidate all cached auth entries for this user so the // revocation is effective immediately. - let uid = user_id.to_string(); + let uid = user_id; self.auth_cache .invalidate_entries_if(move |_key, val| val.user_id == uid) .ok(); @@ -282,7 +283,7 @@ impl AppPasswordService { &self, username: &str, password: &str, - ) -> Result<(String, String, String, String), DomainError> { + ) -> Result<(Uuid, String, String, String), DomainError> { // ── 1. Compute cache key = blake3("username:password") ──────── let cache_key: [u8; 32] = blake3::hash(format!("{}:{}", username, password).as_bytes()).into(); @@ -344,10 +345,10 @@ impl AppPasswordService { .verify_password(&verify_password, &ap.password_hash) .await { - let _ = self.repo.touch_last_used(&ap.id).await; + let _ = self.repo.touch_last_used(ap.id).await; let result = CachedBasicAuthResult { - user_id: user.id().to_string(), + user_id: user.id(), username: user.username().to_string(), email: user.email().to_string(), role: user.role().to_string(), @@ -372,16 +373,16 @@ impl AppPasswordService { /// Returns `(id, plain_password)`. pub async fn create_nc( &self, - user_id: &str, + user_id: Uuid, label: &str, - ) -> Result<(String, String), DomainError> { + ) -> Result<(Uuid, String), DomainError> { let password = generate_nc_app_password(); let normalized = nc_normalize_password(&password); let prefix = nc_token_prefix(&normalized)?; let hash = self.hasher.hash_password(&normalized).await?; let ap = AppPassword::new( - user_id.to_string(), + user_id, label.to_string(), hash, prefix, @@ -397,7 +398,7 @@ impl AppPasswordService { /// Scoped to the authenticated user (fixes I3 — no global prefix search). pub async fn revoke_by_password( &self, - user_id: &str, + user_id: Uuid, password: &str, ) -> Result<(), DomainError> { let normalized = nc_normalize_password(password); @@ -417,10 +418,10 @@ impl AppPasswordService { .verify_password(&normalized, &ap.password_hash) .await { - self.repo.revoke(&ap.id, user_id).await?; + self.repo.revoke(ap.id, user_id).await?; // Invalidate cache for this user - let uid = user_id.to_string(); + let uid = user_id; self.auth_cache .invalidate_entries_if(move |_key, val| val.user_id == uid) .ok(); @@ -432,12 +433,12 @@ impl AppPasswordService { } /// List app passwords for a user (simple summary for NC UI). - pub async fn list_nc(&self, user_id: &str) -> Result, DomainError> { + pub async fn list_nc(&self, user_id: Uuid) -> Result, DomainError> { self.repo.list_by_user(user_id).await } /// Delete an app password by ID, scoped to the owning user. - pub async fn delete_by_user(&self, id: &str, user_id: &str) -> Result<(), DomainError> { + pub async fn delete_by_user(&self, id: Uuid, user_id: Uuid) -> Result<(), DomainError> { let deleted = self.repo.delete_by_user_and_id(id, user_id).await?; if !deleted { return Err(DomainError::new( diff --git a/src/application/services/auth_application_service.rs b/src/application/services/auth_application_service.rs index 371945de..7c77b104 100755 --- a/src/application/services/auth_application_service.rs +++ b/src/application/services/auth_application_service.rs @@ -17,6 +17,7 @@ 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; @@ -33,7 +34,7 @@ pub enum OidcCallbackResult { /// app password and complete the NC login flow. NextcloudLogin { nc_flow_token: String, - user_id: String, + user_id: Uuid, username: String, }, } @@ -411,7 +412,7 @@ impl AuthApplicationService { // Save session let session = Session::new( - user.id().to_string(), + user.id(), refresh_token.clone(), None, // IP (can be added from the HTTP layer) None, // User-Agent (can be added from the HTTP layer) @@ -466,7 +467,7 @@ impl AuthApplicationService { } Ok(crate::application::dtos::user_dto::CurrentUser { - id: user.id().to_string(), + id: user.id(), username: user.username().to_string(), email: user.email().to_string(), role: user.role().to_string(), @@ -514,7 +515,7 @@ impl AuthApplicationService { // Create new session let new_session = Session::new( - user.id().to_string(), + user.id(), new_refresh_token.clone(), None, None, @@ -532,7 +533,7 @@ impl AuthApplicationService { }) } - pub async fn logout(&self, user_id: &str, refresh_token: &str) -> Result<(), DomainError> { + pub async fn logout(&self, user_id: Uuid, refresh_token: &str) -> Result<(), DomainError> { // Get session let session = match self .session_storage @@ -559,7 +560,7 @@ impl AuthApplicationService { Ok(()) } - pub async fn logout_all(&self, user_id: &str) -> Result { + pub async fn logout_all(&self, user_id: Uuid) -> Result { // Revoke all user sessions let revoked_count = self .session_storage @@ -571,7 +572,7 @@ impl AuthApplicationService { pub async fn change_password( &self, - user_id: &str, + user_id: Uuid, dto: ChangePasswordDto, ) -> Result<(), DomainError> { // Get user @@ -627,13 +628,13 @@ impl AuthApplicationService { Ok(()) } - pub async fn get_user(&self, user_id: &str) -> Result { + pub async fn get_user(&self, user_id: Uuid) -> Result { let user = self.user_storage.get_user_by_id(user_id).await?; Ok(UserDto::from(user)) } // Alias for consistency with handler method - pub async fn get_user_by_id(&self, user_id: &str) -> Result { + pub async fn get_user_by_id(&self, user_id: Uuid) -> Result { self.get_user(user_id).await } @@ -778,7 +779,7 @@ impl AuthApplicationService { /// Admin-only: reset a user's password. pub async fn admin_reset_password( &self, - user_id: &str, + user_id: Uuid, new_password: &str, ) -> Result<(), DomainError> { // Block password reset for OIDC-provisioned users @@ -812,13 +813,13 @@ impl AuthApplicationService { } /// Get a single user by ID (for admin panel) - pub async fn get_user_admin(&self, user_id: &str) -> Result { + pub async fn get_user_admin(&self, user_id: Uuid) -> Result { let user = self.user_storage.get_user_by_id(user_id).await?; Ok(UserDto::from(user)) } /// Delete a user by ID (admin only) - pub async fn delete_user_admin(&self, user_id: &str) -> Result<(), DomainError> { + pub async fn delete_user_admin(&self, user_id: Uuid) -> Result<(), DomainError> { // Prevent deleting yourself let user = self.user_storage.get_user_by_id(user_id).await?; tracing::info!("Admin deleting user: {} ({})", user.username(), user_id); @@ -826,14 +827,14 @@ impl AuthApplicationService { } /// Activate or deactivate a user (admin only) - pub async fn set_user_active(&self, user_id: &str, active: bool) -> Result<(), DomainError> { + pub async fn set_user_active(&self, user_id: Uuid, active: bool) -> Result<(), DomainError> { self.user_storage .set_user_active_status(user_id, active) .await } /// Change user role (admin only) - pub async fn change_user_role(&self, user_id: &str, role: &str) -> Result<(), DomainError> { + pub async fn change_user_role(&self, user_id: Uuid, role: &str) -> Result<(), DomainError> { if role != "admin" && role != "user" { return Err(DomainError::new( ErrorKind::InvalidInput, @@ -847,7 +848,7 @@ impl AuthApplicationService { /// Update user's storage quota (admin only) pub async fn update_user_quota( &self, - user_id: &str, + user_id: Uuid, quota_bytes: i64, ) -> Result<(), DomainError> { if quota_bytes < 0 { @@ -865,7 +866,7 @@ impl AuthApplicationService { /// Check if a user has enough quota for an upload of the given size pub async fn check_quota( &self, - user_id: &str, + user_id: Uuid, additional_bytes: i64, ) -> Result { let user = self.user_storage.get_user_by_id(user_id).await?; @@ -1209,7 +1210,7 @@ impl AuthApplicationService { ); return Ok(OidcCallbackResult::NextcloudLogin { nc_flow_token: nc_token, - user_id: user.id().to_string(), + user_id: user.id(), username: user.username().to_string(), }); } @@ -1219,7 +1220,7 @@ impl AuthApplicationService { let refresh_token = self.token_service.generate_refresh_token(); let session = Session::new( - user.id().to_string(), + user.id(), refresh_token.clone(), None, None, @@ -1282,7 +1283,7 @@ impl AuthApplicationService { } /// Helper to create a personal folder for a new user - async fn create_personal_folder(&self, username: &str, user_id: &str) { + async fn create_personal_folder(&self, username: &str, user_id: Uuid) { if let Some(folder_service) = &self.folder_service { let folder_name = format!("My Folder - {}", username); match folder_service diff --git a/src/application/services/batch_operations.rs b/src/application/services/batch_operations.rs index b4199b1c..9881bc63 100755 --- a/src/application/services/batch_operations.rs +++ b/src/application/services/batch_operations.rs @@ -20,6 +20,7 @@ use crate::application::services::folder_service::FolderService; use crate::application::services::trash_service::TrashService; use crate::common::config::AppConfig; use crate::common::errors::DomainError; +use uuid::Uuid; /// Specific errors for batch operations #[derive(Debug, Error)] @@ -117,7 +118,7 @@ impl BatchOperationService { &self, file_ids: Vec, target_folder_id: Option, - caller_id: &str, + user_id: Uuid, ) -> Result, BatchOperationError> { info!("Starting batch copy of {} files", file_ids.len()); let start_time = std::time::Instant::now(); @@ -135,17 +136,15 @@ impl BatchOperationService { // Arc avoids N heap-clones of the same string let target_folder: Option> = target_folder_id.map(|s| Arc::from(s.as_str())); - let caller: Arc = Arc::from(caller_id); // buffer_unordered materialises only max_concurrent futures at a time let mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| { let mgmt = self.file_management.clone(); let target_folder = target_folder.clone(); - let caller = caller.clone(); async move { let copy_result = mgmt - .copy_file_owned(&file_id, &caller, target_folder.map(|s| s.to_string())) + .copy_file_owned(&file_id, user_id, target_folder.map(|s| s.to_string())) .await; (file_id, copy_result) } @@ -187,7 +186,7 @@ impl BatchOperationService { &self, file_ids: Vec, target_folder_id: Option, - caller_id: &str, + user_id: Uuid, ) -> Result, BatchOperationError> { info!("Starting batch move of {} files", file_ids.len()); let start_time = std::time::Instant::now(); @@ -204,16 +203,14 @@ impl BatchOperationService { }; let target_folder: Option> = target_folder_id.map(|s| Arc::from(s.as_str())); - let caller: Arc = Arc::from(caller_id); let mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| { let mgmt = self.file_management.clone(); let target_folder = target_folder.clone(); - let caller = caller.clone(); async move { let move_result = mgmt - .move_file_owned(&file_id, &caller, target_folder.map(|s| s.to_string())) + .move_file_owned(&file_id, user_id, target_folder.map(|s| s.to_string())) .await; (file_id, move_result) } @@ -253,7 +250,7 @@ impl BatchOperationService { pub async fn delete_files( &self, file_ids: Vec, - caller_id: &str, + user_id: Uuid, ) -> Result, BatchOperationError> { info!("Starting batch deletion of {} files", file_ids.len()); let start_time = std::time::Instant::now(); @@ -268,15 +265,11 @@ impl BatchOperationService { }, }; - // Define the operation to perform for each file - let caller: Arc = Arc::from(caller_id); - let mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| { let mgmt = self.file_management.clone(); - let caller = caller.clone(); async move { - let delete_result = mgmt.delete_file_owned(&file_id, &caller).await; + let delete_result = mgmt.delete_file_owned(&file_id, user_id).await; let id_for_result = file_id.clone(); (file_id, delete_result.map(|_| id_for_result)) } @@ -317,7 +310,7 @@ impl BatchOperationService { pub async fn get_multiple_files( &self, file_ids: Vec, - caller_id: &str, + user_id: Uuid, ) -> Result, BatchOperationError> { info!("Starting batch load of {} files", file_ids.len()); let start_time = std::time::Instant::now(); @@ -332,15 +325,11 @@ impl BatchOperationService { }, }; - // Define the operation to perform for each file - let caller: Arc = Arc::from(caller_id); - let mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| { let retrieval = self.file_retrieval.clone(); - let caller = caller.clone(); async move { - let get_result = retrieval.get_file_owned(&file_id, &caller).await; + let get_result = retrieval.get_file_owned(&file_id, user_id).await; (file_id, get_result) } })) @@ -381,7 +370,7 @@ impl BatchOperationService { &self, folder_ids: Vec, _recursive: bool, - caller_id: &str, + user_id: Uuid, ) -> Result, BatchOperationError> { info!("Starting batch deletion of {} folders", folder_ids.len()); let start_time = std::time::Instant::now(); @@ -396,16 +385,11 @@ impl BatchOperationService { }, }; - // Define the operation to perform for each folder - // Arc avoids N heap-clones of the caller string - let caller: Arc = Arc::from(caller_id); - let mut operation_stream = stream::iter(folder_ids.into_iter().map(|folder_id| { let folder_service = self.folder_service.clone(); - let caller = caller.clone(); async move { - let delete_result = folder_service.delete_folder(&folder_id, &caller).await; + let delete_result = folder_service.delete_folder(&folder_id, user_id).await; let id_for_result = folder_id.clone(); (folder_id, delete_result.map(|_| id_for_result)) } @@ -445,7 +429,7 @@ impl BatchOperationService { pub async fn trash_files( &self, file_ids: Vec, - user_id: &str, + user_id: Uuid, ) -> Result, BatchOperationError> { let trash_service = self .trash_service @@ -464,14 +448,14 @@ impl BatchOperationService { }, }; - let uid: Arc = Arc::from(user_id); + 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.clone(); + let uid = uid; async move { - let trash_result = trash.move_to_trash(&file_id, "file", &uid).await; + let trash_result = trash.move_to_trash(&file_id, "file", uid).await; let id_for_result = file_id.clone(); (file_id, trash_result.map(|_| id_for_result)) } @@ -510,7 +494,7 @@ impl BatchOperationService { pub async fn trash_folders( &self, folder_ids: Vec, - user_id: &str, + user_id: Uuid, ) -> Result, BatchOperationError> { let trash_service = self .trash_service @@ -529,14 +513,14 @@ impl BatchOperationService { }, }; - let uid: Arc = Arc::from(user_id); + 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.clone(); + let uid = uid; async move { - let trash_result = trash.move_to_trash(&folder_id, "folder", &uid).await; + let trash_result = trash.move_to_trash(&folder_id, "folder", uid).await; let id_for_result = folder_id.clone(); (folder_id, trash_result.map(|_| id_for_result)) } @@ -576,7 +560,7 @@ impl BatchOperationService { &self, folder_ids: Vec, target_folder_id: Option, - caller_id: &str, + user_id: Uuid, ) -> Result, BatchOperationError> { info!("Starting batch move of {} folders", folder_ids.len()); let start_time = std::time::Instant::now(); @@ -591,18 +575,16 @@ impl BatchOperationService { }; let target: Option> = target_folder_id.map(|s| Arc::from(s.as_str())); - let caller: Arc = Arc::from(caller_id); let mut operation_stream = stream::iter(folder_ids.into_iter().map(|folder_id| { let folder_service = self.folder_service.clone(); let target = target.clone(); - let caller = caller.clone(); async move { let dto = MoveFolderDto { parent_id: target.map(|s| s.to_string()), }; - let move_result = folder_service.move_folder(&folder_id, dto, &caller).await; + let move_result = folder_service.move_folder(&folder_id, dto, user_id).await; (folder_id, move_result) } })) @@ -645,7 +627,7 @@ impl BatchOperationService { &self, file_ids: Vec, folder_ids: Vec, - caller_id: &str, + user_id: Uuid, ) -> Result { info!( "Starting batch download: {} files, {} folders", @@ -665,10 +647,10 @@ impl BatchOperationService { // ── Add individual files at the root of the ZIP ────────────────── for file_id in &file_ids { - match self.file_retrieval.get_file_owned(file_id, caller_id).await { + match self.file_retrieval.get_file_owned(file_id, user_id).await { Ok(file_dto) => { if let Err(e) = self - .add_file_entry_streamed(&mut zip, file_id, &file_dto.name, caller_id) + .add_file_entry_streamed(&mut zip, file_id, &file_dto.name, user_id) .await { info!("Could not add file {} to ZIP: {}", file_dto.name, e); @@ -684,12 +666,12 @@ impl BatchOperationService { for folder_id in &folder_ids { match self .folder_service - .get_folder_owned(folder_id, caller_id) + .get_folder_owned(folder_id, user_id) .await { Ok(root_folder) => { if let Err(e) = self - .add_folder_subtree_to_zip(&mut zip, folder_id, &root_folder, caller_id) + .add_folder_subtree_to_zip(&mut zip, folder_id, &root_folder, user_id) .await { info!("Could not add folder {} to ZIP: {}", root_folder.name, e); @@ -728,7 +710,7 @@ impl BatchOperationService { zip: &mut ZipFileWriter>>, file_id: &str, entry_name: &str, - caller_id: &str, + caller_id: Uuid, ) -> Result<(), BatchOperationError> { let entry = ZipEntryBuilder::new(entry_name.to_string().into(), Compression::Deflate); let mut writer = zip @@ -769,7 +751,7 @@ impl BatchOperationService { zip: &mut ZipFileWriter>>, folder_id: &str, root_folder: &FolderDto, - caller_id: &str, + caller_id: Uuid, ) -> Result<(), BatchOperationError> { // Bulk-fetch folder tree (small — one entry per folder) let all_folders = self @@ -908,7 +890,7 @@ impl BatchOperationService { pub async fn create_folders( &self, folders: Vec<(String, Option)>, // (name, parent_id) - caller_id: &str, + user_id: Uuid, ) -> Result, BatchOperationError> { info!("Starting batch creation of {} folders", folders.len()); let start_time = std::time::Instant::now(); @@ -923,17 +905,13 @@ impl BatchOperationService { }, }; - // Define the operation for each folder - let caller: Arc = Arc::from(caller_id); - let mut operation_stream = stream::iter(folders.into_iter().map(|(name, parent_id)| { let folder_service = self.folder_service.clone(); - let caller = caller.clone(); async move { // If a parent is specified, verify the caller owns it if let Some(ref pid) = parent_id - && let Err(e) = folder_service.get_folder_owned(pid, &caller).await + && let Err(e) = folder_service.get_folder_owned(pid, user_id).await { let id = format!("{}:{}", name, pid); return (id, Err(e)); @@ -983,7 +961,7 @@ impl BatchOperationService { pub async fn get_multiple_folders( &self, folder_ids: Vec, - caller_id: &str, + user_id: Uuid, ) -> Result, BatchOperationError> { info!("Starting batch load of {} folders", folder_ids.len()); let start_time = std::time::Instant::now(); @@ -998,15 +976,11 @@ impl BatchOperationService { }, }; - // Define the operation for each folder - let caller: Arc = Arc::from(caller_id); - let mut operation_stream = stream::iter(folder_ids.into_iter().map(|folder_id| { let folder_service = self.folder_service.clone(); - let caller = caller.clone(); async move { - let get_result = folder_service.get_folder_owned(&folder_id, &caller).await; + let get_result = folder_service.get_folder_owned(&folder_id, user_id).await; (folder_id, get_result) } })) diff --git a/src/application/services/calendar_service.rs b/src/application/services/calendar_service.rs index e887e8fa..7ce70475 100755 --- a/src/application/services/calendar_service.rs +++ b/src/application/services/calendar_service.rs @@ -1,5 +1,6 @@ use chrono::{DateTime, Utc}; use std::sync::Arc; +use uuid::Uuid; use crate::application::dtos::calendar_dto::{ CalendarDto, CalendarEventDto, CreateCalendarDto, CreateEventDto, CreateEventICalDto, @@ -23,7 +24,7 @@ impl CalendarUseCase for CalendarService { async fn create_calendar( &self, calendar: CreateCalendarDto, - user_id: &str, + user_id: Uuid, ) -> Result { self.calendar_storage .create_calendar(calendar, user_id) @@ -34,7 +35,7 @@ impl CalendarUseCase for CalendarService { &self, calendar_id: &str, update: UpdateCalendarDto, - user_id: &str, + user_id: Uuid, ) -> Result { let has_access = self .calendar_storage @@ -52,7 +53,7 @@ impl CalendarUseCase for CalendarService { .await } - async fn delete_calendar(&self, calendar_id: &str, user_id: &str) -> Result<(), DomainError> { + async fn delete_calendar(&self, calendar_id: &str, user_id: Uuid) -> Result<(), DomainError> { let has_access = self .calendar_storage .check_calendar_access(calendar_id, user_id) @@ -70,7 +71,7 @@ impl CalendarUseCase for CalendarService { async fn get_calendar( &self, calendar_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result { let calendar = self.calendar_storage.get_calendar(calendar_id).await?; let has_access = self @@ -87,11 +88,11 @@ impl CalendarUseCase for CalendarService { Ok(calendar) } - async fn list_my_calendars(&self, user_id: &str) -> Result, DomainError> { + async fn list_my_calendars(&self, user_id: Uuid) -> Result, DomainError> { self.calendar_storage.list_calendars_by_owner(user_id).await } - async fn list_shared_calendars(&self, user_id: &str) -> Result, DomainError> { + async fn list_shared_calendars(&self, user_id: Uuid) -> Result, DomainError> { self.calendar_storage .list_calendars_shared_with_user(user_id) .await @@ -112,12 +113,12 @@ impl CalendarUseCase for CalendarService { async fn share_calendar( &self, calendar_id: &str, - target_user_id: &str, + target_user_id: Uuid, access_level: &str, - caller_user_id: &str, + caller_user_id: Uuid, ) -> Result<(), DomainError> { let calendar = self.calendar_storage.get_calendar(calendar_id).await?; - if calendar.owner_id != caller_user_id { + if calendar.owner_id != caller_user_id.to_string() { return Err(DomainError::new( ErrorKind::AccessDenied, "Calendar", @@ -145,11 +146,11 @@ impl CalendarUseCase for CalendarService { async fn remove_calendar_sharing( &self, calendar_id: &str, - target_user_id: &str, - caller_user_id: &str, + target_user_id: Uuid, + caller_user_id: Uuid, ) -> Result<(), DomainError> { let calendar = self.calendar_storage.get_calendar(calendar_id).await?; - if calendar.owner_id != caller_user_id { + if calendar.owner_id != caller_user_id.to_string() { return Err(DomainError::new( ErrorKind::AccessDenied, "Calendar", @@ -164,10 +165,10 @@ impl CalendarUseCase for CalendarService { async fn get_calendar_shares( &self, calendar_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let calendar = self.calendar_storage.get_calendar(calendar_id).await?; - if calendar.owner_id != user_id { + if calendar.owner_id != user_id.to_string() { return Err(DomainError::new( ErrorKind::AccessDenied, "Calendar", @@ -180,7 +181,7 @@ impl CalendarUseCase for CalendarService { async fn create_event( &self, event: CreateEventDto, - user_id: &str, + user_id: Uuid, ) -> Result { let has_access = self .calendar_storage @@ -199,7 +200,7 @@ impl CalendarUseCase for CalendarService { async fn create_event_from_ical( &self, event: CreateEventICalDto, - user_id: &str, + user_id: Uuid, ) -> Result { let has_access = self .calendar_storage @@ -219,7 +220,7 @@ impl CalendarUseCase for CalendarService { &self, event_id: &str, update: UpdateEventDto, - user_id: &str, + user_id: Uuid, ) -> Result { let event = self.calendar_storage.get_event(event_id).await?; let has_access = self @@ -236,7 +237,7 @@ impl CalendarUseCase for CalendarService { self.calendar_storage.update_event(event_id, update).await } - async fn delete_event(&self, event_id: &str, user_id: &str) -> Result<(), DomainError> { + async fn delete_event(&self, event_id: &str, user_id: Uuid) -> Result<(), DomainError> { let event = self.calendar_storage.get_event(event_id).await?; let has_access = self .calendar_storage @@ -255,7 +256,7 @@ impl CalendarUseCase for CalendarService { async fn get_event( &self, event_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result { let event = self.calendar_storage.get_event(event_id).await?; let has_access = self @@ -281,7 +282,7 @@ impl CalendarUseCase for CalendarService { calendar_id: &str, limit: Option, offset: Option, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let has_access = self .calendar_storage @@ -313,7 +314,7 @@ impl CalendarUseCase for CalendarService { calendar_id: &str, start: DateTime, end: DateTime, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let has_access = self .calendar_storage diff --git a/src/application/services/contact_service.rs b/src/application/services/contact_service.rs index 33b8e933..0a7904a7 100755 --- a/src/application/services/contact_service.rs +++ b/src/application/services/contact_service.rs @@ -1,5 +1,5 @@ use chrono::Utc; -use sqlx::types::Uuid; +use uuid::Uuid; use std::sync::Arc; use crate::application::dtos::address_book_dto::{ @@ -43,7 +43,7 @@ impl ContactService { async fn check_address_book_access( &self, address_book_id: &Uuid, - user_id: &str, + user_id: &Uuid, ) -> Result { let address_book = self .address_book_repository @@ -52,7 +52,7 @@ impl ContactService { .ok_or_else(|| DomainError::not_found("Address book", "not found"))?; // Check if user is owner - if address_book.owner_id() == user_id { + if address_book.owner_id() == user_id.to_string() { return Ok(address_book); } @@ -61,7 +61,7 @@ impl ContactService { .address_book_repository .get_address_book_shares(address_book_id) .await?; - if shares.iter().any(|(id, _)| id == user_id) { + if shares.iter().any(|(id, _)| id == &user_id.to_string()) { return Ok(address_book); } @@ -78,7 +78,7 @@ impl ContactService { async fn check_address_book_write_access( &self, address_book_id: &Uuid, - user_id: &str, + user_id: &Uuid, ) -> Result { let address_book = self .address_book_repository @@ -87,7 +87,7 @@ impl ContactService { .ok_or_else(|| DomainError::not_found("Address book", "not found"))?; // Check if user is owner - if address_book.owner_id() == user_id { + if address_book.owner_id() == user_id.to_string() { return Ok(address_book); } @@ -98,7 +98,7 @@ impl ContactService { .await?; if shares .iter() - .any(|(id, can_write)| id == user_id && *can_write) + .any(|(id, can_write)| id == &user_id.to_string() && *can_write) { return Ok(address_book); } @@ -296,7 +296,8 @@ impl AddressBookUseCase for ContactService { // Check if user has write access to the address book let address_book = self - .check_address_book_write_access(&id, &update.user_id) + .check_address_book_write_access(&id, &Uuid::parse_str(&update.user_id) + .map_err(|_| DomainError::validation_error("Invalid user ID format"))?) .await?; // Apply updates @@ -327,7 +328,7 @@ impl AddressBookUseCase for ContactService { async fn delete_address_book( &self, address_book_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result<(), DomainError> { let id = Uuid::parse_str(address_book_id) .map_err(|_| DomainError::validation_error("Invalid address book ID format"))?; @@ -339,7 +340,7 @@ impl AddressBookUseCase for ContactService { .await? .ok_or_else(|| DomainError::not_found("Address book", "not found"))?; - if address_book.owner_id() != user_id { + if address_book.owner_id() != user_id.to_string() { return Err(DomainError::unauthorized( "Only the owner can delete an address book", )); @@ -354,18 +355,18 @@ impl AddressBookUseCase for ContactService { async fn get_address_book( &self, address_book_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result { let id = Uuid::parse_str(address_book_id) .map_err(|_| DomainError::validation_error("Invalid address book ID format"))?; - let address_book = self.check_address_book_access(&id, user_id).await?; + let address_book = self.check_address_book_access(&id, &user_id).await?; Ok(AddressBookDto::from(address_book)) } async fn list_user_address_books( &self, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { // Get address books owned by the user let owned_address_books = self @@ -397,7 +398,7 @@ impl AddressBookUseCase for ContactService { } for address_book in public_address_books { - if address_book.owner_id() != user_id + if address_book.owner_id() != user_id.to_string() && !address_book_map.contains_key(address_book.id()) { address_book_map.insert(*address_book.id(), address_book); @@ -428,7 +429,7 @@ impl AddressBookUseCase for ContactService { async fn share_address_book( &self, dto: ShareAddressBookDto, - user_id: &str, + user_id: Uuid, ) -> Result<(), DomainError> { let id = Uuid::parse_str(&dto.address_book_id) .map_err(|_| DomainError::validation_error("Invalid address book ID format"))?; @@ -440,21 +441,23 @@ impl AddressBookUseCase for ContactService { .await? .ok_or_else(|| DomainError::not_found("Address book", "not found"))?; - if address_book.owner_id() != user_id { + if address_book.owner_id() != user_id.to_string() { return Err(DomainError::unauthorized( "Only the owner can share an address book", )); } // Don't allow sharing with yourself - if dto.user_id == user_id { + if dto.user_id == user_id.to_string() { return Err(DomainError::validation_error( "Cannot share an address book with yourself", )); } + let target_user_id = Uuid::parse_str(&dto.user_id) + .map_err(|_| DomainError::validation_error("Invalid target user ID format"))?; self.address_book_repository - .share_address_book(&id, &dto.user_id, dto.can_write) + .share_address_book(&id, target_user_id, dto.can_write) .await?; Ok(()) } @@ -462,7 +465,7 @@ impl AddressBookUseCase for ContactService { async fn unshare_address_book( &self, dto: UnshareAddressBookDto, - user_id: &str, + user_id: Uuid, ) -> Result<(), DomainError> { let id = Uuid::parse_str(&dto.address_book_id) .map_err(|_| DomainError::validation_error("Invalid address book ID format"))?; @@ -474,14 +477,16 @@ impl AddressBookUseCase for ContactService { .await? .ok_or_else(|| DomainError::not_found("Address book", "not found"))?; - if address_book.owner_id() != user_id { + if address_book.owner_id() != user_id.to_string() { return Err(DomainError::unauthorized( "Only the owner can unshare an address book", )); } + let target_user_id = Uuid::parse_str(&dto.user_id) + .map_err(|_| DomainError::validation_error("Invalid target user ID format"))?; self.address_book_repository - .unshare_address_book(&id, &dto.user_id) + .unshare_address_book(&id, target_user_id) .await?; Ok(()) } @@ -489,7 +494,7 @@ impl AddressBookUseCase for ContactService { async fn get_address_book_shares( &self, address_book_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let id = Uuid::parse_str(address_book_id) .map_err(|_| DomainError::validation_error("Invalid address book ID format"))?; @@ -501,7 +506,7 @@ impl AddressBookUseCase for ContactService { .await? .ok_or_else(|| DomainError::not_found("Address book", "not found"))?; - if address_book.owner_id() != user_id { + if address_book.owner_id() != user_id.to_string() { return Err(DomainError::unauthorized( "Only the owner can view address book shares", )); @@ -521,7 +526,8 @@ 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, &dto.user_id) + 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 @@ -598,7 +604,8 @@ 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, &dto.user_id) + 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 @@ -633,7 +640,9 @@ impl ContactUseCase for ContactService { .ok_or_else(|| DomainError::not_found("Contact", "not found"))?; // Check if user has write access to the address book - self.check_address_book_write_access(contact.address_book_id(), &update.user_id) + let update_user_id = Uuid::parse_str(&update.user_id) + .map_err(|_| DomainError::validation_error("Invalid user ID format"))?; + self.check_address_book_write_access(contact.address_book_id(), &update_user_id) .await?; // Destructure contact into owned parts for updates @@ -720,7 +729,7 @@ impl ContactUseCase for ContactService { Ok(ContactDto::from(result)) } - async fn delete_contact(&self, contact_id: &str, user_id: &str) -> Result<(), DomainError> { + async fn delete_contact(&self, contact_id: &str, user_id: Uuid) -> Result<(), DomainError> { let id = Uuid::parse_str(contact_id) .map_err(|_| DomainError::validation_error("Invalid contact ID format"))?; @@ -732,7 +741,7 @@ impl ContactUseCase for ContactService { .ok_or_else(|| DomainError::not_found("Contact", "not found"))?; // Check if user has write access to the address book - self.check_address_book_write_access(contact.address_book_id(), user_id) + self.check_address_book_write_access(contact.address_book_id(), &user_id) .await?; // Delete the contact @@ -743,7 +752,7 @@ impl ContactUseCase for ContactService { async fn get_contact( &self, contact_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result { let id = Uuid::parse_str(contact_id) .map_err(|_| DomainError::validation_error("Invalid contact ID format"))?; @@ -756,7 +765,7 @@ impl ContactUseCase for ContactService { .ok_or_else(|| DomainError::not_found("Contact", "not found"))?; // Check if user has access to the address book - self.check_address_book_access(contact.address_book_id(), user_id) + self.check_address_book_access(contact.address_book_id(), &user_id) .await?; Ok(ContactDto::from(contact)) @@ -765,13 +774,13 @@ impl ContactUseCase for ContactService { async fn list_contacts( &self, address_book_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let id = Uuid::parse_str(address_book_id) .map_err(|_| DomainError::validation_error("Invalid address book ID format"))?; // Check if user has access to the address book - self.check_address_book_access(&id, user_id).await?; + self.check_address_book_access(&id, &user_id).await?; // Get contacts let contacts = self @@ -787,13 +796,13 @@ impl ContactUseCase for ContactService { &self, address_book_id: &str, query: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let id = Uuid::parse_str(address_book_id) .map_err(|_| DomainError::validation_error("Invalid address book ID format"))?; // Check if user has access to the address book - self.check_address_book_access(&id, user_id).await?; + self.check_address_book_access(&id, &user_id).await?; // Search contacts let contacts = self.contact_repository.search_contacts(&id, query).await?; @@ -810,7 +819,8 @@ 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, &dto.user_id) + 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); @@ -835,7 +845,8 @@ 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(), &update.user_id) + 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 @@ -854,7 +865,7 @@ impl ContactUseCase for ContactService { Ok(ContactGroupDto::from(result)) } - async fn delete_group(&self, group_id: &str, user_id: &str) -> Result<(), DomainError> { + async fn delete_group(&self, group_id: &str, user_id: Uuid) -> Result<(), DomainError> { let id = Uuid::parse_str(group_id) .map_err(|_| DomainError::validation_error("Invalid group ID format"))?; @@ -866,7 +877,7 @@ 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(), user_id) + self.check_address_book_write_access(group.address_book_id(), &user_id) .await?; // Delete the group @@ -877,7 +888,7 @@ impl ContactUseCase for ContactService { async fn get_group( &self, group_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result { let id = Uuid::parse_str(group_id) .map_err(|_| DomainError::validation_error("Invalid group ID format"))?; @@ -890,7 +901,7 @@ impl ContactUseCase for ContactService { .ok_or_else(|| DomainError::not_found("Contact group", "not found"))?; // Check if user has access to the address book - self.check_address_book_access(group.address_book_id(), user_id) + self.check_address_book_access(group.address_book_id(), &user_id) .await?; // Get the number of contacts in the group @@ -908,13 +919,13 @@ impl ContactUseCase for ContactService { async fn list_groups( &self, address_book_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let id = Uuid::parse_str(address_book_id) .map_err(|_| DomainError::validation_error("Invalid address book ID format"))?; // Check if user has access to the address book - self.check_address_book_access(&id, user_id).await?; + self.check_address_book_access(&id, &user_id).await?; // Get groups let groups = self @@ -929,7 +940,7 @@ impl ContactUseCase for ContactService { async fn add_contact_to_group( &self, dto: GroupMembershipDto, - user_id: &str, + user_id: Uuid, ) -> Result<(), DomainError> { let group_id = Uuid::parse_str(&dto.group_id) .map_err(|_| DomainError::validation_error("Invalid group ID format"))?; @@ -945,7 +956,7 @@ 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(), user_id) + self.check_address_book_write_access(group.address_book_id(), &user_id) .await?; // Add contact to group @@ -958,7 +969,7 @@ impl ContactUseCase for ContactService { async fn remove_contact_from_group( &self, dto: GroupMembershipDto, - user_id: &str, + user_id: Uuid, ) -> Result<(), DomainError> { let group_id = Uuid::parse_str(&dto.group_id) .map_err(|_| DomainError::validation_error("Invalid group ID format"))?; @@ -974,7 +985,7 @@ 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(), user_id) + self.check_address_book_write_access(group.address_book_id(), &user_id) .await?; // Remove contact from group @@ -987,7 +998,7 @@ impl ContactUseCase for ContactService { async fn list_contacts_in_group( &self, group_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let id = Uuid::parse_str(group_id) .map_err(|_| DomainError::validation_error("Invalid group ID format"))?; @@ -1000,7 +1011,7 @@ impl ContactUseCase for ContactService { .ok_or_else(|| DomainError::not_found("Contact group", "not found"))?; // Check if user has access to the address book - self.check_address_book_access(group.address_book_id(), user_id) + self.check_address_book_access(group.address_book_id(), &user_id) .await?; // Get contacts in group @@ -1016,7 +1027,7 @@ impl ContactUseCase for ContactService { async fn list_groups_for_contact( &self, contact_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let id = Uuid::parse_str(contact_id) .map_err(|_| DomainError::validation_error("Invalid contact ID format"))?; @@ -1029,7 +1040,7 @@ impl ContactUseCase for ContactService { .ok_or_else(|| DomainError::not_found("Contact", "not found"))?; // Check if user has access to the address book - self.check_address_book_access(contact.address_book_id(), user_id) + self.check_address_book_access(contact.address_book_id(), &user_id) .await?; // Get groups for contact @@ -1045,7 +1056,7 @@ impl ContactUseCase for ContactService { async fn get_contact_vcard( &self, contact_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result { let id = Uuid::parse_str(contact_id) .map_err(|_| DomainError::validation_error("Invalid contact ID format"))?; @@ -1058,7 +1069,7 @@ impl ContactUseCase for ContactService { .ok_or_else(|| DomainError::not_found("Contact", "not found"))?; // Check if user has access to the address book - self.check_address_book_access(contact.address_book_id(), user_id) + self.check_address_book_access(contact.address_book_id(), &user_id) .await?; // Return the vCard data @@ -1068,13 +1079,13 @@ impl ContactUseCase for ContactService { async fn get_contacts_as_vcards( &self, address_book_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let id = Uuid::parse_str(address_book_id) .map_err(|_| DomainError::validation_error("Invalid address book ID format"))?; // Check if user has access to the address book - self.check_address_book_access(&id, user_id).await?; + self.check_address_book_access(&id, &user_id).await?; // Get all contacts in the address book let contacts = self @@ -1130,6 +1141,8 @@ impl StorageUseCase for ContactService { let user_id = params["user_id"] .as_str() .ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?; + let user_id = Uuid::parse_str(user_id) + .map_err(|_| DomainError::validation_error("Invalid user_id format"))?; self.delete_address_book(address_book_id, user_id).await?; Ok(serde_json::Value::Null) @@ -1142,6 +1155,8 @@ impl StorageUseCase for ContactService { let user_id = params["user_id"] .as_str() .ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?; + let user_id = Uuid::parse_str(user_id) + .map_err(|_| DomainError::validation_error("Invalid user_id format"))?; let result = self.get_address_book(address_book_id, user_id).await?; Ok(serde_json::to_value(result).unwrap()) @@ -1150,6 +1165,8 @@ impl StorageUseCase for ContactService { let user_id = params["user_id"] .as_str() .ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?; + let user_id = Uuid::parse_str(user_id) + .map_err(|_| DomainError::validation_error("Invalid user_id format"))?; let result = self.list_user_address_books(user_id).await?; Ok(serde_json::to_value(result).unwrap()) @@ -1167,6 +1184,8 @@ impl StorageUseCase for ContactService { let user_id = params["user_id"] .as_str() .ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?; + let user_id = Uuid::parse_str(user_id) + .map_err(|_| DomainError::validation_error("Invalid user_id format"))?; self.share_address_book(dto, user_id).await?; Ok(serde_json::Value::Null) @@ -1180,6 +1199,8 @@ impl StorageUseCase for ContactService { let user_id = params["user_id"] .as_str() .ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?; + let user_id = Uuid::parse_str(user_id) + .map_err(|_| DomainError::validation_error("Invalid user_id format"))?; self.unshare_address_book(dto, user_id).await?; Ok(serde_json::Value::Null) @@ -1192,6 +1213,8 @@ impl StorageUseCase for ContactService { let user_id = params["user_id"] .as_str() .ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?; + let user_id = Uuid::parse_str(user_id) + .map_err(|_| DomainError::validation_error("Invalid user_id format"))?; let result = self .get_address_book_shares(address_book_id, user_id) @@ -1239,6 +1262,8 @@ impl StorageUseCase for ContactService { let user_id = params["user_id"] .as_str() .ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?; + let user_id = Uuid::parse_str(user_id) + .map_err(|_| DomainError::validation_error("Invalid user_id format"))?; self.delete_contact(contact_id, user_id).await?; Ok(serde_json::Value::Null) @@ -1251,6 +1276,8 @@ impl StorageUseCase for ContactService { let user_id = params["user_id"] .as_str() .ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?; + let user_id = Uuid::parse_str(user_id) + .map_err(|_| DomainError::validation_error("Invalid user_id format"))?; let result = self.get_contact(contact_id, user_id).await?; Ok(serde_json::to_value(result).unwrap()) @@ -1263,6 +1290,8 @@ impl StorageUseCase for ContactService { let user_id = params["user_id"] .as_str() .ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?; + let user_id = Uuid::parse_str(user_id) + .map_err(|_| DomainError::validation_error("Invalid user_id format"))?; let result = self.list_contacts(address_book_id, user_id).await?; Ok(serde_json::to_value(result).unwrap()) @@ -1279,6 +1308,8 @@ impl StorageUseCase for ContactService { let user_id = params["user_id"] .as_str() .ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?; + let user_id = Uuid::parse_str(user_id) + .map_err(|_| DomainError::validation_error("Invalid user_id format"))?; let result = self .search_contacts(address_book_id, query, user_id) @@ -1317,6 +1348,8 @@ impl StorageUseCase for ContactService { let user_id = params["user_id"] .as_str() .ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?; + let user_id = Uuid::parse_str(user_id) + .map_err(|_| DomainError::validation_error("Invalid user_id format"))?; self.delete_group(group_id, user_id).await?; Ok(serde_json::Value::Null) @@ -1329,6 +1362,8 @@ impl StorageUseCase for ContactService { let user_id = params["user_id"] .as_str() .ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?; + let user_id = Uuid::parse_str(user_id) + .map_err(|_| DomainError::validation_error("Invalid user_id format"))?; let result = self.get_group(group_id, user_id).await?; Ok(serde_json::to_value(result).unwrap()) @@ -1341,6 +1376,8 @@ impl StorageUseCase for ContactService { let user_id = params["user_id"] .as_str() .ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?; + let user_id = Uuid::parse_str(user_id) + .map_err(|_| DomainError::validation_error("Invalid user_id format"))?; let result = self.list_groups(address_book_id, user_id).await?; Ok(serde_json::to_value(result).unwrap()) @@ -1356,6 +1393,8 @@ impl StorageUseCase for ContactService { let user_id = params["user_id"] .as_str() .ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?; + let user_id = Uuid::parse_str(user_id) + .map_err(|_| DomainError::validation_error("Invalid user_id format"))?; self.add_contact_to_group(dto, user_id).await?; Ok(serde_json::Value::Null) @@ -1369,6 +1408,8 @@ impl StorageUseCase for ContactService { let user_id = params["user_id"] .as_str() .ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?; + let user_id = Uuid::parse_str(user_id) + .map_err(|_| DomainError::validation_error("Invalid user_id format"))?; self.remove_contact_from_group(dto, user_id).await?; Ok(serde_json::Value::Null) @@ -1381,6 +1422,8 @@ impl StorageUseCase for ContactService { let user_id = params["user_id"] .as_str() .ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?; + let user_id = Uuid::parse_str(user_id) + .map_err(|_| DomainError::validation_error("Invalid user_id format"))?; let result = self.list_contacts_in_group(group_id, user_id).await?; Ok(serde_json::to_value(result).unwrap()) @@ -1393,6 +1436,8 @@ impl StorageUseCase for ContactService { let user_id = params["user_id"] .as_str() .ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?; + let user_id = Uuid::parse_str(user_id) + .map_err(|_| DomainError::validation_error("Invalid user_id format"))?; let result = self.list_groups_for_contact(contact_id, user_id).await?; Ok(serde_json::to_value(result).unwrap()) @@ -1407,6 +1452,8 @@ impl StorageUseCase for ContactService { let user_id = params["user_id"] .as_str() .ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?; + let user_id = Uuid::parse_str(user_id) + .map_err(|_| DomainError::validation_error("Invalid user_id format"))?; let result = self.get_contact_vcard(contact_id, user_id).await?; Ok(serde_json::to_value(result).unwrap()) @@ -1419,6 +1466,8 @@ impl StorageUseCase for ContactService { let user_id = params["user_id"] .as_str() .ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?; + let user_id = Uuid::parse_str(user_id) + .map_err(|_| DomainError::validation_error("Invalid user_id format"))?; let result = self .get_contacts_as_vcards(address_book_id, user_id) diff --git a/src/application/services/device_auth_service.rs b/src/application/services/device_auth_service.rs index b1f050d3..aa047071 100755 --- a/src/application/services/device_auth_service.rs +++ b/src/application/services/device_auth_service.rs @@ -22,6 +22,7 @@ use crate::infrastructure::repositories::pg::DeviceCodePgRepository; use crate::infrastructure::repositories::pg::SessionPgRepository; use crate::infrastructure::repositories::pg::UserPgRepository; use crate::infrastructure::services::jwt_service::JwtTokenService; +use uuid::Uuid; /// Default device code lifetime: 15 minutes (RFC 8628 recommends 5-30 min). const DEVICE_CODE_LIFETIME_SECS: i64 = 900; @@ -155,7 +156,7 @@ impl DeviceAuthService { /// /// * `user_code` — the code from the verification page /// * `user_id` — the authenticated user's ID (from session/JWT) - pub async fn approve(&self, user_code: &str, user_id: &str) -> Result<(), DomainError> { + pub async fn approve(&self, user_code: &str, user_id: Uuid) -> Result<(), DomainError> { let normalized = user_code.trim().to_uppercase().replace(' ', ""); let mut dc = self @@ -180,7 +181,7 @@ impl DeviceAuthService { // Persist refresh token as a session let session = Session::new( - user_id.to_string(), + user_id, refresh_token.clone(), None, // ip_address Some(format!("device:{}", dc.client_name())), // user_agent @@ -189,7 +190,7 @@ impl DeviceAuthService { self.session_storage.create_session(session).await?; // Store tokens on the device code entity - dc.authorize(user_id.to_string(), access_token, refresh_token); + dc.authorize(user_id, access_token, refresh_token); self.device_code_storage.update_device_code(dc).await?; tracing::info!( @@ -297,7 +298,7 @@ impl DeviceAuthService { pub async fn list_user_devices( &self, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let codes = self.device_code_storage.list_by_user(user_id).await?; Ok(codes @@ -318,7 +319,7 @@ impl DeviceAuthService { // 8. Revoke — user revokes a device authorization // ======================================================================== - pub async fn revoke_device(&self, device_id: &str, user_id: &str) -> Result<(), DomainError> { + pub async fn revoke_device(&self, device_id: Uuid, user_id: Uuid) -> Result<(), DomainError> { // Verify ownership before deleting let devices = self.device_code_storage.list_by_user(user_id).await?; let found = devices.iter().any(|d| d.id() == device_id); diff --git a/src/application/services/favorites_service.rs b/src/application/services/favorites_service.rs index d117f258..e1da74d2 100755 --- a/src/application/services/favorites_service.rs +++ b/src/application/services/favorites_service.rs @@ -2,6 +2,7 @@ use std::collections::HashSet; use std::sync::Arc; use tracing::info; +use uuid::Uuid; use crate::application::dtos::favorites_dto::{ BatchFavoritesResult, BatchFavoritesStats, FavoriteItemDto, @@ -27,7 +28,7 @@ impl FavoritesService { impl FavoritesUseCase for FavoritesService { /// Get all favorites for a user - async fn get_favorites(&self, user_id: &str) -> Result> { + async fn get_favorites(&self, user_id: Uuid) -> Result> { info!("Getting favorites for user: {}", user_id); let favorites = self.repo.get_favorites(user_id).await?; info!( @@ -39,7 +40,7 @@ impl FavoritesUseCase for FavoritesService { } /// Add an item to user's favorites - async fn add_to_favorites(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()> { + async fn add_to_favorites(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<()> { info!( "Adding {} '{}' to favorites for user {}", item_type, item_id, user_id @@ -64,7 +65,7 @@ impl FavoritesUseCase for FavoritesService { /// Remove an item from user's favorites async fn remove_from_favorites( &self, - user_id: &str, + user_id: Uuid, item_id: &str, item_type: &str, ) -> Result { @@ -91,7 +92,7 @@ impl FavoritesUseCase for FavoritesService { } /// Check if an item is in user's favorites - async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result { + async fn is_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result { info!( "Checking if {} '{}' is favorite for user {}", item_type, item_id, user_id @@ -101,7 +102,7 @@ impl FavoritesUseCase for FavoritesService { async fn batch_add_to_favorites( &self, - user_id: &str, + user_id: Uuid, items: &[(String, String)], ) -> Result { info!( @@ -148,7 +149,7 @@ impl FavoritesUseCase for FavoritesService { async fn batch_check_favorites( &self, - user_id: &str, + user_id: Uuid, item_ids: &[(&str, &str)], ) -> Result> { self.repo.batch_check_favorites(user_id, item_ids).await diff --git a/src/application/services/file_management_service.rs b/src/application/services/file_management_service.rs index e96f861d..9a003362 100755 --- a/src/application/services/file_management_service.rs +++ b/src/application/services/file_management_service.rs @@ -9,6 +9,7 @@ use crate::common::errors::DomainError; use crate::infrastructure::repositories::pg::file_blob_read_repository::FileBlobReadRepository; use crate::infrastructure::repositories::pg::file_blob_write_repository::FileBlobWriteRepository; use tracing::{error, info, warn}; +use uuid::Uuid; /// Service for file management operations (move, delete). /// @@ -46,7 +47,7 @@ impl FileManagementService { } /// Verifies ownership via the read repository. - async fn verify_owner(&self, file_id: &str, caller_id: &str) -> Result<(), DomainError> { + async fn verify_owner(&self, file_id: &str, caller_id: Uuid) -> Result<(), DomainError> { if let Some(read) = &self.file_read { read.verify_file_owner(file_id, caller_id).await } else { @@ -92,7 +93,7 @@ impl FileManagementUseCase for FileManagementService { async fn move_file_owned( &self, file_id: &str, - caller_id: &str, + caller_id: Uuid, folder_id: Option, ) -> Result { self.verify_owner(file_id, caller_id).await?; @@ -131,7 +132,7 @@ impl FileManagementUseCase for FileManagementService { async fn copy_file_owned( &self, file_id: &str, - caller_id: &str, + caller_id: Uuid, target_folder_id: Option, ) -> Result { self.verify_owner(file_id, caller_id).await?; @@ -162,7 +163,7 @@ impl FileManagementUseCase for FileManagementService { async fn rename_file_owned( &self, file_id: &str, - caller_id: &str, + caller_id: Uuid, new_name: &str, ) -> Result { self.verify_owner(file_id, caller_id).await?; @@ -173,7 +174,7 @@ impl FileManagementUseCase for FileManagementService { self.file_repository.delete_file(id).await } - async fn delete_file_owned(&self, id: &str, caller_id: &str) -> Result<(), DomainError> { + async fn delete_file_owned(&self, id: &str, caller_id: Uuid) -> Result<(), DomainError> { self.verify_owner(id, caller_id).await?; self.delete_file(id).await } @@ -184,7 +185,7 @@ impl FileManagementUseCase for FileManagementService { /// `trg_files_decrement_blob_ref` which fires on DELETE FROM storage.files. /// We do NOT decrement here — trashing is a soft-delete (UPDATE, not DELETE) /// so the blob must remain referenced until the file is permanently deleted. - async fn delete_with_cleanup(&self, id: &str, user_id: &str) -> Result { + async fn delete_with_cleanup(&self, id: &str, user_id: Uuid) -> Result { // Step 1: Try trash (soft delete — file row stays, blob stays referenced) if let Some(trash) = &self.trash_service { info!("Moving file to trash: {}", id); diff --git a/src/application/services/file_retrieval_service.rs b/src/application/services/file_retrieval_service.rs index 7e1fb9a9..2587b9af 100755 --- a/src/application/services/file_retrieval_service.rs +++ b/src/application/services/file_retrieval_service.rs @@ -13,6 +13,7 @@ use crate::infrastructure::services::image_transcode_service::{ ImageTranscodeService, OutputFormat, }; use tracing::{debug, info}; +use uuid::Uuid; /// Threshold below which files are served from RAM cache (10 MB). const CACHE_THRESHOLD: u64 = 10 * 1024 * 1024; @@ -201,7 +202,7 @@ impl FileRetrievalUseCase for FileRetrievalService { Ok(FileDto::from(file)) } - async fn get_file_owned(&self, id: &str, caller_id: &str) -> Result { + async fn get_file_owned(&self, id: &str, caller_id: Uuid) -> Result { let file = self.file_read.get_file_for_owner(id, caller_id).await?; Ok(FileDto::from(file)) } @@ -226,7 +227,7 @@ impl FileRetrievalUseCase for FileRetrievalService { async fn list_files_owned( &self, folder_id: Option<&str>, - owner_id: &str, + owner_id: Uuid, ) -> Result, DomainError> { let files = self .file_read @@ -245,7 +246,7 @@ impl FileRetrievalUseCase for FileRetrievalService { async fn get_file_stream_owned( &self, id: &str, - caller_id: &str, + caller_id: Uuid, ) -> Result> + Send>, DomainError> { self.file_read.verify_file_owner(id, caller_id).await?; self.file_read.get_file_stream(id).await @@ -267,7 +268,7 @@ impl FileRetrievalUseCase for FileRetrievalService { async fn get_file_optimized_owned( &self, id: &str, - caller_id: &str, + caller_id: Uuid, accept_webp: bool, prefer_original: bool, ) -> Result<(FileDto, OptimizedFileContent), DomainError> { @@ -302,7 +303,7 @@ impl FileRetrievalUseCase for FileRetrievalService { async fn get_file_range_stream_owned( &self, id: &str, - caller_id: &str, + caller_id: Uuid, start: u64, end: Option, ) -> Result> + Send>, DomainError> { @@ -336,7 +337,7 @@ impl FileRetrievalUseCase for FileRetrievalService { async fn list_files_batch_for_owner( &self, folder_id: Option<&str>, - owner_id: &str, + owner_id: Uuid, offset: i64, limit: i64, ) -> Result, DomainError> { diff --git a/src/application/services/folder_service.rs b/src/application/services/folder_service.rs index e547dd2e..c665ded6 100755 --- a/src/application/services/folder_service.rs +++ b/src/application/services/folder_service.rs @@ -7,6 +7,7 @@ use crate::domain::repositories::folder_repository::FolderRepository; use crate::domain::services::path_service::StoragePath; use crate::infrastructure::repositories::pg::folder_db_repository::FolderDbRepository; use std::sync::Arc; +use uuid::Uuid; /// Implementation of the use case for folder operations pub struct FolderService { @@ -35,7 +36,7 @@ impl FolderService { async fn get_folder_owned( &self, _id: &str, - _caller_id: &str, + _caller_id: Uuid, ) -> Result { Ok(FolderDto::empty()) } @@ -54,7 +55,7 @@ impl FolderService { async fn list_folders_for_owner( &self, _parent_id: Option<&str>, - _owner_id: &str, + _owner_id: Uuid, ) -> Result, DomainError> { Ok(vec![]) } @@ -80,7 +81,7 @@ impl FolderService { async fn list_folders_for_owner_paginated( &self, _parent_id: Option<&str>, - _owner_id: &str, + _owner_id: Uuid, _pagination: &crate::application::dtos::pagination::PaginationRequestDto, ) -> Result< crate::application::dtos::pagination::PaginatedResponseDto, @@ -100,7 +101,7 @@ impl FolderService { &self, _id: &str, _dto: RenameFolderDto, - _caller_id: &str, + _caller_id: Uuid, ) -> Result { Ok(FolderDto::empty()) } @@ -109,18 +110,18 @@ impl FolderService { &self, _id: &str, _dto: MoveFolderDto, - _caller_id: &str, + _caller_id: Uuid, ) -> Result { Ok(FolderDto::empty()) } - async fn delete_folder(&self, _id: &str, _caller_id: &str) -> Result<(), DomainError> { + async fn delete_folder(&self, _id: &str, _caller_id: Uuid) -> Result<(), DomainError> { Ok(()) } async fn create_home_folder( &self, - _user_id: &str, + _user_id: Uuid, _name: String, ) -> Result { Ok(FolderDto::empty()) @@ -170,12 +171,13 @@ impl FolderUseCase for FolderService { /// Creates a root-level home folder for a user during registration. async fn create_home_folder( &self, - user_id: &str, + user_id: Uuid, name: String, ) -> Result { + let user_id_str = user_id.to_string(); let folder = self .folder_storage - .create_home_folder(user_id, name) + .create_home_folder(&user_id_str, name) .await .map_err(|e| { DomainError::internal_error( @@ -205,9 +207,10 @@ impl FolderUseCase for FolderService { } /// Gets a folder by its ID, enforcing that `caller_id` is the owner. - async fn get_folder_owned(&self, id: &str, caller_id: &str) -> Result { + async fn get_folder_owned(&self, id: &str, caller_id: Uuid) -> Result { + let caller_id_str = caller_id.to_string(); let folder_dto = self.get_folder(id).await?; - if folder_dto.owner_id.as_deref() != Some(caller_id) { + if folder_dto.owner_id.as_deref() != Some(caller_id_str.as_str()) { tracing::warn!( "get_folder_owned: user '{}' attempted to access folder '{}' owned by '{:?}'", caller_id, @@ -260,11 +263,12 @@ impl FolderUseCase for FolderService { async fn list_folders_for_owner( &self, parent_id: Option<&str>, - owner_id: &str, + owner_id: Uuid, ) -> Result, DomainError> { + let owner_id_str = owner_id.to_string(); let folders = self .folder_storage - .list_folders_by_owner(parent_id, owner_id) + .list_folders_by_owner(parent_id, &owner_id_str) .await .map_err(|e| { DomainError::internal_error( @@ -283,10 +287,10 @@ impl FolderUseCase for FolderService { "No root folders found for user {}, creating home folder automatically", owner_id ); - let folder_name = format!("My Folder - {}", &owner_id[..8.min(owner_id.len())]); + let folder_name = format!("My Folder - {}", &owner_id_str[..8.min(owner_id_str.len())]); match self .folder_storage - .create_home_folder(owner_id, folder_name.clone()) + .create_home_folder(&owner_id_str, folder_name.clone()) .await { Ok(home_folder) => { @@ -346,17 +350,18 @@ impl FolderUseCase for FolderService { async fn list_folders_for_owner_paginated( &self, parent_id: Option<&str>, - owner_id: &str, + owner_id: Uuid, pagination: &crate::application::dtos::pagination::PaginationRequestDto, ) -> Result, DomainError> { + let owner_id_str = owner_id.to_string(); let pagination = pagination.validate_and_adjust(); let (folders, total_items) = self .folder_storage .list_folders_by_owner_paginated( parent_id, - owner_id, + &owner_id_str, pagination.offset(), pagination.limit(), true, @@ -389,8 +394,9 @@ impl FolderUseCase for FolderService { &self, id: &str, dto: RenameFolderDto, - caller_id: &str, + caller_id: Uuid, ) -> Result { + let caller_id_str = caller_id.to_string(); // Input validation if dto.name.is_empty() { return Err(DomainError::new( @@ -408,7 +414,7 @@ impl FolderUseCase for FolderService { ) })?; - if existing_folder.owner_id() != Some(caller_id) { + if existing_folder.owner_id() != Some(caller_id_str.as_str()) { tracing::warn!( "rename_folder: user '{}' attempted to rename folder '{}' owned by '{:?}'", caller_id, @@ -438,8 +444,9 @@ impl FolderUseCase for FolderService { &self, id: &str, dto: MoveFolderDto, - caller_id: &str, + caller_id: Uuid, ) -> Result { + let caller_id_str = caller_id.to_string(); // Verify the source folder exists and belongs to the caller let source_folder = self.folder_storage.get_folder(id).await.map_err(|e| { DomainError::internal_error( @@ -448,7 +455,7 @@ impl FolderUseCase for FolderService { ) })?; - if source_folder.owner_id() != Some(caller_id) { + if source_folder.owner_id() != Some(caller_id_str.as_str()) { tracing::warn!( "move_folder: user '{}' attempted to move folder '{}' owned by '{:?}'", caller_id, @@ -495,7 +502,8 @@ impl FolderUseCase for FolderService { } /// Deletes a folder after verifying ownership. - async fn delete_folder(&self, id: &str, caller_id: &str) -> Result<(), DomainError> { + async fn delete_folder(&self, id: &str, caller_id: Uuid) -> Result<(), DomainError> { + let caller_id_str = caller_id.to_string(); // Verify the folder exists and belongs to the caller let folder = self.folder_storage.get_folder(id).await.map_err(|e| { DomainError::internal_error( @@ -504,7 +512,7 @@ impl FolderUseCase for FolderService { ) })?; - if folder.owner_id() != Some(caller_id) { + if folder.owner_id() != Some(caller_id_str.as_str()) { tracing::warn!( "delete_folder: user '{}' attempted to delete folder '{}' owned by '{:?}'", caller_id, diff --git a/src/application/services/recent_service.rs b/src/application/services/recent_service.rs index 0b06efe1..5632a5be 100755 --- a/src/application/services/recent_service.rs +++ b/src/application/services/recent_service.rs @@ -4,6 +4,7 @@ use crate::common::errors::{DomainError, ErrorKind, Result}; use crate::infrastructure::repositories::pg::RecentItemsPgRepository; use std::sync::Arc; use tracing::info; +use uuid::Uuid; /// Implementation of the use case for managing recent items. /// @@ -28,7 +29,7 @@ impl RecentItemsUseCase for RecentService { /// Get recent items for a user async fn get_recent_items( &self, - user_id: &str, + user_id: Uuid, limit: Option, ) -> Result> { info!("Getting recent items for user: {}", user_id); @@ -47,7 +48,7 @@ impl RecentItemsUseCase for RecentService { /// Record access to an item async fn record_item_access( &self, - user_id: &str, + user_id: Uuid, item_id: &str, item_type: &str, ) -> Result<()> { @@ -77,7 +78,7 @@ impl RecentItemsUseCase for RecentService { /// Remove an item from recent async fn remove_from_recent( &self, - user_id: &str, + user_id: Uuid, item_id: &str, item_type: &str, ) -> Result { @@ -101,7 +102,7 @@ impl RecentItemsUseCase for RecentService { } /// Clear all recent items - async fn clear_recent_items(&self, user_id: &str) -> Result<()> { + async fn clear_recent_items(&self, user_id: Uuid) -> Result<()> { info!("Clearing all recent items for user {}", user_id); self.repo.clear_all(user_id).await?; info!("Cleared all recent items for user {}", user_id); diff --git a/src/application/services/search_service.rs b/src/application/services/search_service.rs index 034b83f2..934723c2 100755 --- a/src/application/services/search_service.rs +++ b/src/application/services/search_service.rs @@ -19,6 +19,7 @@ use crate::domain::repositories::folder_repository::FolderRepository; use crate::infrastructure::repositories::pg::file_blob_read_repository::FileBlobReadRepository; use crate::infrastructure::repositories::pg::folder_db_repository::FolderDbRepository; use std::hash::{Hash, Hasher}; +use uuid::Uuid; /** * High-performance search service implementation for files and folders. @@ -285,12 +286,13 @@ impl SearchUseCase for SearchService { async fn search( &self, criteria: SearchCriteriaDto, - user_id: &str, + user_id: Uuid, ) -> Result> { let start = Instant::now(); + let user_id_str = user_id.to_string(); // Try to get from cache - let cache_key = Self::create_cache_key(&criteria, user_id); + let cache_key = Self::create_cache_key(&criteria, &user_id_str); if let Some(cached_results) = self.get_from_cache(cache_key).await { return Ok(cached_results); } @@ -321,7 +323,7 @@ impl SearchUseCase for SearchService { .search_folders( criteria.folder_id.as_deref(), criteria.name_contains.as_deref(), - user_id, + &user_id_str, false, ) .await?; @@ -403,7 +405,7 @@ impl SearchUseCase for SearchService { .search_folders( criteria.folder_id.as_deref(), criteria.name_contains.as_deref(), - user_id, + &user_id_str, true, ) .await?; @@ -504,7 +506,7 @@ impl SearchService { async fn search( &self, _criteria: SearchCriteriaDto, - _user_id: &str, + _user_id: Uuid, ) -> Result> { Ok(Arc::new(SearchResultsDto::empty())) } diff --git a/src/application/services/share_service.rs b/src/application/services/share_service.rs index 2089d3fe..1d88f22f 100755 --- a/src/application/services/share_service.rs +++ b/src/application/services/share_service.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use thiserror::Error; use tokio::sync::Semaphore; +use uuid::Uuid; use crate::domain::repositories::folder_repository::FolderRepository; use crate::infrastructure::repositories::pg::SharePgRepository; @@ -150,7 +151,7 @@ impl ShareService { /// but belongs to a different user — this prevents share-ID enumeration /// attacks where an attacker probes IDs and uses 403-vs-404 to learn /// which ones are valid. - async fn fetch_owned_share(&self, id: &str, requester_id: &str) -> Result { + async fn fetch_owned_share(&self, id: Uuid, requester_id: Uuid) -> Result { let share = self .share_repository .find_share_by_id_for_user(id, requester_id) @@ -163,7 +164,7 @@ impl ShareService { impl ShareUseCase for ShareService { async fn create_shared_link( &self, - user_id: &str, + user_id: Uuid, dto: CreateShareDto, ) -> Result { // Convert the item type @@ -187,7 +188,7 @@ impl ShareUseCase for ShareService { dto.item_id.clone(), dto.item_name.clone(), item_type, - user_id.to_string(), + user_id, permissions, password_hash, dto.expires_at, @@ -205,7 +206,7 @@ impl ShareUseCase for ShareService { Ok(ShareDto::from_entity(&saved_share, &self.config.base_url())) } - async fn get_shared_link(&self, id: &str, requester_id: &str) -> Result { + async fn get_shared_link(&self, id: Uuid, requester_id: Uuid) -> Result { // SECURITY: ownership-verified lookup — returns 404 if the share // doesn't exist OR belongs to another user. let share = self.fetch_owned_share(id, requester_id).await?; @@ -253,7 +254,7 @@ impl ShareUseCase for ShareService { &self, item_id: &str, item_type: &ShareItemType, - requester_id: &str, + requester_id: Uuid, ) -> Result, DomainError> { // SECURITY: only return shares created by the requester let shares = self @@ -276,8 +277,8 @@ impl ShareUseCase for ShareService { async fn update_shared_link( &self, - id: &str, - requester_id: &str, + id: Uuid, + requester_id: Uuid, dto: UpdateShareDto, ) -> Result { // SECURITY: ownership-verified lookup — prevents IDOR @@ -322,7 +323,7 @@ impl ShareUseCase for ShareService { )) } - async fn delete_shared_link(&self, id: &str, requester_id: &str) -> Result<(), DomainError> { + async fn delete_shared_link(&self, id: Uuid, requester_id: Uuid) -> Result<(), DomainError> { // SECURITY: ownership-verified delete — only the creator can remove self.share_repository .delete_share_for_user(id, requester_id) @@ -333,7 +334,7 @@ impl ShareUseCase for ShareService { async fn get_user_shared_links( &self, - user_id: &str, + user_id: Uuid, page: usize, per_page: usize, ) -> Result, DomainError> { @@ -519,7 +520,7 @@ mod tests { { async fn create_shared_link( &self, - user_id: &str, + user_id: Uuid, dto: CreateShareDto, ) -> Result { let item_type = ShareItemType::try_from(dto.item_type.as_str()) @@ -534,7 +535,7 @@ mod tests { dto.item_id.clone(), dto.item_name.clone(), item_type, - user_id.to_string(), + user_id, permissions, password_hash, dto.expires_at, @@ -550,8 +551,8 @@ mod tests { async fn get_shared_link( &self, - id: &str, - requester_id: &str, + id: Uuid, + requester_id: Uuid, ) -> Result { let share = self .share_repository @@ -584,7 +585,7 @@ mod tests { &self, item_id: &str, item_type: &ShareItemType, - requester_id: &str, + requester_id: Uuid, ) -> Result, DomainError> { let shares = self .share_repository @@ -600,8 +601,8 @@ mod tests { async fn update_shared_link( &self, - id: &str, - requester_id: &str, + id: Uuid, + requester_id: Uuid, dto: UpdateShareDto, ) -> Result { let mut share = self @@ -635,8 +636,8 @@ mod tests { async fn delete_shared_link( &self, - id: &str, - requester_id: &str, + id: Uuid, + requester_id: Uuid, ) -> Result<(), DomainError> { self.share_repository .delete_share_for_user(id, requester_id) @@ -647,7 +648,7 @@ mod tests { async fn get_user_shared_links( &self, - user_id: &str, + user_id: Uuid, page: usize, per_page: usize, ) -> Result, DomainError> { @@ -1018,28 +1019,30 @@ mod tests { async fn find_share_by_id_for_user( &self, - id: &str, - user_id: &str, + id: Uuid, + user_id: Uuid, ) -> Result { let shares = self.shares.lock().unwrap(); + let id_str = id.to_string(); shares - .get(id) + .get(&id_str) .filter(|s| s.created_by() == user_id) .cloned() - .ok_or_else(|| DomainError::not_found("Share", id)) + .ok_or_else(|| DomainError::not_found("Share", &id_str)) } - async fn delete_share_for_user(&self, id: &str, user_id: &str) -> Result<(), DomainError> { + async fn delete_share_for_user(&self, id: Uuid, user_id: Uuid) -> Result<(), DomainError> { let mut shares = self.shares.lock().unwrap(); let mut tokens = self.tokens.lock().unwrap(); + let id_str = id.to_string(); let share = shares - .get(id) + .get(&id_str) .filter(|s| s.created_by() == user_id) - .ok_or_else(|| DomainError::not_found("Share", id))?; + .ok_or_else(|| DomainError::not_found("Share", &id_str))?; tokens.remove(share.token()); - shares.remove(id); + shares.remove(&id_str); Ok(()) } @@ -1047,7 +1050,7 @@ mod tests { &self, item_id: &str, item_type: &ShareItemType, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let shares = self.shares.lock().unwrap(); let type_str = item_type.to_string(); @@ -1078,7 +1081,7 @@ mod tests { async fn find_shares_by_user( &self, - user_id: &str, + user_id: Uuid, offset: usize, limit: usize, ) -> Result<(Vec, usize), DomainError> { @@ -1125,7 +1128,7 @@ mod tests { }), }; - let result = service.create_shared_link("user123", dto).await; + let result = service.create_shared_link(Uuid::new_v4(), dto).await; assert!(result.is_ok()); let share_dto = result.unwrap(); diff --git a/src/application/services/storage_usage_service.rs b/src/application/services/storage_usage_service.rs index 9221617b..0451f251 100755 --- a/src/application/services/storage_usage_service.rs +++ b/src/application/services/storage_usage_service.rs @@ -6,6 +6,7 @@ use sqlx::PgPool; use std::sync::Arc; use tokio::task; use tracing::{debug, error, info}; +use uuid::Uuid; /** * Service for managing and updating user storage usage statistics. @@ -31,7 +32,7 @@ impl StorageUsageService { } /// Calculates and updates storage usage for a specific user - pub async fn update_user_storage_usage(&self, user_id: &str) -> Result { + pub async fn update_user_storage_usage(&self, user_id: Uuid) -> Result { info!("Updating storage usage for user: {}", user_id); // Calculate storage usage directly from database @@ -52,12 +53,11 @@ impl StorageUsageService { /// Calculates a user's storage usage by summing all their file sizes. /// Uses a direct SQL query for O(1) performance. - async fn calculate_user_storage_usage(&self, user_id: &str) -> Result { + async fn calculate_user_storage_usage(&self, user_id: Uuid) -> Result { debug!("Calculating storage for user: {}", user_id); // Direct SQL query to sum all file sizes for this user // This is much more efficient than recursively walking folders - // Note: user_id is stored as varchar, not uuid, so we bind it directly as text let total_size: i64 = sqlx::query_scalar( r#" SELECT COALESCE(SUM(size), 0)::bigint @@ -88,14 +88,14 @@ impl StorageUsageService { info!("Updating storage usage for username: {}", username); let user = self.user_repository.get_user_by_username(username).await?; - let user_id = user.id().to_string(); + let user_id = user.id(); // Reuse the existing calculation logic - let total_usage = self.calculate_user_storage_usage(&user_id).await?; + let total_usage = self.calculate_user_storage_usage(user_id).await?; // Update the user's storage usage in the database self.user_repository - .update_storage_usage(&user_id, total_usage) + .update_storage_usage(user_id, total_usage) .await?; info!( @@ -112,7 +112,7 @@ impl StorageUsageService { * to the application layer. */ impl StorageUsagePort for StorageUsageService { - async fn update_user_storage_usage(&self, user_id: &str) -> Result { + async fn update_user_storage_usage(&self, user_id: Uuid) -> Result { StorageUsageService::update_user_storage_usage(self, user_id).await } @@ -133,12 +133,12 @@ impl StorageUsagePort for StorageUsageService { // Process users in parallel for user in users { - let user_id = user.id().to_string(); + let user_id = user.id(); let service_clone = self.clone(); // Spawn a background task for each user let task = task::spawn(async move { - match service_clone.update_user_storage_usage(&user_id).await { + match service_clone.update_user_storage_usage(user_id).await { Ok(usage) => { debug!( "Updated storage usage for user {}: {} bytes", @@ -168,7 +168,7 @@ impl StorageUsagePort for StorageUsageService { async fn check_storage_quota( &self, - user_id: &str, + user_id: Uuid, additional_bytes: u64, ) -> Result<(), DomainError> { let user = self.user_repository.get_user_by_id(user_id).await?; @@ -206,7 +206,7 @@ impl StorageUsagePort for StorageUsageService { Ok(()) } - async fn get_user_storage_info(&self, user_id: &str) -> Result<(i64, i64), DomainError> { + async fn get_user_storage_info(&self, user_id: Uuid) -> Result<(i64, i64), DomainError> { let user = self.user_repository.get_user_by_id(user_id).await?; Ok((user.storage_used_bytes(), user.storage_quota_bytes())) } diff --git a/src/application/services/trash_service.rs b/src/application/services/trash_service.rs index 5697e1fa..434a9f5f 100755 --- a/src/application/services/trash_service.rs +++ b/src/application/services/trash_service.rs @@ -123,13 +123,10 @@ impl TrashService { impl TrashUseCase for TrashService { #[instrument(skip(self))] - async fn get_trash_items(&self, user_id: &str) -> Result> { + async fn get_trash_items(&self, user_id: Uuid) -> Result> { debug!("Getting trash items for user: {}", user_id); - 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?; + let items = self.trash_repository.get_trash_items(&user_id).await?; let dtos = items.into_iter().map(|item| self.to_dto(item)).collect(); @@ -137,7 +134,7 @@ impl TrashUseCase for TrashService { } #[instrument(skip(self))] - 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<()> { info!( "Moving to trash: type={}, id={}, user={}", item_type, item_id, user_id @@ -163,20 +160,7 @@ impl TrashUseCase for TrashService { } }; - debug!("Validating user UUID: {}", user_id); - let user_uuid = match Uuid::parse_str(user_id) { - Ok(uuid) => { - debug!("Valid user UUID: {}", uuid); - uuid - } - Err(e) => { - error!("Invalid user UUID: {} - Error: {}", user_id, e); - return Err(DomainError::validation_error(format!( - "Invalid user ID: {}", - e - ))); - } - }; + let user_uuid = user_id; match item_type { "file" => { @@ -276,7 +260,7 @@ impl TrashUseCase for TrashService { // Ownership check — return NotFound (not Forbidden) to // prevent leaking whether the folder exists. - if folder.owner_id().is_none_or(|o| o != user_id) { + if folder.owner_id().is_none_or(|o| o != user_id.to_string()) { return Err(DomainError::not_found( "Folder", format!("Folder not found: {}", item_id), @@ -331,7 +315,7 @@ impl TrashUseCase for TrashService { } #[instrument(skip(self))] - async fn restore_item(&self, trash_id: &str, user_id: &str) -> Result<()> { + async fn restore_item(&self, trash_id: &str, user_id: Uuid) -> Result<()> { info!("Restoring item {} for user {}", trash_id, user_id); let trash_uuid = match Uuid::parse_str(trash_id) { @@ -348,19 +332,7 @@ impl TrashUseCase for TrashService { } }; - let user_uuid = match Uuid::parse_str(user_id) { - Ok(id) => { - info!("User UUID parsed successfully: {}", id); - id - } - Err(e) => { - error!("Invalid user ID format: {} - {}", user_id, e); - return Err(DomainError::validation_error(format!( - "Invalid user ID: {}", - e - ))); - } - }; + let user_uuid = user_id; // Get the trash item info!("Retrieving trash item from repository: ID={}", trash_id); @@ -514,7 +486,7 @@ impl TrashUseCase for TrashService { } #[instrument(skip(self))] - async fn delete_permanently(&self, trash_id: &str, user_id: &str) -> Result<()> { + async fn delete_permanently(&self, trash_id: &str, user_id: Uuid) -> Result<()> { info!( "Permanently deleting item {} for user {}", trash_id, user_id @@ -534,19 +506,7 @@ impl TrashUseCase for TrashService { } }; - let user_uuid = match Uuid::parse_str(user_id) { - Ok(id) => { - info!("User UUID parsed successfully: {}", id); - id - } - Err(e) => { - error!("Invalid user ID format: {} - {}", user_id, e); - return Err(DomainError::validation_error(format!( - "Invalid user ID: {}", - e - ))); - } - }; + let user_uuid = user_id; // Get the trash item info!("Retrieving trash item from repository: ID={}", trash_id); @@ -684,12 +644,9 @@ impl TrashUseCase for TrashService { } #[instrument(skip(self))] - async fn empty_trash(&self, user_id: &str) -> Result<()> { + async fn empty_trash(&self, user_id: Uuid) -> Result<()> { info!("Emptying trash for user {}", user_id); - let user_uuid = Uuid::parse_str(user_id) - .map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?; - // clear_trash() already performs bulk SQL DELETEs in 2 queries: // 1. DELETE FROM storage.files WHERE user_id = $1 AND is_trashed = TRUE // 2. DELETE FROM storage.folders WHERE user_id = $1 AND is_trashed = TRUE @@ -700,7 +657,7 @@ impl TrashUseCase for TrashService { // remove_reference() call is needed. // // Finally it clears the trash_items index for the user. - self.trash_repository.clear_trash(&user_uuid).await?; + self.trash_repository.clear_trash(&user_id).await?; info!("Trash emptied for user {}", user_id); Ok(()) diff --git a/src/common/stubs.rs b/src/common/stubs.rs index 08cde4ea..341d9a0e 100755 --- a/src/common/stubs.rs +++ b/src/common/stubs.rs @@ -12,6 +12,7 @@ use std::sync::Arc; use bytes::Bytes; use futures::Stream; +use uuid::Uuid; use crate::application::dtos::file_dto::FileDto; use crate::application::dtos::folder_dto::{ @@ -108,7 +109,7 @@ impl FileReadPort for StubFileReadPort { &self, _folder_id: Option<&str>, _criteria: &SearchCriteriaDto, - _user_id: &str, + _user_id: Uuid, ) -> Result<(Vec, usize), DomainError> { Ok((Vec::new(), 0)) } @@ -117,7 +118,7 @@ impl FileReadPort for StubFileReadPort { &self, _folder_id: Option<&str>, _criteria: &SearchCriteriaDto, - _user_id: &str, + _user_id: Uuid, ) -> Result { Ok(0) } @@ -129,7 +130,7 @@ impl FileReadPort for StubFileReadPort { Ok(Box::pin(futures::stream::empty())) } - async fn get_file_for_owner(&self, _id: &str, _owner_id: &str) -> Result { + async fn get_file_for_owner(&self, _id: &str, _owner_id: Uuid) -> Result { Ok(File::default()) } } @@ -362,7 +363,7 @@ impl FolderUseCase for StubFolderUseCase { async fn get_folder_owned( &self, _id: &str, - _caller_id: &str, + _caller_id: Uuid, ) -> Result { Ok(FolderDto::default()) } @@ -378,7 +379,7 @@ impl FolderUseCase for StubFolderUseCase { async fn list_folders_for_owner( &self, _parent_id: Option<&str>, - _owner_id: &str, + _owner_id: Uuid, ) -> Result, DomainError> { Ok(Vec::new()) } @@ -394,7 +395,7 @@ impl FolderUseCase for StubFolderUseCase { async fn list_folders_for_owner_paginated( &self, _parent_id: Option<&str>, - _owner_id: &str, + _owner_id: Uuid, _pagination: &PaginationRequestDto, ) -> Result, DomainError> { Ok(PaginatedResponseDto::new(Vec::new(), 0, 10, 0)) @@ -404,7 +405,7 @@ impl FolderUseCase for StubFolderUseCase { &self, _id: &str, _dto: RenameFolderDto, - _caller_id: &str, + _caller_id: Uuid, ) -> Result { Ok(FolderDto::default()) } @@ -413,18 +414,18 @@ impl FolderUseCase for StubFolderUseCase { &self, _id: &str, _dto: MoveFolderDto, - _caller_id: &str, + _caller_id: Uuid, ) -> Result { Ok(FolderDto::default()) } - async fn delete_folder(&self, _id: &str, _caller_id: &str) -> Result<(), DomainError> { + async fn delete_folder(&self, _id: &str, _caller_id: Uuid) -> Result<(), DomainError> { Ok(()) } async fn create_home_folder( &self, - _user_id: &str, + _user_id: Uuid, _name: String, ) -> Result { Ok(FolderDto::default()) @@ -510,7 +511,7 @@ impl FileRetrievalUseCase for StubFileRetrievalUseCase { async fn list_files_owned( &self, _folder_id: Option<&str>, - _owner_id: &str, + _owner_id: Uuid, ) -> Result, DomainError> { Ok(Vec::new()) } @@ -526,7 +527,7 @@ impl FileRetrievalUseCase for StubFileRetrievalUseCase { async fn get_file_stream_owned( &self, _id: &str, - _caller_id: &str, + _caller_id: Uuid, ) -> Result> + Send>, DomainError> { let empty_stream = futures::stream::empty::>(); Ok(Box::new(empty_stream)) @@ -569,14 +570,14 @@ impl FileRetrievalUseCase for StubFileRetrievalUseCase { Ok(Box::pin(futures::stream::empty())) } - async fn get_file_owned(&self, _id: &str, _caller_id: &str) -> Result { + async fn get_file_owned(&self, _id: &str, _caller_id: Uuid) -> Result { Ok(FileDto::default()) } async fn get_file_optimized_owned( &self, _id: &str, - _caller_id: &str, + _caller_id: Uuid, _accept_webp: bool, _prefer_original: bool, ) -> Result<(FileDto, OptimizedFileContent), DomainError> { @@ -593,7 +594,7 @@ impl FileRetrievalUseCase for StubFileRetrievalUseCase { async fn get_file_range_stream_owned( &self, _id: &str, - _caller_id: &str, + _caller_id: Uuid, _start: u64, _end: Option, ) -> Result> + Send>, DomainError> { @@ -628,7 +629,7 @@ impl FileManagementUseCase for StubFileManagementUseCase { async fn copy_file_owned( &self, _file_id: &str, - _caller_id: &str, + _caller_id: Uuid, _folder_id: Option, ) -> Result { Ok(FileDto::default()) @@ -642,18 +643,18 @@ impl FileManagementUseCase for StubFileManagementUseCase { Ok(()) } - async fn delete_file_owned(&self, _id: &str, _caller_id: &str) -> Result<(), DomainError> { + async fn delete_file_owned(&self, _id: &str, _caller_id: Uuid) -> Result<(), DomainError> { Ok(()) } - async fn delete_with_cleanup(&self, _id: &str, _user_id: &str) -> Result { + async fn delete_with_cleanup(&self, _id: &str, _user_id: Uuid) -> Result { Ok(false) } async fn move_file_owned( &self, _file_id: &str, - _caller_id: &str, + _caller_id: Uuid, _folder_id: Option, ) -> Result { Ok(FileDto::default()) @@ -662,7 +663,7 @@ impl FileManagementUseCase for StubFileManagementUseCase { async fn rename_file_owned( &self, _file_id: &str, - _caller_id: &str, + _caller_id: Uuid, _new_name: &str, ) -> Result { Ok(FileDto::default()) @@ -679,7 +680,7 @@ impl SearchUseCase for StubSearchUseCase { async fn search( &self, _criteria: SearchCriteriaDto, - _user_id: &str, + _user_id: Uuid, ) -> Result, DomainError> { Ok(Arc::new(SearchResultsDto::empty())) } diff --git a/src/domain/entities/app_password.rs b/src/domain/entities/app_password.rs index f283cdce..e5401f58 100755 --- a/src/domain/entities/app_password.rs +++ b/src/domain/entities/app_password.rs @@ -11,9 +11,9 @@ use uuid::Uuid; #[derive(Debug, Clone)] pub struct AppPassword { /// Unique identifier. - pub id: String, + pub id: Uuid, /// Owner user ID. - pub user_id: String, + pub user_id: Uuid, /// Human-readable label chosen by the user (e.g. "DAVx5 on Pixel 8"). pub label: String, /// Argon2 hash of the generated password token. @@ -41,7 +41,7 @@ impl AppPassword { /// The caller is responsible for hashing the raw token and passing /// the hash and prefix. pub fn new( - user_id: String, + user_id: Uuid, label: String, password_hash: String, prefix: String, @@ -49,7 +49,7 @@ impl AppPassword { expires_at: Option>, ) -> Self { Self { - id: Uuid::new_v4().to_string(), + id: Uuid::new_v4(), user_id, label, password_hash, diff --git a/src/domain/entities/device_code.rs b/src/domain/entities/device_code.rs index eb216658..677660cd 100755 --- a/src/domain/entities/device_code.rs +++ b/src/domain/entities/device_code.rs @@ -48,13 +48,13 @@ impl std::fmt::Display for DeviceCodeStatus { /// Domain entity for a Device Authorization flow. #[derive(Debug, Clone)] pub struct DeviceCode { - id: String, + id: Uuid, device_code: String, user_code: String, client_name: String, scopes: String, status: DeviceCodeStatus, - user_id: Option, + user_id: Option, access_token: Option, refresh_token: Option, verification_uri: String, @@ -89,7 +89,7 @@ impl DeviceCode { ) -> Self { let now = Utc::now(); Self { - id: Uuid::new_v4().to_string(), + id: Uuid::new_v4(), device_code, user_code, client_name, @@ -111,13 +111,13 @@ impl DeviceCode { /// Reconstruct from database row. #[allow(clippy::too_many_arguments)] pub fn from_raw( - id: String, + id: Uuid, device_code: String, user_code: String, client_name: String, scopes: String, status: DeviceCodeStatus, - user_id: Option, + user_id: Option, access_token: Option, refresh_token: Option, verification_uri: String, @@ -150,8 +150,8 @@ impl DeviceCode { // ── Getters ────────────────────────────────────────────────── - pub fn id(&self) -> &str { - &self.id + pub fn id(&self) -> Uuid { + self.id } pub fn device_code(&self) -> &str { @@ -174,8 +174,8 @@ impl DeviceCode { self.status } - pub fn user_id(&self) -> Option<&str> { - self.user_id.as_deref() + pub fn user_id(&self) -> Option { + self.user_id } pub fn access_token(&self) -> Option<&str> { @@ -243,7 +243,7 @@ impl DeviceCode { } /// Authorize this device code for a specific user, storing the tokens. - pub fn authorize(&mut self, user_id: String, access_token: String, refresh_token: String) { + pub fn authorize(&mut self, user_id: Uuid, access_token: String, refresh_token: String) { self.status = DeviceCodeStatus::Authorized; self.user_id = Some(user_id); self.access_token = Some(access_token); diff --git a/src/domain/entities/session.rs b/src/domain/entities/session.rs index 11ac60f3..17e33be0 100755 --- a/src/domain/entities/session.rs +++ b/src/domain/entities/session.rs @@ -3,8 +3,8 @@ use uuid::Uuid; #[derive(Debug, Clone)] pub struct Session { - id: String, - user_id: String, + id: Uuid, + user_id: Uuid, refresh_token: String, expires_at: DateTime, ip_address: Option, @@ -15,22 +15,19 @@ pub struct Session { impl Session { pub fn new( - user_id: String, + user_id: Uuid, refresh_token: String, ip_address: Option, user_agent: Option, expires_in_days: i64, ) -> Self { - if user_id.is_empty() { - panic!("Session user_id cannot be empty"); - } if refresh_token.is_empty() { panic!("Session refresh_token cannot be empty"); } let now = Utc::now(); Self { - id: Uuid::new_v4().to_string(), + id: Uuid::new_v4(), user_id, refresh_token, expires_at: now + Duration::days(expires_in_days), @@ -43,8 +40,8 @@ impl Session { #[allow(clippy::too_many_arguments)] pub fn from_raw( - id: String, - user_id: String, + id: Uuid, + user_id: Uuid, refresh_token: String, expires_at: DateTime, ip_address: Option, @@ -65,12 +62,12 @@ impl Session { } // Getters - pub fn id(&self) -> &str { - &self.id + pub fn id(&self) -> Uuid { + self.id } - pub fn user_id(&self) -> &str { - &self.user_id + pub fn user_id(&self) -> Uuid { + self.user_id } pub fn refresh_token(&self) -> &str { diff --git a/src/domain/entities/share.rs b/src/domain/entities/share.rs index 4ba9b1ce..68915229 100755 --- a/src/domain/entities/share.rs +++ b/src/domain/entities/share.rs @@ -6,7 +6,7 @@ pub use super::entity_errors::ShareError; #[derive(Debug, Clone, PartialEq)] pub struct Share { - id: String, + id: Uuid, item_id: String, item_name: Option, item_type: ShareItemType, @@ -15,7 +15,7 @@ pub struct Share { expires_at: Option, permissions: SharePermissions, created_at: u64, - created_by: String, + created_by: Uuid, access_count: u64, } @@ -37,7 +37,7 @@ impl Share { item_id: String, item_name: Option, item_type: ShareItemType, - created_by: String, + created_by: Uuid, permissions: Option, password_hash: Option, expires_at: Option, @@ -69,7 +69,7 @@ impl Share { .as_secs(); Ok(Self { - id: Uuid::new_v4().to_string(), + id: Uuid::new_v4(), item_id, item_name, item_type, @@ -89,7 +89,7 @@ impl Share { #[allow(clippy::too_many_arguments)] pub fn from_raw( - id: String, + id: Uuid, item_id: String, item_name: Option, item_type: ShareItemType, @@ -98,7 +98,7 @@ impl Share { expires_at: Option, permissions: SharePermissions, created_at: u64, - created_by: String, + created_by: Uuid, access_count: u64, ) -> Self { Self { @@ -118,8 +118,8 @@ impl Share { // ── Getters ── - pub fn id(&self) -> &str { - &self.id + pub fn id(&self) -> Uuid { + self.id } pub fn item_id(&self) -> &str { @@ -150,8 +150,8 @@ impl Share { self.created_at } - pub fn created_by(&self) -> &str { - &self.created_by + pub fn created_by(&self) -> Uuid { + self.created_by } pub fn access_count(&self) -> u64 { @@ -262,13 +262,18 @@ impl TryFrom<&str> for ShareItemType { mod tests { use super::*; + fn test_user_id() -> Uuid { + Uuid::new_v4() + } + #[test] fn test_create_share() { + let uid = test_user_id(); let share = Share::new( "test_file_id".to_string(), None, ShareItemType::File, - "user123".to_string(), + uid, None, None, None, @@ -277,7 +282,7 @@ mod tests { assert_eq!(share.item_id(), "test_file_id"); assert_eq!(*share.item_type(), ShareItemType::File); - assert_eq!(share.created_by(), "user123"); + assert_eq!(share.created_by(), uid); assert!(share.permissions().read()); assert!(!share.permissions().write()); assert!(!share.permissions().reshare()); @@ -299,7 +304,7 @@ mod tests { "test_file_id".to_string(), None, ShareItemType::File, - "user123".to_string(), + test_user_id(), None, None, Some(future), @@ -314,7 +319,7 @@ mod tests { "test_file_id".to_string(), None, ShareItemType::File, - "user123".to_string(), + test_user_id(), None, None, Some(past), @@ -349,7 +354,7 @@ mod tests { "test_file_id".to_string(), None, ShareItemType::File, - "user123".to_string(), + test_user_id(), None, Some("some_hash_value".to_string()), None, @@ -366,7 +371,7 @@ mod tests { "test_file_id".to_string(), None, ShareItemType::File, - "user123".to_string(), + test_user_id(), None, None, // No password None, diff --git a/src/domain/entities/user.rs b/src/domain/entities/user.rs index 519f7724..f815b101 100755 --- a/src/domain/entities/user.rs +++ b/src/domain/entities/user.rs @@ -22,7 +22,7 @@ impl std::fmt::Display for UserRole { #[derive(Debug, Clone)] pub struct User { - id: String, + id: Uuid, username: String, email: String, password_hash: String, @@ -70,7 +70,7 @@ impl User { let now = Utc::now(); Ok(Self { - id: Uuid::new_v4().to_string(), + id: Uuid::new_v4(), username, email, password_hash, @@ -99,7 +99,7 @@ impl User { Self::validate_email(&email)?; let now = Utc::now(); Ok(Self { - id: Uuid::new_v4().to_string(), + id: Uuid::new_v4(), username, email, password_hash: "__OIDC_NO_PASSWORD__".to_string(), @@ -117,7 +117,7 @@ impl User { #[allow(clippy::too_many_arguments)] pub fn from_data( - id: String, + id: Uuid, username: String, email: String, password_hash: String, @@ -148,7 +148,7 @@ impl User { #[allow(clippy::too_many_arguments)] pub fn from_data_full( - id: String, + id: Uuid, username: String, email: String, password_hash: String, @@ -180,8 +180,8 @@ impl User { } // Getters - pub fn id(&self) -> &str { - &self.id + pub fn id(&self) -> Uuid { + self.id } pub fn username(&self) -> &str { diff --git a/src/domain/repositories/address_book_repository.rs b/src/domain/repositories/address_book_repository.rs index a08943e8..7adbf6d2 100755 --- a/src/domain/repositories/address_book_repository.rs +++ b/src/domain/repositories/address_book_repository.rs @@ -22,23 +22,23 @@ pub trait AddressBookRepository: Send + Sync + 'static { ) -> AddressBookRepositoryResult>; async fn get_address_books_by_owner( &self, - owner_id: &str, + owner_id: Uuid, ) -> AddressBookRepositoryResult>; async fn get_shared_address_books( &self, - user_id: &str, + user_id: Uuid, ) -> AddressBookRepositoryResult>; async fn get_public_address_books(&self) -> AddressBookRepositoryResult>; async fn share_address_book( &self, address_book_id: &Uuid, - user_id: &str, + user_id: Uuid, can_write: bool, ) -> AddressBookRepositoryResult<()>; async fn unshare_address_book( &self, address_book_id: &Uuid, - user_id: &str, + user_id: Uuid, ) -> AddressBookRepositoryResult<()>; async fn get_address_book_shares( &self, diff --git a/src/domain/repositories/calendar_repository.rs b/src/domain/repositories/calendar_repository.rs index a5cba3e4..2ba3ead8 100755 --- a/src/domain/repositories/calendar_repository.rs +++ b/src/domain/repositories/calendar_repository.rs @@ -21,20 +21,20 @@ pub trait CalendarRepository: Send + Sync + 'static { /// Lists all calendars for a specific user async fn list_calendars_by_owner( &self, - owner_id: &str, + owner_id: Uuid, ) -> CalendarRepositoryResult>; /// Finds a calendar by name and owner async fn find_calendar_by_name_and_owner( &self, name: &str, - owner_id: &str, + owner_id: Uuid, ) -> CalendarRepositoryResult; /// Lists calendars shared with a specific user async fn list_calendars_shared_with_user( &self, - user_id: &str, + user_id: Uuid, ) -> CalendarRepositoryResult>; /// List public calendars @@ -48,7 +48,7 @@ pub trait CalendarRepository: Send + Sync + 'static { async fn user_has_calendar_access( &self, calendar_id: &Uuid, - user_id: &str, + user_id: Uuid, ) -> CalendarRepositoryResult; /// Gets a custom property for a calendar @@ -83,7 +83,7 @@ pub trait CalendarRepository: Send + Sync + 'static { async fn share_calendar( &self, calendar_id: &Uuid, - user_id: &str, + user_id: Uuid, access_level: &str, ) -> CalendarRepositoryResult<()>; @@ -91,7 +91,7 @@ pub trait CalendarRepository: Send + Sync + 'static { async fn remove_calendar_sharing( &self, calendar_id: &Uuid, - user_id: &str, + user_id: Uuid, ) -> CalendarRepositoryResult<()>; /// Get calendar sharing information (who has access to this calendar) diff --git a/src/domain/repositories/session_repository.rs b/src/domain/repositories/session_repository.rs index 1c2a747f..84966d74 100755 --- a/src/domain/repositories/session_repository.rs +++ b/src/domain/repositories/session_repository.rs @@ -1,5 +1,6 @@ use crate::common::errors::DomainError; use crate::domain::entities::session::Session; +use uuid::Uuid; #[derive(Debug, thiserror::Error)] pub enum SessionRepositoryError { @@ -33,7 +34,7 @@ pub trait SessionRepository: Send + Sync + 'static { async fn create_session(&self, session: Session) -> SessionRepositoryResult; /// Gets a session by ID - async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult; + async fn get_session_by_id(&self, id: Uuid) -> SessionRepositoryResult; /// Gets a session by refresh token async fn get_session_by_refresh_token( @@ -42,14 +43,14 @@ pub trait SessionRepository: Send + Sync + 'static { ) -> SessionRepositoryResult; /// Gets all sessions for a user - async fn get_sessions_by_user_id(&self, user_id: &str) + async fn get_sessions_by_user_id(&self, user_id: Uuid) -> SessionRepositoryResult>; /// Revokes a specific session - async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()>; + async fn revoke_session(&self, session_id: Uuid) -> SessionRepositoryResult<()>; /// Revokes all sessions for a user - async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult; + async fn revoke_all_user_sessions(&self, user_id: Uuid) -> SessionRepositoryResult; /// Deletes expired sessions async fn delete_expired_sessions(&self) -> SessionRepositoryResult; diff --git a/src/domain/repositories/settings_repository.rs b/src/domain/repositories/settings_repository.rs index e921d3d9..d2e8d627 100755 --- a/src/domain/repositories/settings_repository.rs +++ b/src/domain/repositories/settings_repository.rs @@ -1,5 +1,6 @@ use crate::common::errors::DomainError; use std::collections::HashMap; +use uuid::Uuid; /// Repository for platform settings stored in the database. /// Settings are key-value pairs organized by category (e.g., "oidc", "general"). @@ -18,7 +19,7 @@ pub trait SettingsRepository: Send + Sync + 'static { value: &str, category: &str, is_secret: bool, - updated_by: Option<&str>, + updated_by: Option, ) -> Result<(), DomainError>; /// Delete a setting by key @@ -34,7 +35,7 @@ pub trait SettingsRepository: Send + Sync + 'static { /// The default implementation falls back to the non-atomic /// get-then-set pattern for repositories that don't support a native /// atomic upsert. - async fn try_claim_initialization(&self, admin_user_id: &str) -> Result { + async fn try_claim_initialization(&self, admin_user_id: Uuid) -> Result { // Default: non-atomic fallback (overridden by PG implementation) match self.get("system_initialized").await? { Some(v) if v == "true" => Ok(false), diff --git a/src/domain/repositories/share_repository.rs b/src/domain/repositories/share_repository.rs index 1639b1b1..14ac57ab 100755 --- a/src/domain/repositories/share_repository.rs +++ b/src/domain/repositories/share_repository.rs @@ -1,4 +1,5 @@ use thiserror::Error; +use uuid::Uuid; use crate::domain::{ entities::share::{Share, ShareItemType}, @@ -24,7 +25,7 @@ pub trait ShareRepository: Send + Sync + 'static { async fn save(&self, share: &Share) -> Result; /// Find a share by its ID - async fn find_by_id(&self, id: &str) -> Result; + async fn find_by_id(&self, id: Uuid) -> Result; /// Find a share by its token async fn find_by_token(&self, token: &str) -> Result; @@ -37,10 +38,10 @@ pub trait ShareRepository: Send + Sync + 'static { ) -> Result, ShareRepositoryError>; /// Delete a share by its ID - async fn delete(&self, id: &str) -> Result<(), ShareRepositoryError>; + async fn delete(&self, id: Uuid) -> Result<(), ShareRepositoryError>; /// Find all shares created by a specific user - async fn find_by_user(&self, user_id: &str) -> Result, ShareRepositoryError>; + async fn find_by_user(&self, user_id: Uuid) -> Result, ShareRepositoryError>; /// Find all shares (admin operation) async fn find_all(&self) -> Result, ShareRepositoryError>; diff --git a/src/domain/repositories/user_repository.rs b/src/domain/repositories/user_repository.rs index e286a5bd..eaae091c 100755 --- a/src/domain/repositories/user_repository.rs +++ b/src/domain/repositories/user_repository.rs @@ -1,5 +1,6 @@ use crate::common::errors::DomainError; use crate::domain::entities::user::{User, UserRole}; +use uuid::Uuid; #[derive(Debug, thiserror::Error)] pub enum UserRepositoryError { @@ -45,7 +46,7 @@ pub trait UserRepository: Send + Sync + 'static { async fn create_user(&self, user: User) -> UserRepositoryResult; /// Gets a user by ID - async fn get_user_by_id(&self, id: &str) -> UserRepositoryResult; + async fn get_user_by_id(&self, id: Uuid) -> UserRepositoryResult; /// Gets a user by username async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult; @@ -59,12 +60,12 @@ pub trait UserRepository: Send + Sync + 'static { /// Updates only a user's storage usage async fn update_storage_usage( &self, - user_id: &str, + user_id: Uuid, usage_bytes: i64, ) -> UserRepositoryResult<()>; /// Updates the last login date - async fn update_last_login(&self, user_id: &str) -> UserRepositoryResult<()>; + async fn update_last_login(&self, user_id: Uuid) -> UserRepositoryResult<()>; /// Lists users with pagination async fn list_users(&self, limit: i64, offset: i64) -> UserRepositoryResult>; @@ -73,21 +74,21 @@ pub trait UserRepository: Send + Sync + 'static { async fn search_users(&self, query: &str, limit: i64) -> UserRepositoryResult>; /// Activates or deactivates a user - async fn set_user_active_status(&self, user_id: &str, active: bool) + async fn set_user_active_status(&self, user_id: Uuid, active: bool) -> UserRepositoryResult<()>; /// Changes a user's password - async fn change_password(&self, user_id: &str, password_hash: &str) + async fn change_password(&self, user_id: Uuid, password_hash: &str) -> UserRepositoryResult<()>; /// Changes a user's role - async fn change_role(&self, user_id: &str, role: UserRole) -> UserRepositoryResult<()>; + async fn change_role(&self, user_id: Uuid, role: UserRole) -> UserRepositoryResult<()>; /// Lists users by role (admin or user) async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult>; /// Deletes a user - async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()>; + async fn delete_user(&self, user_id: Uuid) -> UserRepositoryResult<()>; /// Finds a user by OIDC provider + subject pair async fn get_user_by_oidc_subject( @@ -99,7 +100,7 @@ pub trait UserRepository: Send + Sync + 'static { /// Updates a user's storage quota async fn update_storage_quota( &self, - user_id: &str, + user_id: Uuid, quota_bytes: i64, ) -> UserRepositoryResult<()>; diff --git a/src/infrastructure/adapters/calendar_storage_adapter.rs b/src/infrastructure/adapters/calendar_storage_adapter.rs index 335931ab..5a18e65b 100755 --- a/src/infrastructure/adapters/calendar_storage_adapter.rs +++ b/src/infrastructure/adapters/calendar_storage_adapter.rs @@ -47,7 +47,7 @@ impl CalendarStoragePort for CalendarStorageAdapter { async fn create_calendar( &self, dto: CreateCalendarDto, - owner_id: &str, + owner_id: Uuid, ) -> Result { let calendar = Calendar::new(dto.name, owner_id.to_string(), dto.description, dto.color)?; @@ -117,7 +117,7 @@ impl CalendarStoragePort for CalendarStorageAdapter { async fn list_calendars_by_owner( &self, - owner_id: &str, + owner_id: Uuid, ) -> Result, DomainError> { let calendars = self .calendar_repository @@ -128,7 +128,7 @@ impl CalendarStoragePort for CalendarStorageAdapter { async fn list_calendars_shared_with_user( &self, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let calendars = self .calendar_repository @@ -152,7 +152,7 @@ impl CalendarStoragePort for CalendarStorageAdapter { async fn check_calendar_access( &self, calendar_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result { let uuid = Uuid::parse_str(calendar_id).map_err(|_| { DomainError::new( @@ -172,7 +172,7 @@ impl CalendarStoragePort for CalendarStorageAdapter { async fn share_calendar( &self, calendar_id: &str, - user_id: &str, + user_id: Uuid, access_level: &str, ) -> Result<(), DomainError> { let uuid = Uuid::parse_str(calendar_id).map_err(|_| { @@ -191,7 +191,7 @@ impl CalendarStoragePort for CalendarStorageAdapter { async fn remove_calendar_sharing( &self, calendar_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result<(), DomainError> { let uuid = Uuid::parse_str(calendar_id).map_err(|_| { DomainError::new( diff --git a/src/infrastructure/adapters/contact_storage_adapter.rs b/src/infrastructure/adapters/contact_storage_adapter.rs index 999b4046..b80f3b89 100755 --- a/src/infrastructure/adapters/contact_storage_adapter.rs +++ b/src/infrastructure/adapters/contact_storage_adapter.rs @@ -61,7 +61,7 @@ impl ContactStorageAdapter { async fn check_address_book_access( &self, address_book_id: &Uuid, - user_id: &str, + user_id: Uuid, ) -> Result { let address_book = self .address_book_repository @@ -72,7 +72,7 @@ impl ContactStorageAdapter { })?; // Check if user is owner - if address_book.owner_id() == user_id { + if address_book.owner_id() == user_id.to_string() { return Ok(address_book); } @@ -86,7 +86,7 @@ impl ContactStorageAdapter { .address_book_repository .get_address_book_shares(address_book_id) .await?; - if shares.iter().any(|(shared_user, _)| shared_user == user_id) { + if shares.iter().any(|(shared_user, _)| shared_user == &user_id.to_string()) { return Ok(address_book); } @@ -101,7 +101,7 @@ impl ContactStorageAdapter { async fn check_write_access( &self, address_book_id: &Uuid, - user_id: &str, + user_id: Uuid, ) -> Result { let address_book = self .address_book_repository @@ -112,7 +112,7 @@ impl ContactStorageAdapter { })?; // Owner always has write access - if address_book.owner_id() == user_id { + if address_book.owner_id() == user_id.to_string() { return Ok(address_book); } @@ -123,7 +123,7 @@ impl ContactStorageAdapter { .await?; if shares .iter() - .any(|(shared_user, can_write)| shared_user == user_id && *can_write) + .any(|(shared_user, can_write)| shared_user == &user_id.to_string() && *can_write) { return Ok(address_book); } @@ -247,7 +247,10 @@ impl AddressBookUseCase for ContactStorageAdapter { let uuid = Self::parse_uuid(address_book_id, "AddressBook")?; // Check write access - let mut address_book = self.check_write_access(&uuid, &update.user_id).await?; + let user_id = Uuid::parse_str(&update.user_id).map_err(|_| { + DomainError::new(ErrorKind::InvalidInput, "AddressBook", "Invalid user ID format") + })?; + let mut address_book = self.check_write_access(&uuid, user_id).await?; if let Some(name) = update.name { address_book.set_name(name); @@ -273,7 +276,7 @@ impl AddressBookUseCase for ContactStorageAdapter { async fn delete_address_book( &self, address_book_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result<(), DomainError> { let uuid = Self::parse_uuid(address_book_id, "AddressBook")?; @@ -286,7 +289,7 @@ impl AddressBookUseCase for ContactStorageAdapter { DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found") })?; - if address_book.owner_id() != user_id { + if address_book.owner_id() != user_id.to_string() { return Err(DomainError::new( ErrorKind::AccessDenied, "AddressBook", @@ -302,7 +305,7 @@ impl AddressBookUseCase for ContactStorageAdapter { async fn get_address_book( &self, address_book_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result { let uuid = Self::parse_uuid(address_book_id, "AddressBook")?; let address_book = self.check_address_book_access(&uuid, user_id).await?; @@ -311,7 +314,7 @@ impl AddressBookUseCase for ContactStorageAdapter { async fn list_user_address_books( &self, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let owned = self .address_book_repository @@ -339,7 +342,7 @@ impl AddressBookUseCase for ContactStorageAdapter { async fn share_address_book( &self, dto: ShareAddressBookDto, - user_id: &str, + user_id: Uuid, ) -> Result<(), DomainError> { let uuid = Self::parse_uuid(&dto.address_book_id, "AddressBook")?; @@ -352,7 +355,7 @@ impl AddressBookUseCase for ContactStorageAdapter { DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found") })?; - if address_book.owner_id() != user_id { + if address_book.owner_id() != user_id.to_string() { return Err(DomainError::new( ErrorKind::AccessDenied, "AddressBook", @@ -360,15 +363,19 @@ 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") + })?; + self.address_book_repository - .share_address_book(&uuid, &dto.user_id, dto.can_write) + .share_address_book(&uuid, target_user_id, dto.can_write) .await } async fn unshare_address_book( &self, dto: UnshareAddressBookDto, - user_id: &str, + user_id: Uuid, ) -> Result<(), DomainError> { let uuid = Self::parse_uuid(&dto.address_book_id, "AddressBook")?; @@ -381,7 +388,7 @@ impl AddressBookUseCase for ContactStorageAdapter { DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found") })?; - if address_book.owner_id() != user_id { + if address_book.owner_id() != user_id.to_string() { return Err(DomainError::new( ErrorKind::AccessDenied, "AddressBook", @@ -389,15 +396,19 @@ 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") + })?; + self.address_book_repository - .unshare_address_book(&uuid, &dto.user_id) + .unshare_address_book(&uuid, target_user_id) .await } async fn get_address_book_shares( &self, address_book_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let uuid = Self::parse_uuid(address_book_id, "AddressBook")?; @@ -410,7 +421,7 @@ impl AddressBookUseCase for ContactStorageAdapter { DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found") })?; - if address_book.owner_id() != user_id { + if address_book.owner_id() != user_id.to_string() { return Err(DomainError::new( ErrorKind::AccessDenied, "AddressBook", @@ -429,7 +440,10 @@ impl ContactUseCase for ContactStorageAdapter { let address_book_id = Self::parse_uuid(&dto.address_book_id, "AddressBook")?; // Check write access - self.check_write_access(&address_book_id, &dto.user_id) + 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?; let now = chrono::Utc::now(); @@ -471,7 +485,10 @@ impl ContactUseCase for ContactStorageAdapter { let address_book_id = Self::parse_uuid(&dto.address_book_id, "AddressBook")?; // Check write access - self.check_write_access(&address_book_id, &dto.user_id) + 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?; // Parse vCard fields @@ -591,7 +608,10 @@ impl ContactUseCase for ContactStorageAdapter { .ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?; // Check write access to the address book - self.check_write_access(contact.address_book_id(), &update.user_id) + let user_id = Uuid::parse_str(&update.user_id).map_err(|_| { + DomainError::new(ErrorKind::InvalidInput, "Contact", "Invalid user ID format") + })?; + self.check_write_access(contact.address_book_id(), user_id) .await?; if let Some(full_name) = update.full_name { @@ -643,7 +663,7 @@ impl ContactUseCase for ContactStorageAdapter { Ok(ContactDto::from(updated)) } - async fn delete_contact(&self, contact_id: &str, user_id: &str) -> Result<(), DomainError> { + async fn delete_contact(&self, contact_id: &str, user_id: Uuid) -> Result<(), DomainError> { let uuid = Self::parse_uuid(contact_id, "Contact")?; let contact = self @@ -662,7 +682,7 @@ impl ContactUseCase for ContactStorageAdapter { async fn get_contact( &self, contact_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result { let uuid = Self::parse_uuid(contact_id, "Contact")?; @@ -682,7 +702,7 @@ impl ContactUseCase for ContactStorageAdapter { async fn list_contacts( &self, address_book_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let uuid = Self::parse_uuid(address_book_id, "AddressBook")?; @@ -700,7 +720,7 @@ impl ContactUseCase for ContactStorageAdapter { &self, address_book_id: &str, query: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let uuid = Self::parse_uuid(address_book_id, "AddressBook")?; @@ -721,7 +741,10 @@ impl ContactUseCase for ContactStorageAdapter { let address_book_id = Self::parse_uuid(&dto.address_book_id, "AddressBook")?; // Check write access - self.check_write_access(&address_book_id, &dto.user_id) + let user_id = Uuid::parse_str(&dto.user_id).map_err(|_| { + DomainError::new(ErrorKind::InvalidInput, "ContactGroup", "Invalid user ID format") + })?; + self.check_write_access(&address_book_id, user_id) .await?; let group = ContactGroup::new(address_book_id, dto.name); @@ -746,7 +769,10 @@ impl ContactUseCase for ContactStorageAdapter { })?; // Check write access - self.check_write_access(group.address_book_id(), &update.user_id) + let user_id = Uuid::parse_str(&update.user_id).map_err(|_| { + DomainError::new(ErrorKind::InvalidInput, "ContactGroup", "Invalid user ID format") + })?; + self.check_write_access(group.address_book_id(), user_id) .await?; group.set_name(update.name); @@ -756,7 +782,7 @@ impl ContactUseCase for ContactStorageAdapter { Ok(ContactGroupDto::from(updated)) } - async fn delete_group(&self, group_id: &str, user_id: &str) -> Result<(), DomainError> { + async fn delete_group(&self, group_id: &str, user_id: Uuid) -> Result<(), DomainError> { let uuid = Self::parse_uuid(group_id, "ContactGroup")?; let group = self @@ -777,7 +803,7 @@ impl ContactUseCase for ContactStorageAdapter { async fn get_group( &self, group_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result { let uuid = Self::parse_uuid(group_id, "ContactGroup")?; @@ -799,7 +825,7 @@ impl ContactUseCase for ContactStorageAdapter { async fn list_groups( &self, address_book_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let uuid = Self::parse_uuid(address_book_id, "AddressBook")?; @@ -816,7 +842,7 @@ impl ContactUseCase for ContactStorageAdapter { async fn add_contact_to_group( &self, dto: GroupMembershipDto, - user_id: &str, + user_id: Uuid, ) -> Result<(), DomainError> { let group_id = Self::parse_uuid(&dto.group_id, "ContactGroup")?; let contact_id = Self::parse_uuid(&dto.contact_id, "Contact")?; @@ -841,7 +867,7 @@ impl ContactUseCase for ContactStorageAdapter { async fn remove_contact_from_group( &self, dto: GroupMembershipDto, - user_id: &str, + user_id: Uuid, ) -> Result<(), DomainError> { let group_id = Self::parse_uuid(&dto.group_id, "ContactGroup")?; let contact_id = Self::parse_uuid(&dto.contact_id, "Contact")?; @@ -866,7 +892,7 @@ impl ContactUseCase for ContactStorageAdapter { async fn list_contacts_in_group( &self, group_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let uuid = Self::parse_uuid(group_id, "ContactGroup")?; @@ -889,7 +915,7 @@ impl ContactUseCase for ContactStorageAdapter { async fn list_groups_for_contact( &self, contact_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let uuid = Self::parse_uuid(contact_id, "Contact")?; @@ -910,7 +936,7 @@ impl ContactUseCase for ContactStorageAdapter { async fn get_contact_vcard( &self, contact_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result { let uuid = Self::parse_uuid(contact_id, "Contact")?; @@ -930,7 +956,7 @@ impl ContactUseCase for ContactStorageAdapter { async fn get_contacts_as_vcards( &self, address_book_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let uuid = Self::parse_uuid(address_book_id, "AddressBook")?; diff --git a/src/infrastructure/repositories/pg/address_book_pg_repository.rs b/src/infrastructure/repositories/pg/address_book_pg_repository.rs index f77f83d1..5dbe30e7 100755 --- a/src/infrastructure/repositories/pg/address_book_pg_repository.rs +++ b/src/infrastructure/repositories/pg/address_book_pg_repository.rs @@ -144,7 +144,7 @@ impl AddressBookRepository for AddressBookPgRepository { async fn get_address_books_by_owner( &self, - owner_id: &str, + owner_id: Uuid, ) -> AddressBookRepositoryResult> { let rows = sqlx::query( r#" @@ -182,7 +182,7 @@ impl AddressBookRepository for AddressBookPgRepository { async fn get_shared_address_books( &self, - user_id: &str, + user_id: Uuid, ) -> AddressBookRepositoryResult> { let rows = sqlx::query( r#" @@ -254,7 +254,7 @@ impl AddressBookRepository for AddressBookPgRepository { async fn share_address_book( &self, address_book_id: &Uuid, - user_id: &str, + user_id: Uuid, can_write: bool, ) -> AddressBookRepositoryResult<()> { sqlx::query( @@ -277,7 +277,7 @@ impl AddressBookRepository for AddressBookPgRepository { async fn unshare_address_book( &self, address_book_id: &Uuid, - user_id: &str, + user_id: Uuid, ) -> AddressBookRepositoryResult<()> { sqlx::query( r#" diff --git a/src/infrastructure/repositories/pg/app_password_pg_repository.rs b/src/infrastructure/repositories/pg/app_password_pg_repository.rs index c55fb80c..8c1b2635 100755 --- a/src/infrastructure/repositories/pg/app_password_pg_repository.rs +++ b/src/infrastructure/repositories/pg/app_password_pg_repository.rs @@ -6,6 +6,7 @@ use crate::domain::entities::app_password::AppPassword; use chrono::{DateTime, Utc}; use sqlx::PgPool; use std::sync::Arc; +use uuid::Uuid; pub struct AppPasswordPgRepository { pool: Arc, @@ -48,7 +49,7 @@ impl AppPasswordStoragePort for AppPasswordPgRepository { Ok(ap) } - async fn list_by_user(&self, user_id: &str) -> Result, DomainError> { + async fn list_by_user(&self, user_id: Uuid) -> Result, DomainError> { let rows = sqlx::query_as::<_, AppPasswordRow>( r#" SELECT id, user_id, label, password_hash, prefix, scopes, @@ -66,7 +67,7 @@ impl AppPasswordStoragePort for AppPasswordPgRepository { Ok(rows.into_iter().map(|r| r.into()).collect()) } - async fn get_by_id(&self, id: &str) -> Result { + async fn get_by_id(&self, id: Uuid) -> Result { let row = sqlx::query_as::<_, AppPasswordRow>( r#" SELECT id, user_id, label, password_hash, prefix, scopes, @@ -79,12 +80,12 @@ impl AppPasswordStoragePort for AppPasswordPgRepository { .fetch_optional(self.pool()) .await .map_err(|e| DomainError::internal_error("AppPasswordPg", format!("get_by_id: {e}")))? - .ok_or_else(|| DomainError::not_found("AppPassword", id))?; + .ok_or_else(|| DomainError::not_found("AppPassword", id.to_string()))?; Ok(row.into()) } - async fn get_active_by_user_id(&self, user_id: &str) -> Result, DomainError> { + async fn get_active_by_user_id(&self, user_id: Uuid) -> Result, DomainError> { let rows = sqlx::query_as::<_, AppPasswordRow>( r#" SELECT id, user_id, label, password_hash, prefix, scopes, @@ -105,7 +106,7 @@ impl AppPasswordStoragePort for AppPasswordPgRepository { async fn get_active_by_user_prefix( &self, - user_id: &str, + user_id: Uuid, prefix: &str, ) -> Result, DomainError> { let rows = sqlx::query_as::<_, AppPasswordRow>( @@ -131,7 +132,7 @@ impl AppPasswordStoragePort for AppPasswordPgRepository { Ok(rows.into_iter().map(|r| r.into()).collect()) } - async fn touch_last_used(&self, id: &str) -> Result<(), DomainError> { + async fn touch_last_used(&self, id: Uuid) -> Result<(), DomainError> { sqlx::query("UPDATE auth.app_passwords SET last_used_at = NOW() WHERE id = $1") .bind(id) .execute(self.pool()) @@ -140,7 +141,7 @@ impl AppPasswordStoragePort for AppPasswordPgRepository { Ok(()) } - async fn revoke(&self, id: &str, user_id: &str) -> Result<(), DomainError> { + async fn revoke(&self, id: Uuid, user_id: Uuid) -> Result<(), DomainError> { let result = sqlx::query( "UPDATE auth.app_passwords SET active = FALSE WHERE id = $1 AND user_id = $2", ) @@ -151,12 +152,12 @@ impl AppPasswordStoragePort for AppPasswordPgRepository { .map_err(|e| DomainError::internal_error("AppPasswordPg", format!("revoke: {e}")))?; if result.rows_affected() == 0 { - return Err(DomainError::not_found("AppPassword", id)); + return Err(DomainError::not_found("AppPassword", id.to_string())); } Ok(()) } - async fn delete_by_user_and_id(&self, id: &str, user_id: &str) -> Result { + async fn delete_by_user_and_id(&self, id: Uuid, user_id: Uuid) -> Result { let result = sqlx::query("DELETE FROM auth.app_passwords WHERE id = $1 AND user_id = $2") .bind(id) .bind(user_id) @@ -190,8 +191,8 @@ impl AppPasswordStoragePort for AppPasswordPgRepository { /// Internal row struct for sqlx mapping. #[derive(sqlx::FromRow)] struct AppPasswordRow { - id: String, - user_id: String, + id: Uuid, + user_id: Uuid, label: String, password_hash: String, prefix: String, diff --git a/src/infrastructure/repositories/pg/calendar_pg_repository.rs b/src/infrastructure/repositories/pg/calendar_pg_repository.rs index 74e83e62..10bb3cc3 100755 --- a/src/infrastructure/repositories/pg/calendar_pg_repository.rs +++ b/src/infrastructure/repositories/pg/calendar_pg_repository.rs @@ -140,7 +140,7 @@ impl CalendarRepository for CalendarPgRepository { async fn list_calendars_by_owner( &self, - owner_id: &str, + owner_id: Uuid, ) -> CalendarRepositoryResult> { let rows = sqlx::query( r#" @@ -180,7 +180,7 @@ impl CalendarRepository for CalendarPgRepository { async fn find_calendar_by_name_and_owner( &self, name: &str, - owner_id: &str, + owner_id: Uuid, ) -> CalendarRepositoryResult { let row = sqlx::query( r#" @@ -218,7 +218,7 @@ impl CalendarRepository for CalendarPgRepository { async fn list_calendars_shared_with_user( &self, - user_id: &str, + user_id: Uuid, ) -> CalendarRepositoryResult> { let rows = sqlx::query( r#" @@ -299,7 +299,7 @@ impl CalendarRepository for CalendarPgRepository { async fn user_has_calendar_access( &self, calendar_id: &Uuid, - user_id: &str, + user_id: Uuid, ) -> CalendarRepositoryResult { // Check if the user is the owner of the calendar or has a share let row = sqlx::query( @@ -327,7 +327,7 @@ impl CalendarRepository for CalendarPgRepository { async fn share_calendar( &self, calendar_id: &Uuid, - user_id: &str, + user_id: Uuid, access_level: &str, ) -> CalendarRepositoryResult<()> { // Validate access level @@ -358,7 +358,7 @@ impl CalendarRepository for CalendarPgRepository { async fn remove_calendar_sharing( &self, calendar_id: &Uuid, - user_id: &str, + user_id: Uuid, ) -> CalendarRepositoryResult<()> { sqlx::query( r#" diff --git a/src/infrastructure/repositories/pg/device_code_pg_repository.rs b/src/infrastructure/repositories/pg/device_code_pg_repository.rs index 8a539e26..04c426eb 100755 --- a/src/infrastructure/repositories/pg/device_code_pg_repository.rs +++ b/src/infrastructure/repositories/pg/device_code_pg_repository.rs @@ -2,6 +2,7 @@ use sqlx::{PgPool, Row}; use std::sync::Arc; +use uuid::Uuid; use crate::application::ports::auth_ports::DeviceCodeStoragePort; use crate::common::errors::{DomainError, ErrorKind}; @@ -28,7 +29,7 @@ impl DeviceCodePgRepository { let status = DeviceCodeStatus::parse(&status_str).unwrap_or(DeviceCodeStatus::Expired); Ok(DeviceCode::from_raw( - row.try_get("id").unwrap_or_default(), + row.try_get("id").unwrap(), row.try_get("device_code").unwrap_or_default(), row.try_get("user_code").unwrap_or_default(), row.try_get("client_name").unwrap_or_default(), @@ -212,7 +213,7 @@ impl DeviceCodeStoragePort for DeviceCodePgRepository { Ok(result.rows_affected()) } - async fn list_by_user(&self, user_id: &str) -> Result, DomainError> { + async fn list_by_user(&self, user_id: Uuid) -> Result, DomainError> { let rows = sqlx::query( r#" SELECT id, device_code, user_code, client_name, scopes, @@ -239,7 +240,7 @@ impl DeviceCodeStoragePort for DeviceCodePgRepository { rows.iter().map(Self::map_row).collect() } - async fn delete_by_id(&self, id: &str) -> Result<(), DomainError> { + async fn delete_by_id(&self, id: Uuid) -> Result<(), DomainError> { sqlx::query("DELETE FROM auth.device_codes WHERE id = $1") .bind(id) .execute(self.pool.as_ref()) diff --git a/src/infrastructure/repositories/pg/favorites_pg_repository.rs b/src/infrastructure/repositories/pg/favorites_pg_repository.rs index 5f30bedf..800cb847 100755 --- a/src/infrastructure/repositories/pg/favorites_pg_repository.rs +++ b/src/infrastructure/repositories/pg/favorites_pg_repository.rs @@ -20,9 +20,7 @@ impl FavoritesPgRepository { } impl FavoritesRepositoryPort for FavoritesPgRepository { - async fn get_favorites(&self, user_id: &str) -> Result> { - let user_uuid = Uuid::parse_str(user_id)?; - + async fn get_favorites(&self, user_id: Uuid) -> Result> { let rows = sqlx::query( r#" SELECT @@ -41,12 +39,12 @@ impl FavoritesRepositoryPort for FavoritesPgRepository { AND f.id = uf.item_id::UUID LEFT JOIN storage.folders fld ON uf.item_type = 'folder' AND fld.id = uf.item_id::UUID - WHERE uf.user_id = $1::TEXT + WHERE uf.user_id = $1 ORDER BY uf.created_at DESC LIMIT 500 "#, ) - .bind(user_uuid) + .bind(user_id) .fetch_all(&*self.db_pool) .await .map_err(|e| { @@ -85,17 +83,15 @@ impl FavoritesRepositoryPort for FavoritesPgRepository { Ok(favorites) } - async fn add_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()> { - let user_uuid = Uuid::parse_str(user_id)?; - + async fn add_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<()> { sqlx::query( r#" INSERT INTO auth.user_favorites (user_id, item_id, item_type) - VALUES ($1::TEXT, $2, $3) + VALUES ($1, $2, $3) ON CONFLICT (user_id, item_id, item_type) DO NOTHING "#, ) - .bind(user_uuid) + .bind(user_id) .bind(item_id) .bind(item_type) .execute(&*self.db_pool) @@ -112,16 +108,14 @@ impl FavoritesRepositoryPort for FavoritesPgRepository { Ok(()) } - async fn remove_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result { - let user_uuid = Uuid::parse_str(user_id)?; - + async fn remove_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result { let result = sqlx::query( r#" DELETE FROM auth.user_favorites - WHERE user_id = $1::TEXT AND item_id = $2 AND item_type = $3 + WHERE user_id = $1 AND item_id = $2 AND item_type = $3 "#, ) - .bind(user_uuid) + .bind(user_id) .bind(item_id) .bind(item_type) .execute(&*self.db_pool) @@ -138,18 +132,16 @@ impl FavoritesRepositoryPort for FavoritesPgRepository { Ok(result.rows_affected() > 0) } - async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result { - let user_uuid = Uuid::parse_str(user_id)?; - + async fn is_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result { let row = sqlx::query( r#" SELECT EXISTS ( SELECT 1 FROM auth.user_favorites - WHERE user_id = $1::TEXT AND item_id = $2 AND item_type = $3 + WHERE user_id = $1 AND item_id = $2 AND item_type = $3 ) AS "is_favorite" "#, ) - .bind(user_uuid) + .bind(user_id) .bind(item_id) .bind(item_type) .fetch_one(&*self.db_pool) @@ -166,13 +158,11 @@ impl FavoritesRepositoryPort for FavoritesPgRepository { Ok(row.try_get("is_favorite").unwrap_or(false)) } - async fn add_favorites_batch(&self, user_id: &str, items: &[(String, String)]) -> Result { + async fn add_favorites_batch(&self, user_id: Uuid, items: &[(String, String)]) -> Result { if items.is_empty() { return Ok(0); } - let user_uuid = Uuid::parse_str(user_id)?; - // Validate all item_types upfront for (_, item_type) in items { if item_type != "file" && item_type != "folder" { @@ -210,7 +200,7 @@ impl FavoritesRepositoryPort for FavoritesPgRepository { query.push_str(", "); } query.push_str(&format!( - "(${}::TEXT, ${}, ${})", + "(${}, ${}, ${})", param_idx, param_idx + 1, param_idx + 2 @@ -222,7 +212,7 @@ impl FavoritesRepositoryPort for FavoritesPgRepository { let mut q = sqlx::query(&query); for (item_id, item_type) in chunk { - q = q.bind(user_uuid).bind(item_id).bind(item_type); + q = q.bind(user_id).bind(item_id).bind(item_type); } let result = q.execute(&mut *tx).await.map_err(|e| { @@ -251,22 +241,20 @@ impl FavoritesRepositoryPort for FavoritesPgRepository { async fn batch_check_favorites( &self, - user_id: &str, + user_id: Uuid, item_ids: &[(&str, &str)], ) -> Result> { if item_ids.is_empty() { return Ok(HashSet::new()); } - let user_uuid = Uuid::parse_str(user_id)?; - // Collect just the IDs for the IN clause let ids: Vec = item_ids.iter().map(|(id, _)| id.to_string()).collect(); let rows = sqlx::query( - "SELECT item_id FROM auth.user_favorites WHERE user_id = $1::TEXT AND item_id = ANY($2)", + "SELECT item_id FROM auth.user_favorites WHERE user_id = $1 AND item_id = ANY($2)", ) - .bind(user_uuid) + .bind(user_id) .bind(&ids) .fetch_all(&*self.db_pool) .await diff --git a/src/infrastructure/repositories/pg/file_blob_read_repository.rs b/src/infrastructure/repositories/pg/file_blob_read_repository.rs index 9625a3be..b22bf323 100755 --- a/src/infrastructure/repositories/pg/file_blob_read_repository.rs +++ b/src/infrastructure/repositories/pg/file_blob_read_repository.rs @@ -35,6 +35,7 @@ use crate::common::errors::DomainError; use crate::domain::entities::file::File; use crate::domain::services::path_service::StoragePath; use crate::infrastructure::services::dedup_service::DedupService; +use uuid::Uuid; /// Type alias for file metadata rows from SQL queries. type FileRow = ( @@ -166,7 +167,7 @@ impl FileBlobReadRepository { /// `sort_date` epoch for each file (used as pagination cursor). pub async fn list_media_files( &self, - owner_id: &str, + owner_id: Uuid, before: Option, limit: i64, ) -> Result<(Vec, Vec), DomainError> { @@ -255,7 +256,7 @@ impl FileReadPort for FileBlobReadRepository { ) } - async fn get_file_for_owner(&self, id: &str, owner_id: &str) -> Result { + async fn get_file_for_owner(&self, id: &str, owner_id: Uuid) -> Result { let row = sqlx::query_as::< _, ( @@ -286,7 +287,7 @@ impl FileReadPort for FileBlobReadRepository { "#, ) .bind(id) - .bind(owner_id) + .bind(owner_id.to_string()) .fetch_optional(self.pool.as_ref()) .await .map_err(|e| DomainError::internal_error("FileBlobRead", format!("get_for_owner: {e}")))? @@ -350,8 +351,9 @@ impl FileReadPort for FileBlobReadRepository { async fn list_files_for_owner( &self, folder_id: Option<&str>, - owner_id: &str, + owner_id: Uuid, ) -> Result, DomainError> { + let owner_str = owner_id.to_string(); let rows: Vec = if let Some(fid) = folder_id { sqlx::query_as( r#" @@ -368,7 +370,7 @@ impl FileReadPort for FileBlobReadRepository { "#, ) .bind(fid) - .bind(owner_id) + .bind(&owner_str) .fetch_all(self.pool.as_ref()) .await } else { @@ -386,7 +388,7 @@ impl FileReadPort for FileBlobReadRepository { ORDER BY fi.name "#, ) - .bind(owner_id) + .bind(&owner_str) .fetch_all(self.pool.as_ref()) .await } @@ -468,10 +470,11 @@ impl FileReadPort for FileBlobReadRepository { async fn list_files_batch_for_owner( &self, folder_id: Option<&str>, - owner_id: &str, + owner_id: Uuid, offset: i64, limit: i64, ) -> Result, DomainError> { + let owner_str = owner_id.to_string(); let rows: Vec = if let Some(fid) = folder_id { sqlx::query_as( r#" @@ -491,7 +494,7 @@ impl FileReadPort for FileBlobReadRepository { .bind(fid) .bind(limit) .bind(offset) - .bind(owner_id) + .bind(&owner_str) .fetch_all(self.pool.as_ref()) .await } else { @@ -512,7 +515,7 @@ impl FileReadPort for FileBlobReadRepository { ) .bind(limit) .bind(offset) - .bind(owner_id) + .bind(&owner_str) .fetch_all(self.pool.as_ref()) .await } @@ -753,7 +756,7 @@ impl FileReadPort for FileBlobReadRepository { &self, folder_id: Option<&str>, criteria: &SearchCriteriaDto, - user_id: &str, + user_id: Uuid, ) -> Result<(Vec, usize), DomainError> { let offset = criteria.offset as i64; let limit = criteria.limit as i64; @@ -822,7 +825,7 @@ impl FileReadPort for FileBlobReadRepository { i64, ), >(&sql) - .bind(user_id); + .bind(user_id.to_string()); if let Some(fid) = folder_id { query = query.bind(fid); @@ -867,7 +870,7 @@ impl FileReadPort for FileBlobReadRepository { &self, root_folder_id: Option<&str>, criteria: &SearchCriteriaDto, - user_id: &str, + user_id: Uuid, ) -> Result<(Vec, usize), DomainError> { // When no root folder specified, delegate to existing paginated search let root_id = match root_folder_id { @@ -983,7 +986,7 @@ impl FileReadPort for FileBlobReadRepository { i64, ), >(&sql) - .bind(user_id) + .bind(user_id.to_string()) .bind(root_id); if let Some(name) = &criteria.name_contains @@ -1043,7 +1046,7 @@ impl FileReadPort for FileBlobReadRepository { &self, folder_id: Option<&str>, criteria: &SearchCriteriaDto, - user_id: &str, + user_id: Uuid, ) -> Result { let (_, count) = self .search_files_paginated(folder_id, criteria, user_id) diff --git a/src/infrastructure/repositories/pg/recent_items_pg_repository.rs b/src/infrastructure/repositories/pg/recent_items_pg_repository.rs index f7a1f92d..759d82fe 100755 --- a/src/infrastructure/repositories/pg/recent_items_pg_repository.rs +++ b/src/infrastructure/repositories/pg/recent_items_pg_repository.rs @@ -19,9 +19,7 @@ impl RecentItemsPgRepository { } impl RecentItemsRepositoryPort for RecentItemsPgRepository { - async fn get_recent_items(&self, user_id: &str, limit: i32) -> Result> { - let user_uuid = Uuid::parse_str(user_id)?; - + async fn get_recent_items(&self, user_id: Uuid, limit: i32) -> Result> { let rows = sqlx::query( r#" SELECT @@ -39,12 +37,12 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository { AND f.id = ur.item_id::UUID LEFT JOIN storage.folders fld ON ur.item_type = 'folder' AND fld.id = ur.item_id::UUID - WHERE ur.user_id = $1::TEXT + WHERE ur.user_id = $1 ORDER BY ur.accessed_at DESC LIMIT $2 "#, ) - .bind(user_uuid) + .bind(user_id) .bind(limit) .fetch_all(&*self.db_pool) .await @@ -83,18 +81,16 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository { Ok(items) } - async fn upsert_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()> { - let user_uuid = Uuid::parse_str(user_id)?; - + async fn upsert_access(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<()> { sqlx::query( r#" INSERT INTO auth.user_recent_files (user_id, item_id, item_type, accessed_at) - VALUES ($1::TEXT, $2, $3, CURRENT_TIMESTAMP) + VALUES ($1, $2, $3, CURRENT_TIMESTAMP) ON CONFLICT (user_id, item_id, item_type) DO UPDATE SET accessed_at = CURRENT_TIMESTAMP "#, ) - .bind(user_uuid) + .bind(user_id) .bind(item_id) .bind(item_type) .execute(&*self.db_pool) @@ -111,16 +107,14 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository { Ok(()) } - async fn remove_item(&self, user_id: &str, item_id: &str, item_type: &str) -> Result { - let user_uuid = Uuid::parse_str(user_id)?; - + async fn remove_item(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result { let result = sqlx::query( r#" DELETE FROM auth.user_recent_files - WHERE user_id = $1::TEXT AND item_id = $2 AND item_type = $3 + WHERE user_id = $1 AND item_id = $2 AND item_type = $3 "#, ) - .bind(user_uuid) + .bind(user_id) .bind(item_id) .bind(item_type) .execute(&*self.db_pool) @@ -137,16 +131,14 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository { Ok(result.rows_affected() > 0) } - async fn clear_all(&self, user_id: &str) -> Result<()> { - let user_uuid = Uuid::parse_str(user_id)?; - + async fn clear_all(&self, user_id: Uuid) -> Result<()> { sqlx::query( r#" DELETE FROM auth.user_recent_files - WHERE user_id = $1::TEXT + WHERE user_id = $1 "#, ) - .bind(user_uuid) + .bind(user_id) .execute(&*self.db_pool) .await .map_err(|e| { @@ -161,21 +153,19 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository { Ok(()) } - async fn prune(&self, user_id: &str, max_items: i32) -> Result<()> { - let user_uuid = Uuid::parse_str(user_id)?; - + async fn prune(&self, user_id: Uuid, max_items: i32) -> Result<()> { sqlx::query( r#" DELETE FROM auth.user_recent_files WHERE id IN ( SELECT id FROM auth.user_recent_files - WHERE user_id = $1::TEXT + WHERE user_id = $1 ORDER BY accessed_at DESC OFFSET $2 ) "#, ) - .bind(user_uuid) + .bind(user_id) .bind(max_items) .execute(&*self.db_pool) .await diff --git a/src/infrastructure/repositories/pg/session_pg_repository.rs b/src/infrastructure/repositories/pg/session_pg_repository.rs index f3cafd5c..2bd45e8d 100755 --- a/src/infrastructure/repositories/pg/session_pg_repository.rs +++ b/src/infrastructure/repositories/pg/session_pg_repository.rs @@ -2,6 +2,7 @@ use chrono::Utc; use futures::future::BoxFuture; use sqlx::{PgPool, Row}; use std::sync::Arc; +use uuid::Uuid; use crate::application::ports::auth_ports::SessionStoragePort; use crate::common::errors::DomainError; @@ -104,7 +105,7 @@ impl SessionRepository for SessionPgRepository { } /// Gets a session by ID - async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult { + async fn get_session_by_id(&self, id: Uuid) -> SessionRepositoryResult { let row = sqlx::query( r#" SELECT @@ -165,7 +166,7 @@ impl SessionRepository for SessionPgRepository { /// Gets all sessions for a user async fn get_sessions_by_user_id( &self, - user_id: &str, + user_id: Uuid, ) -> SessionRepositoryResult> { let rows = sqlx::query( r#" @@ -202,8 +203,8 @@ impl SessionRepository for SessionPgRepository { } /// Revokes a specific session using a transaction - async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()> { - let id = session_id.to_string(); // Clone for use in closure + async fn revoke_session(&self, session_id: Uuid) -> SessionRepositoryResult<()> { + let id = session_id; // Copy for use in closure with_transaction(&self.pool, "revoke_session", |tx| { Box::pin(async move { @@ -216,14 +217,14 @@ impl SessionRepository for SessionPgRepository { RETURNING user_id "#, ) - .bind(&id) + .bind(id) .fetch_optional(&mut **tx) .await .map_err(Self::map_sqlx_error)?; // If we found the session, we can log a security event if let Some(row) = result { - let user_id: String = row.try_get("user_id").unwrap_or_default(); + let user_id: Uuid = row.try_get("user_id").unwrap_or_default(); // Log security event (in a security table) // This is optional but shows how additional operations @@ -238,8 +239,8 @@ impl SessionRepository for SessionPgRepository { } /// Revokes all sessions for a user using a transaction - async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult { - let user_id_clone = user_id.to_string(); // Clone for use in closure + async fn revoke_all_user_sessions(&self, user_id: Uuid) -> SessionRepositoryResult { + let user_id_copy = user_id; // Copy for use in closure with_transaction(&self.pool, "revoke_all_user_sessions", |tx| { Box::pin(async move { @@ -251,7 +252,7 @@ impl SessionRepository for SessionPgRepository { WHERE user_id = $1 AND revoked = false "#, ) - .bind(&user_id_clone) + .bind(user_id_copy) .execute(&mut **tx) .await .map_err(Self::map_sqlx_error)?; @@ -260,7 +261,7 @@ impl SessionRepository for SessionPgRepository { // Log security event if affected > 0 { - tracing::info!("Revoked {} sessions for user {}", affected, user_id_clone); + tracing::info!("Revoked {} sessions for user {}", affected, user_id_copy); } Ok(affected) @@ -305,13 +306,13 @@ impl SessionStoragePort for SessionPgRepository { .map_err(DomainError::from) } - async fn revoke_session(&self, session_id: &str) -> Result<(), DomainError> { + async fn revoke_session(&self, session_id: Uuid) -> Result<(), DomainError> { SessionRepository::revoke_session(self, session_id) .await .map_err(DomainError::from) } - async fn revoke_all_user_sessions(&self, user_id: &str) -> Result { + async fn revoke_all_user_sessions(&self, user_id: Uuid) -> Result { SessionRepository::revoke_all_user_sessions(self, user_id) .await .map_err(DomainError::from) diff --git a/src/infrastructure/repositories/pg/settings_pg_repository.rs b/src/infrastructure/repositories/pg/settings_pg_repository.rs index c98af3b9..91801035 100755 --- a/src/infrastructure/repositories/pg/settings_pg_repository.rs +++ b/src/infrastructure/repositories/pg/settings_pg_repository.rs @@ -1,6 +1,7 @@ use sqlx::PgPool; use std::collections::HashMap; use std::sync::Arc; +use uuid::Uuid; use crate::common::errors::{DomainError, ErrorKind}; use crate::domain::repositories::settings_repository::SettingsRepository; @@ -60,7 +61,7 @@ impl SettingsRepository for SettingsPgRepository { value: &str, category: &str, is_secret: bool, - updated_by: Option<&str>, + updated_by: Option, ) -> Result<(), DomainError> { sqlx::query( "INSERT INTO auth.admin_settings (key, value, category, is_secret, updated_by, updated_at) @@ -102,7 +103,7 @@ impl SettingsRepository for SettingsPgRepository { /// /// Only the first caller that inserts the row gets `rows_affected == 1`; /// concurrent callers see 0 rows affected and receive `false`. - async fn try_claim_initialization(&self, admin_user_id: &str) -> Result { + async fn try_claim_initialization(&self, admin_user_id: Uuid) -> Result { let result = sqlx::query( "INSERT INTO auth.admin_settings (key, value, category, is_secret, updated_by, updated_at) VALUES ('system_initialized', 'true', 'system', false, $1, NOW()) diff --git a/src/infrastructure/repositories/pg/share_pg_repository.rs b/src/infrastructure/repositories/pg/share_pg_repository.rs index 62779069..bfdff9ba 100755 --- a/src/infrastructure/repositories/pg/share_pg_repository.rs +++ b/src/infrastructure/repositories/pg/share_pg_repository.rs @@ -1,5 +1,6 @@ use sqlx::{PgPool, Row}; use std::sync::Arc; +use uuid::Uuid; use crate::{ application::ports::share_ports::ShareStoragePort, @@ -37,7 +38,7 @@ impl SharePgRepository { /// Maps a [`sqlx::postgres::PgRow`] to the domain [`Share`] entity. fn row_to_entity(row: &sqlx::postgres::PgRow) -> Result { - let id: String = row + let id: Uuid = row .try_get("id") .map_err(|e| DomainError::internal_error("Share", format!("Failed to read id: {e}")))?; let item_id: String = row.try_get("item_id").map_err(|e| { @@ -58,7 +59,7 @@ impl SharePgRepository { let created_at: i64 = row.try_get("created_at").map_err(|e| { DomainError::internal_error("Share", format!("Failed to read created_at: {e}")) })?; - let created_by: String = row.try_get("created_by").map_err(|e| { + let created_by: Uuid = row.try_get("created_by").map_err(|e| { DomainError::internal_error("Share", format!("Failed to read created_by: {e}")) })?; let access_count: i64 = row.try_get("access_count").unwrap_or(0); @@ -93,7 +94,7 @@ impl ShareStoragePort for SharePgRepository { expires_at, permissions_read, permissions_write, permissions_reshare, created_at, created_by, access_count) VALUES - ($1::UUID, $2, $3, $4, $5, $6, + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) ON CONFLICT (id) DO UPDATE SET @@ -105,7 +106,7 @@ impl ShareStoragePort for SharePgRepository { permissions_reshare = EXCLUDED.permissions_reshare, access_count = EXCLUDED.access_count RETURNING - id::TEXT, item_id, item_name, item_type, token, password_hash, + id, item_id, item_name, item_type, token, password_hash, expires_at, permissions_read, permissions_write, permissions_reshare, created_at, created_by, access_count "#, @@ -136,7 +137,7 @@ impl ShareStoragePort for SharePgRepository { async fn find_share_by_token(&self, token: &str) -> Result { let row = sqlx::query( r#" - SELECT id::TEXT, item_id, item_name, item_type, token, password_hash, + SELECT id, item_id, item_name, item_type, token, password_hash, expires_at, permissions_read, permissions_write, permissions_reshare, created_at, created_by, access_count FROM storage.shares @@ -162,16 +163,16 @@ impl ShareStoragePort for SharePgRepository { async fn find_share_by_id_for_user( &self, - id: &str, - user_id: &str, + id: Uuid, + user_id: Uuid, ) -> Result { let row = sqlx::query( r#" - SELECT id::TEXT, item_id, item_name, item_type, token, password_hash, + SELECT id, item_id, item_name, item_type, token, password_hash, expires_at, permissions_read, permissions_write, permissions_reshare, created_at, created_by, access_count FROM storage.shares - WHERE id = $1::UUID AND created_by = $2 + WHERE id = $1 AND created_by = $2 "#, ) .bind(id) @@ -193,9 +194,9 @@ impl ShareStoragePort for SharePgRepository { } } - async fn delete_share_for_user(&self, id: &str, user_id: &str) -> Result<(), DomainError> { + 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::UUID AND created_by = $2") + sqlx::query("DELETE FROM storage.shares WHERE id = $1 AND created_by = $2") .bind(id) .bind(user_id) .execute(&*self.db_pool) @@ -220,11 +221,11 @@ impl ShareStoragePort for SharePgRepository { &self, item_id: &str, item_type: &ShareItemType, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let rows = sqlx::query( r#" - SELECT id::TEXT, item_id, item_name, item_type, token, password_hash, + SELECT id, item_id, item_name, item_type, token, password_hash, expires_at, permissions_read, permissions_write, permissions_reshare, created_at, created_by, access_count FROM storage.shares @@ -256,9 +257,9 @@ impl ShareStoragePort for SharePgRepository { permissions_write = $6, permissions_reshare = $7, access_count = $8 - WHERE id = $1::UUID + WHERE id = $1 RETURNING - id::TEXT, item_id, item_name, item_type, token, password_hash, + id, item_id, item_name, item_type, token, password_hash, expires_at, permissions_read, permissions_write, permissions_reshare, created_at, created_by, access_count "#, @@ -289,14 +290,14 @@ impl ShareStoragePort for SharePgRepository { async fn find_shares_by_user( &self, - user_id: &str, + user_id: Uuid, offset: usize, limit: usize, ) -> Result<(Vec, usize), DomainError> { // Single query with window function — count + rows in one roundtrip let rows = sqlx::query( r#" - SELECT id::TEXT, item_id, item_name, item_type, token, password_hash, + SELECT id, item_id, item_name, item_type, token, password_hash, expires_at, permissions_read, permissions_write, permissions_reshare, created_at, created_by, access_count, COUNT(*) OVER() AS total_count diff --git a/src/infrastructure/repositories/pg/trash_db_repository.rs b/src/infrastructure/repositories/pg/trash_db_repository.rs index f1a77182..e8cc3ec9 100755 --- a/src/infrastructure/repositories/pg/trash_db_repository.rs +++ b/src/infrastructure/repositories/pg/trash_db_repository.rs @@ -50,7 +50,7 @@ impl TrashDbRepository { id: Uuid, name: String, item_type: String, - user_id: String, + user_id: Uuid, trashed_at: Option>, ) -> TrashedItem { let trashed_at = trashed_at.unwrap_or_else(Utc::now); @@ -61,14 +61,12 @@ impl TrashDbRepository { _ => TrashedItemType::File, }; - let user_uuid = Uuid::parse_str(&user_id).unwrap_or_else(|_| Uuid::nil()); - // 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_uuid, // 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 @@ -87,7 +85,7 @@ impl TrashRepository for TrashDbRepository { } async fn get_trash_items(&self, user_id: &Uuid) -> Result> { - let rows = sqlx::query_as::<_, (Uuid, String, String, String, Option>)>( + let rows = sqlx::query_as::<_, (Uuid, String, String, Uuid, Option>)>( r#" SELECT id, name, item_type, user_id, trashed_at FROM storage.trash_items @@ -95,7 +93,7 @@ impl TrashRepository for TrashDbRepository { ORDER BY trashed_at DESC "#, ) - .bind(user_id.to_string()) + .bind(user_id) .fetch_all(self.pool.as_ref()) .await .map_err(|e| DomainError::internal_error("TrashDb", format!("list: {e}")))?; @@ -109,7 +107,7 @@ impl TrashRepository for TrashDbRepository { } async fn get_trash_item(&self, id: &Uuid, user_id: &Uuid) -> Result> { - let row = sqlx::query_as::<_, (Uuid, String, String, String, Option>)>( + let row = sqlx::query_as::<_, (Uuid, String, String, Uuid, Option>)>( r#" SELECT id, name, item_type, user_id, trashed_at FROM storage.trash_items @@ -117,7 +115,7 @@ impl TrashRepository for TrashDbRepository { "#, ) .bind(id) - .bind(user_id.to_string()) + .bind(user_id) .fetch_optional(self.pool.as_ref()) .await .map_err(|e| DomainError::internal_error("TrashDb", format!("get: {e}")))?; @@ -144,14 +142,14 @@ impl TrashRepository for TrashDbRepository { async fn clear_trash(&self, user_id: &Uuid) -> Result<()> { // Delete all trashed files for this user sqlx::query("DELETE FROM storage.files WHERE user_id = $1 AND is_trashed = TRUE") - .bind(user_id.to_string()) + .bind(user_id) .execute(self.pool.as_ref()) .await .map_err(|e| DomainError::internal_error("TrashDb", format!("clear files: {e}")))?; // Delete all trashed folders for this user sqlx::query("DELETE FROM storage.folders WHERE user_id = $1 AND is_trashed = TRUE") - .bind(user_id.to_string()) + .bind(user_id) .execute(self.pool.as_ref()) .await .map_err(|e| DomainError::internal_error("TrashDb", format!("clear folders: {e}")))?; diff --git a/src/infrastructure/repositories/pg/user_pg_repository.rs b/src/infrastructure/repositories/pg/user_pg_repository.rs index 2389d460..8198350a 100755 --- a/src/infrastructure/repositories/pg/user_pg_repository.rs +++ b/src/infrastructure/repositories/pg/user_pg_repository.rs @@ -1,6 +1,7 @@ use futures::future::BoxFuture; use sqlx::{PgPool, Row}; use std::sync::Arc; +use uuid::Uuid; use crate::application::ports::auth_ports::UserStoragePort; use crate::common::errors::DomainError; @@ -101,7 +102,7 @@ impl UserRepository for UserPgRepository { } /// Gets a user by ID - async fn get_user_by_id(&self, id: &str) -> UserRepositoryResult { + async fn get_user_by_id(&self, id: Uuid) -> UserRepositoryResult { let row = sqlx::query( r#" SELECT @@ -278,7 +279,7 @@ impl UserRepository for UserPgRepository { /// Updates only the storage usage of a user async fn update_storage_usage( &self, - user_id: &str, + user_id: Uuid, usage_bytes: i64, ) -> UserRepositoryResult<()> { sqlx::query( @@ -300,7 +301,7 @@ impl UserRepository for UserPgRepository { } /// Updates the last login date - async fn update_last_login(&self, user_id: &str) -> UserRepositoryResult<()> { + async fn update_last_login(&self, user_id: Uuid) -> UserRepositoryResult<()> { sqlx::query( r#" UPDATE auth.users @@ -423,7 +424,7 @@ impl UserRepository for UserPgRepository { /// Activates or deactivates a user async fn set_user_active_status( &self, - user_id: &str, + user_id: Uuid, active: bool, ) -> UserRepositoryResult<()> { sqlx::query( @@ -447,7 +448,7 @@ impl UserRepository for UserPgRepository { /// Changes a user's password async fn change_password( &self, - user_id: &str, + user_id: Uuid, password_hash: &str, ) -> UserRepositoryResult<()> { sqlx::query( @@ -469,7 +470,7 @@ impl UserRepository for UserPgRepository { } /// Changes a user's role - async fn change_role(&self, user_id: &str, role: UserRole) -> UserRepositoryResult<()> { + async fn change_role(&self, user_id: Uuid, role: UserRole) -> UserRepositoryResult<()> { // Convert the role to string for the binding let role_str = role.to_string(); @@ -542,7 +543,7 @@ impl UserRepository for UserPgRepository { } /// Deletes a user - async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()> { + async fn delete_user(&self, user_id: Uuid) -> UserRepositoryResult<()> { sqlx::query( r#" DELETE FROM auth.users @@ -606,7 +607,7 @@ impl UserRepository for UserPgRepository { /// Updates a user's storage quota async fn update_storage_quota( &self, - user_id: &str, + user_id: Uuid, quota_bytes: i64, ) -> UserRepositoryResult<()> { sqlx::query( @@ -675,7 +676,7 @@ impl UserStoragePort for UserPgRepository { .map_err(DomainError::from) } - async fn get_user_by_id(&self, id: &str) -> Result { + async fn get_user_by_id(&self, id: Uuid) -> Result { UserRepository::get_user_by_id(self, id) .await .map_err(DomainError::from) @@ -701,7 +702,7 @@ impl UserStoragePort for UserPgRepository { async fn update_storage_usage( &self, - user_id: &str, + user_id: Uuid, usage_bytes: i64, ) -> Result<(), DomainError> { UserRepository::update_storage_usage(self, user_id, usage_bytes) @@ -727,13 +728,13 @@ impl UserStoragePort for UserPgRepository { .map_err(DomainError::from) } - async fn delete_user(&self, user_id: &str) -> Result<(), DomainError> { + async fn delete_user(&self, user_id: Uuid) -> Result<(), DomainError> { UserRepository::delete_user(self, user_id) .await .map_err(DomainError::from) } - async fn change_password(&self, user_id: &str, password_hash: &str) -> Result<(), DomainError> { + async fn change_password(&self, user_id: Uuid, password_hash: &str) -> Result<(), DomainError> { UserRepository::change_password(self, user_id, password_hash) .await .map_err(DomainError::from) @@ -749,13 +750,13 @@ impl UserStoragePort for UserPgRepository { .map_err(DomainError::from) } - async fn set_user_active_status(&self, user_id: &str, active: bool) -> Result<(), DomainError> { + async fn set_user_active_status(&self, user_id: Uuid, active: bool) -> Result<(), DomainError> { UserRepository::set_user_active_status(self, user_id, active) .await .map_err(DomainError::from) } - async fn change_role(&self, user_id: &str, role: &str) -> Result<(), DomainError> { + async fn change_role(&self, user_id: Uuid, role: &str) -> Result<(), DomainError> { let user_role = match role { "admin" => UserRole::Admin, _ => UserRole::User, @@ -767,7 +768,7 @@ impl UserStoragePort for UserPgRepository { async fn update_storage_quota( &self, - user_id: &str, + user_id: Uuid, quota_bytes: i64, ) -> Result<(), DomainError> { UserRepository::update_storage_quota(self, user_id, quota_bytes) diff --git a/src/infrastructure/services/chunked_upload_service.rs b/src/infrastructure/services/chunked_upload_service.rs index 25938350..4d5a3353 100755 --- a/src/infrastructure/services/chunked_upload_service.rs +++ b/src/infrastructure/services/chunked_upload_service.rs @@ -801,7 +801,7 @@ impl ChunkedUploadService { impl ChunkedUploadPort for ChunkedUploadService { async fn create_session( &self, - user_id: &str, + user_id: Uuid, filename: String, folder_id: Option, content_type: String, @@ -809,7 +809,7 @@ impl ChunkedUploadPort for ChunkedUploadService { chunk_size: Option, ) -> Result { self.create_session_inner( - user_id.to_owned(), + user_id.to_string(), filename, folder_id, content_type, @@ -823,12 +823,12 @@ impl ChunkedUploadPort for ChunkedUploadService { async fn upload_chunk( &self, upload_id: &str, - user_id: &str, + user_id: Uuid, chunk_index: usize, data: bytes::Bytes, checksum: Option, ) -> Result { - self.upload_chunk_inner(upload_id, user_id, chunk_index, data, checksum) + self.upload_chunk_inner(upload_id, &user_id.to_string(), chunk_index, data, checksum) .await .map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e)) } @@ -836,9 +836,9 @@ impl ChunkedUploadPort for ChunkedUploadService { async fn get_status( &self, upload_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result { - self.get_status_inner(upload_id, user_id) + self.get_status_inner(upload_id, &user_id.to_string()) .await .map_err(|e| DomainError::new(ErrorKind::NotFound, "ChunkedUpload", e)) } @@ -846,21 +846,21 @@ impl ChunkedUploadPort for ChunkedUploadService { async fn complete_upload( &self, upload_id: &str, - user_id: &str, + user_id: Uuid, ) -> Result<(PathBuf, String, Option, String, u64, String), DomainError> { - self.complete_upload_inner(upload_id, user_id) + self.complete_upload_inner(upload_id, &user_id.to_string()) .await .map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e)) } - async fn finalize_upload(&self, upload_id: &str, user_id: &str) -> Result<(), DomainError> { - self.finalize_upload_inner(upload_id, user_id) + async fn finalize_upload(&self, upload_id: &str, user_id: Uuid) -> Result<(), DomainError> { + self.finalize_upload_inner(upload_id, &user_id.to_string()) .await .map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e)) } - async fn cancel_upload(&self, upload_id: &str, user_id: &str) -> Result<(), DomainError> { - self.cancel_upload_inner(upload_id, user_id) + async fn cancel_upload(&self, upload_id: &str, user_id: Uuid) -> Result<(), DomainError> { + self.cancel_upload_inner(upload_id, &user_id.to_string()) .await .map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e)) } diff --git a/src/infrastructure/services/path_resolver_service.rs b/src/infrastructure/services/path_resolver_service.rs index 60a0b391..c4393c3f 100755 --- a/src/infrastructure/services/path_resolver_service.rs +++ b/src/infrastructure/services/path_resolver_service.rs @@ -7,6 +7,7 @@ use sqlx::PgPool; use std::sync::Arc; +use uuid::Uuid; use crate::application::dtos::display_helpers::{ category_for, format_file_size, icon_class_for, icon_special_class_for, @@ -39,7 +40,7 @@ impl PathResolverService { pub async fn resolve_path_for_user( &self, path: &str, - user_id: &str, + user_id: Uuid, ) -> Result { let path = path.trim_start_matches('/').trim_end_matches('/'); if path.is_empty() { @@ -180,7 +181,7 @@ impl PathResolverService { } /// Returns `true` if the resource at `path` belongs to `user_id`. - pub async fn exists_for_user(&self, path: &str, user_id: &str) -> Result { + pub async fn exists_for_user(&self, path: &str, user_id: Uuid) -> Result { let path = path.trim_start_matches('/').trim_end_matches('/'); if path.is_empty() { return Ok(false); diff --git a/src/interfaces/api/handlers/admin_handler.rs b/src/interfaces/api/handlers/admin_handler.rs index f230e793..7f37ed0c 100755 --- a/src/interfaces/api/handlers/admin_handler.rs +++ b/src/interfaces/api/handlers/admin_handler.rs @@ -15,6 +15,7 @@ use crate::application::ports::auth_ports::TokenServicePort; use crate::common::di::AppState; use crate::interfaces::errors::AppError; use std::sync::Arc; +use uuid::Uuid; /// Admin API routes — all require admin role. pub fn admin_routes() -> Router> { @@ -41,7 +42,7 @@ pub fn admin_routes() -> Router> { } /// Validate JWT and require admin role. Returns (user_id, role). -async fn admin_guard(state: &AppState, headers: &HeaderMap) -> Result<(String, String), AppError> { +async fn admin_guard(state: &AppState, headers: &HeaderMap) -> Result<(Uuid, String), AppError> { let auth = state .auth_service .as_ref() @@ -72,7 +73,7 @@ async fn admin_guard(state: &AppState, headers: &HeaderMap) -> Result<(String, S )); } - Ok((claims.sub, 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 @@ -108,7 +109,7 @@ async fn save_oidc_settings( .as_ref() .ok_or_else(|| AppError::internal_error("Admin settings service not available"))?; - svc.save_oidc_settings(dto, &user_id) + svc.save_oidc_settings(dto, user_id) .await .map_err(|e| AppError::internal_error(format!("Failed to save settings: {}", e)))?; @@ -292,6 +293,8 @@ async fn get_user( ) -> Result { admin_guard(&state, &headers).await?; + let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?; + let auth = state .auth_service .as_ref() @@ -299,7 +302,7 @@ async fn get_user( let user = auth .auth_application_service - .get_user_admin(&id) + .get_user_admin(id) .await .map_err(|e| AppError::not_found(format!("User not found: {}", e)))?; @@ -314,6 +317,8 @@ async fn delete_user( ) -> Result { let (admin_id, _) = admin_guard(&state, &headers).await?; + let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?; + // Prevent self-deletion if admin_id == id { return Err(AppError::new( @@ -329,7 +334,7 @@ async fn delete_user( .ok_or_else(|| AppError::internal_error("Auth service not configured"))?; auth.auth_application_service - .delete_user_admin(&id) + .delete_user_admin(id) .await .map_err(|e| AppError::internal_error(format!("Failed to delete user: {}", e)))?; @@ -350,6 +355,8 @@ async fn update_user_role( ) -> Result { let (admin_id, _) = admin_guard(&state, &headers).await?; + let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?; + // Prevent changing own role if admin_id == id { return Err(AppError::new( @@ -365,7 +372,7 @@ async fn update_user_role( .ok_or_else(|| AppError::internal_error("Auth service not configured"))?; auth.auth_application_service - .change_user_role(&id, &dto.role) + .change_user_role(id, &dto.role) .await .map_err(|e| AppError::internal_error(format!("Failed to change role: {}", e)))?; @@ -386,6 +393,8 @@ async fn update_user_active( ) -> Result { let (admin_id, _) = admin_guard(&state, &headers).await?; + let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?; + // Prevent deactivating yourself if admin_id == id && !dto.active { return Err(AppError::new( @@ -401,7 +410,7 @@ async fn update_user_active( .ok_or_else(|| AppError::internal_error("Auth service not configured"))?; auth.auth_application_service - .set_user_active(&id, dto.active) + .set_user_active(id, dto.active) .await .map_err(|e| AppError::internal_error(format!("Failed to update user status: {}", e)))?; @@ -427,13 +436,15 @@ async fn update_user_quota( ) -> Result { admin_guard(&state, &headers).await?; + let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?; + let auth = state .auth_service .as_ref() .ok_or_else(|| AppError::internal_error("Auth service not configured"))?; auth.auth_application_service - .update_user_quota(&id, dto.quota_bytes) + .update_user_quota(id, dto.quota_bytes) .await .map_err(|e| AppError::internal_error(format!("Failed to update quota: {}", e)))?; @@ -487,13 +498,15 @@ async fn reset_user_password( ) -> Result { admin_guard(&state, &headers).await?; + let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?; + let auth = state .auth_service .as_ref() .ok_or_else(|| AppError::internal_error("Auth service not configured"))?; auth.auth_application_service - .admin_reset_password(&id, &dto.new_password) + .admin_reset_password(id, &dto.new_password) .await .map_err(|e| { AppError::new( @@ -558,7 +571,7 @@ async fn set_registration_setting( .as_ref() .ok_or_else(|| AppError::internal_error("Admin settings service not available"))?; - svc.set_registration_enabled(enabled, &admin_id) + svc.set_registration_enabled(enabled, admin_id) .await .map_err(|e| AppError::internal_error(format!("Failed to save setting: {}", e)))?; diff --git a/src/interfaces/api/handlers/app_password_handler.rs b/src/interfaces/api/handlers/app_password_handler.rs index 3acfa7d8..8e33ebc9 100755 --- a/src/interfaces/api/handlers/app_password_handler.rs +++ b/src/interfaces/api/handlers/app_password_handler.rs @@ -11,6 +11,7 @@ use axum::extract::State; use axum::routing::{delete, get, post}; use axum::{Json, Router}; use std::sync::Arc; +use uuid::Uuid; /// Protected routes — require JWT auth middleware. pub fn app_password_routes() -> Router> { @@ -35,7 +36,7 @@ async fn create_app_password( .ok_or_else(|| AppError::internal_error("App password service not configured"))?; let response = service - .create(&user.id, request) + .create(user.id, request) .await .map_err(AppError::from)?; @@ -55,7 +56,7 @@ async fn list_app_passwords( .as_ref() .ok_or_else(|| AppError::internal_error("App password service not configured"))?; - let response = service.list(&user.id).await.map_err(AppError::from)?; + let response = service.list(user.id).await.map_err(AppError::from)?; Ok(Json(response)) } @@ -72,8 +73,10 @@ async fn revoke_app_password( .as_ref() .ok_or_else(|| AppError::internal_error("App password service not configured"))?; + let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?; + let response = service - .revoke(&user.id, &id) + .revoke(user.id, id) .await .map_err(AppError::from)?; diff --git a/src/interfaces/api/handlers/auth_handler.rs b/src/interfaces/api/handlers/auth_handler.rs index 4d66b474..be6bd5d4 100755 --- a/src/interfaces/api/handlers/auth_handler.rs +++ b/src/interfaces/api/handlers/auth_handler.rs @@ -6,6 +6,7 @@ use axum::{ routing::{get, post, put}, }; use std::sync::Arc; +use uuid::Uuid; use crate::application::dtos::user_dto::{ ChangePasswordDto, LoginDto, OidcCallbackQueryDto, OidcExchangeDto, OidcProviderInfoDto, @@ -273,7 +274,7 @@ async fn get_current_user( if let Some(storage_usage_service) = state.storage_usage_service.as_ref() { // Calculate storage synchronously (we await the result) match storage_usage_service - .update_user_storage_usage(&user_id) + .update_user_storage_usage(user_id) .await { Ok(usage) => { @@ -293,7 +294,7 @@ async fn get_current_user( // Now get the user data WITH the updated storage let user = auth_service .auth_application_service - .get_user_by_id(&user_id) + .get_user_by_id(user_id) .await?; Ok((StatusCode::OK, Json(user))) @@ -311,7 +312,7 @@ async fn change_password( auth_service .auth_application_service - .change_password(&user_id, dto) + .change_password(user_id, dto) .await?; Ok(StatusCode::OK) @@ -341,7 +342,7 @@ async fn logout( auth_service .auth_application_service - .logout(&user_id, &refresh_token) + .logout(user_id, &refresh_token) .await?; // Clear HttpOnly + CSRF cookies so the browser forgets the session @@ -393,10 +394,10 @@ async fn setup_admin( } // 4. ATOMIC: claim initialization — only one concurrent request can win. - // We use a placeholder user_id ("pending") because the admin user + // We use Uuid::nil() as a placeholder because the admin user // doesn't exist yet. It will be updated to the real id below. let claimed = admin_svc - .try_claim_initialization("pending") + .try_claim_initialization(Uuid::nil()) .await .map_err(|e| { tracing::error!("Failed to claim system initialization: {}", e); @@ -426,7 +427,8 @@ async fn setup_admin( })?; // 5. Update the initialization record with the real admin user_id - if let Err(e) = admin_svc.mark_system_initialized(&user.id).await { + let real_user_id = Uuid::parse_str(&user.id).unwrap_or_default(); + if let Err(e) = admin_svc.mark_system_initialized(real_user_id).await { // Not fatal — the claim already prevents concurrent re-initialization, // and the "pending" marker is still "true" so the system stays locked. tracing::error!( @@ -603,7 +605,7 @@ async fn oidc_callback( let (_id, app_password) = nextcloud .app_passwords - .create_nc(&user_id, "Nextcloud (OIDC)") + .create_nc(user_id, "Nextcloud (OIDC)") .await .map_err(|e| { tracing::error!(error = %e, user = %username, "OIDC+NC: failed to create app password"); diff --git a/src/interfaces/api/handlers/batch_handler.rs b/src/interfaces/api/handlers/batch_handler.rs index 3bfc24be..903b1ff7 100755 --- a/src/interfaces/api/handlers/batch_handler.rs +++ b/src/interfaces/api/handlers/batch_handler.rs @@ -160,7 +160,7 @@ pub async fn move_files_batch( // Execute batch operation let result = state .batch_service - .move_files(request.file_ids, request.target_folder_id, &auth_user.id) + .move_files(request.file_ids, request.target_folder_id, auth_user.id) .await .map_err(|e| { tracing::error!("Batch move_files failed: {}", e); @@ -216,7 +216,7 @@ pub async fn copy_files_batch( // Execute batch operation let result = state .batch_service - .copy_files(request.file_ids, request.target_folder_id, &auth_user.id) + .copy_files(request.file_ids, request.target_folder_id, auth_user.id) .await .map_err(|e| { tracing::error!("Batch copy_files failed: {}", e); @@ -272,7 +272,7 @@ pub async fn delete_files_batch( // Execute batch operation let result = state .batch_service - .delete_files(request.file_ids, &auth_user.id) + .delete_files(request.file_ids, auth_user.id) .await .map_err(|e| { tracing::error!("Batch delete_files failed: {}", e); @@ -336,7 +336,7 @@ pub async fn delete_folders_batch( // Execute batch operation let result = state .batch_service - .delete_folders(request.folder_ids, request.recursive, &auth_user.id) + .delete_folders(request.folder_ids, request.recursive, auth_user.id) .await .map_err(|e| { tracing::error!("Batch delete_folders failed: {}", e); @@ -407,7 +407,7 @@ pub async fn create_folders_batch( // Execute batch operation let result = state .batch_service - .create_folders(folders, &auth_user.id) + .create_folders(folders, auth_user.id) .await .map_err(|e| { tracing::error!("Batch create_folders failed: {}", e); @@ -463,7 +463,7 @@ pub async fn get_files_batch( // Execute batch operation let result = state .batch_service - .get_multiple_files(request.file_ids, &auth_user.id) + .get_multiple_files(request.file_ids, auth_user.id) .await .map_err(|e| { tracing::error!("Batch get_files failed: {}", e); @@ -519,7 +519,7 @@ pub async fn get_folders_batch( // Execute batch operation let result = state .batch_service - .get_multiple_folders(request.folder_ids, &auth_user.id) + .get_multiple_folders(request.folder_ids, auth_user.id) .await .map_err(|e| { tracing::error!("Batch get_folders failed: {}", e); @@ -603,7 +603,7 @@ pub async fn trash_batch( if !request.file_ids.is_empty() { match state .batch_service - .trash_files(request.file_ids, &auth_user.id) + .trash_files(request.file_ids, auth_user.id) .await { Ok(result) => { @@ -630,7 +630,7 @@ pub async fn trash_batch( if !request.folder_ids.is_empty() { match state .batch_service - .trash_folders(request.folder_ids, &auth_user.id) + .trash_folders(request.folder_ids, auth_user.id) .await { Ok(result) => { @@ -707,7 +707,7 @@ pub async fn move_folders_batch( let result = state .batch_service - .move_folders(request.folder_ids, request.target_folder_id, &auth_user.id) + .move_folders(request.folder_ids, request.target_folder_id, auth_user.id) .await .map_err(|e| { tracing::error!("Batch move_folders failed: {}", e); @@ -760,7 +760,7 @@ pub async fn download_batch( let temp_file = state .batch_service - .download_zip(request.file_ids, request.folder_ids, &auth_user.id) + .download_zip(request.file_ids, request.folder_ids, auth_user.id) .await .map_err(|e| { tracing::error!("Batch download ZIP failed: {}", e); diff --git a/src/interfaces/api/handlers/caldav_handler.rs b/src/interfaces/api/handlers/caldav_handler.rs index 95b89aa5..f7f6e2b3 100755 --- a/src/interfaces/api/handlers/caldav_handler.rs +++ b/src/interfaces/api/handlers/caldav_handler.rs @@ -209,7 +209,7 @@ async fn handle_propfind( vec![] } else { calendar_service - .list_my_calendars(&user.id) + .list_my_calendars(user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to list calendars: {}", e)))? }; @@ -264,13 +264,13 @@ async fn handle_propfind( if parts.len() == 1 { // Single path segment: try as calendar ID first, fall back to user home - let calendar_result = calendar_service.get_calendar(first_segment, &user.id).await; + let calendar_result = calendar_service.get_calendar(first_segment, user.id).await; if let Ok(calendar) = calendar_result { // Valid calendar ID — return calendar collection let events = if depth != "0" { calendar_service - .list_events(first_segment, None, None, &user.id) + .list_events(first_segment, None, None, user.id) .await .unwrap_or_default() } else { @@ -300,7 +300,7 @@ async fn handle_propfind( // List all calendars for this user let calendars = calendar_service - .list_my_calendars(&user.id) + .list_my_calendars(user.id) .await .map_err(|e| { AppError::internal_error(format!("Failed to list calendars: {}", e)) @@ -328,7 +328,7 @@ async fn handle_propfind( let rest = parts[1]; // Check if first_segment is a valid calendar ID - let calendar_result = calendar_service.get_calendar(first_segment, &user.id).await; + let calendar_result = calendar_service.get_calendar(first_segment, user.id).await; let (calendar_id, event_path) = if calendar_result.is_ok() { // first_segment is a calendar ID, rest is event path @@ -341,13 +341,13 @@ async fn handle_propfind( // /caldav/{username}/{calendar_id} // Try to get this as a calendar collection let cal = calendar_service - .get_calendar(sub_parts[0], &user.id) + .get_calendar(sub_parts[0], user.id) .await .map_err(|e| AppError::not_found(format!("Calendar not found: {}", e)))?; let events = if depth != "0" { calendar_service - .list_events(sub_parts[0], None, None, &user.id) + .list_events(sub_parts[0], None, None, user.id) .await .unwrap_or_default() } else { @@ -384,7 +384,7 @@ async fn handle_propfind( let ical_uid = event_path.trim_end_matches(".ics"); let events = calendar_service - .list_events(calendar_id, None, None, &user.id) + .list_events(calendar_id, None, None, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?; @@ -444,14 +444,14 @@ async fn handle_report( CalDavReportType::CalendarQuery { time_range, .. } => { if let Some((start, end)) = time_range { calendar_service - .get_events_in_range(calendar_id, *start, *end, &user.id) + .get_events_in_range(calendar_id, *start, *end, user.id) .await .map_err(|e| { AppError::internal_error(format!("Failed to query events: {}", e)) })? } else { calendar_service - .list_events(calendar_id, None, None, &user.id) + .list_events(calendar_id, None, None, user.id) .await .map_err(|e| { AppError::internal_error(format!("Failed to list events: {}", e)) @@ -460,7 +460,7 @@ async fn handle_report( } CalDavReportType::CalendarMultiget { hrefs, .. } => { let all_events = calendar_service - .list_events(calendar_id, None, None, &user.id) + .list_events(calendar_id, None, None, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?; @@ -470,7 +470,7 @@ async fn handle_report( .collect() } CalDavReportType::SyncCollection { .. } => calendar_service - .list_events(calendar_id, None, None, &user.id) + .list_events(calendar_id, None, None, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?, }; @@ -526,7 +526,7 @@ async fn handle_mkcalendar( }; calendar_service - .create_calendar(create_dto, &user.id) + .create_calendar(create_dto, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to create calendar: {}", e)))?; @@ -566,7 +566,7 @@ async fn handle_put( let existing = if let Some(ref uid) = ical_uid { let events = calendar_service - .list_events(calendar_id, None, None, &user.id) + .list_events(calendar_id, None, None, user.id) .await .unwrap_or_default(); events.into_iter().find(|e| e.ical_uid == *uid) @@ -577,7 +577,7 @@ async fn handle_put( if let Some(existing_event) = existing { // Update existing event — re-create from iCal for full fidelity calendar_service - .delete_event(&existing_event.id, &user.id) + .delete_event(&existing_event.id, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to update event: {}", e)))?; @@ -586,7 +586,7 @@ async fn handle_put( ical_data, }; let event = calendar_service - .create_event_from_ical(create_dto, &user.id) + .create_event_from_ical(create_dto, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to recreate event: {}", e)))?; @@ -602,7 +602,7 @@ async fn handle_put( }; let event = calendar_service - .create_event_from_ical(create_dto, &user.id) + .create_event_from_ical(create_dto, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to create event: {}", e)))?; @@ -641,12 +641,12 @@ async fn handle_get( if parts.len() < 2 { // GET on calendar collection let events = calendar_service - .list_events(calendar_id, None, None, &user.id) + .list_events(calendar_id, None, None, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?; let calendar = calendar_service - .get_calendar(calendar_id, &user.id) + .get_calendar(calendar_id, user.id) .await .map_err(|e| AppError::not_found(format!("Calendar not found: {}", e)))?; @@ -664,7 +664,7 @@ async fn handle_get( let ical_uid = event_file.trim_end_matches(".ics"); let events = calendar_service - .list_events(calendar_id, None, None, &user.id) + .list_events(calendar_id, None, None, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?; @@ -760,7 +760,7 @@ async fn handle_delete( if parts.len() < 2 { calendar_service - .delete_calendar(calendar_id, &user.id) + .delete_calendar(calendar_id, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to delete calendar: {}", e)))?; } else { @@ -768,7 +768,7 @@ async fn handle_delete( let ical_uid = event_file.trim_end_matches(".ics"); let events = calendar_service - .list_events(calendar_id, None, None, &user.id) + .list_events(calendar_id, None, None, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?; @@ -778,7 +778,7 @@ async fn handle_delete( .ok_or_else(|| AppError::not_found(format!("Event not found: {}", ical_uid)))?; calendar_service - .delete_event(&event.id, &user.id) + .delete_event(&event.id, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to delete event: {}", e)))?; } @@ -833,7 +833,7 @@ async fn handle_proppatch( if update.name.is_some() || update.description.is_some() || update.color.is_some() { calendar_service - .update_calendar(calendar_id, update, &user.id) + .update_calendar(calendar_id, update, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to update calendar: {}", e)))?; } diff --git a/src/interfaces/api/handlers/carddav_handler.rs b/src/interfaces/api/handlers/carddav_handler.rs index 28412a79..2f432147 100755 --- a/src/interfaces/api/handlers/carddav_handler.rs +++ b/src/interfaces/api/handlers/carddav_handler.rs @@ -203,7 +203,7 @@ async fn handle_propfind( if path.is_empty() { // Root CardDAV path — list user's address books let address_books = addressbook_service - .list_user_address_books(&user.id) + .list_user_address_books(user.id) .await .map_err(|e| { AppError::internal_error(format!("Failed to list address books: {}", e)) @@ -231,13 +231,13 @@ async fn handle_propfind( if parts.len() == 1 { // Address book collection let address_book = addressbook_service - .get_address_book(address_book_id, &user.id) + .get_address_book(address_book_id, user.id) .await .map_err(|e| AppError::not_found(format!("Address book not found: {}", e)))?; let contacts = if depth != "0" { contact_svc - .list_contacts(address_book_id, &user.id) + .list_contacts(address_book_id, user.id) .await .unwrap_or_default() } else { @@ -269,7 +269,7 @@ async fn handle_propfind( // Look up by UID across all contacts in this address book let contacts = contact_svc - .list_contacts(address_book_id, &user.id) + .list_contacts(address_book_id, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?; @@ -331,12 +331,12 @@ async fn handle_report( let contacts = match &report { CardDavReportType::AddressbookQuery { .. } => contact_svc - .list_contacts(address_book_id, &user.id) + .list_contacts(address_book_id, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?, CardDavReportType::AddressbookMultiget { hrefs, .. } => { let all_contacts = contact_svc - .list_contacts(address_book_id, &user.id) + .list_contacts(address_book_id, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?; @@ -346,7 +346,7 @@ async fn handle_report( .collect() } CardDavReportType::SyncCollection { .. } => contact_svc - .list_contacts(address_book_id, &user.id) + .list_contacts(address_book_id, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?, }; @@ -403,7 +403,7 @@ async fn handle_mkcol( let create_dto = CreateAddressBookDto { name, - owner_id: user.id.clone(), + owner_id: user.id.to_string(), description, color, is_public: Some(false), @@ -452,7 +452,7 @@ async fn handle_put( // Check if contact already exists let existing = if let Some(ref uid) = vcard_uid { let contacts = contact_svc - .list_contacts(address_book_id, &user.id) + .list_contacts(address_book_id, user.id) .await .unwrap_or_default(); contacts.into_iter().find(|c| c.uid == *uid) @@ -463,14 +463,14 @@ async fn handle_put( if let Some(existing_contact) = existing { // Update: delete + recreate from vCard contact_svc - .delete_contact(&existing_contact.id, &user.id) + .delete_contact(&existing_contact.id, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to update contact: {}", e)))?; let create_dto = CreateContactVCardDto { address_book_id: address_book_id.to_string(), vcard: vcard_data, - user_id: user.id.clone(), + user_id: user.id.to_string(), }; let contact = contact_svc .create_contact_from_vcard(create_dto) @@ -486,7 +486,7 @@ async fn handle_put( let create_dto = CreateContactVCardDto { address_book_id: address_book_id.to_string(), vcard: vcard_data, - user_id: user.id.clone(), + user_id: user.id.to_string(), }; let contact = contact_svc @@ -529,7 +529,7 @@ async fn handle_get( if parts.len() < 2 { // GET on address book collection — return all contacts as vcf let contacts = contact_svc - .list_contacts(address_book_id, &user.id) + .list_contacts(address_book_id, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?; @@ -549,7 +549,7 @@ async fn handle_get( let contact_uid = contact_file.trim_end_matches(".vcf"); let contacts = contact_svc - .list_contacts(address_book_id, &user.id) + .list_contacts(address_book_id, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?; @@ -590,7 +590,7 @@ async fn handle_delete( if parts.len() < 2 { // Delete address book addressbook_service - .delete_address_book(address_book_id, &user.id) + .delete_address_book(address_book_id, user.id) .await .map_err(|e| { AppError::internal_error(format!("Failed to delete address book: {}", e)) @@ -601,7 +601,7 @@ async fn handle_delete( let contact_uid = contact_file.trim_end_matches(".vcf"); let contacts = contact_svc - .list_contacts(address_book_id, &user.id) + .list_contacts(address_book_id, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?; @@ -611,7 +611,7 @@ async fn handle_delete( .ok_or_else(|| AppError::not_found(format!("Contact not found: {}", contact_uid)))?; contact_svc - .delete_contact(&contact.id, &user.id) + .delete_contact(&contact.id, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to delete contact: {}", e)))?; } @@ -653,7 +653,7 @@ async fn handle_proppatch( description: None, color: None, is_public: None, - user_id: user.id.clone(), + user_id: user.id.to_string(), }; for prop in &props_to_set { diff --git a/src/interfaces/api/handlers/chunked_upload_handler.rs b/src/interfaces/api/handlers/chunked_upload_handler.rs index b1e31233..afbf387a 100755 --- a/src/interfaces/api/handlers/chunked_upload_handler.rs +++ b/src/interfaces/api/handlers/chunked_upload_handler.rs @@ -108,7 +108,7 @@ impl ChunkedUploadHandler { // ── Quota enforcement ──────────────────────────────────── if let Some(storage_svc) = state.storage_usage_service.as_ref() && let Err(err) = storage_svc - .check_storage_quota(&auth_user.id, request.total_size) + .check_storage_quota(auth_user.id, request.total_size) .await { tracing::warn!( @@ -146,7 +146,7 @@ impl ChunkedUploadHandler { match chunked_service .create_session( - &auth_user.id, + auth_user.id, request.filename, request.folder_id, content_type, @@ -192,7 +192,7 @@ impl ChunkedUploadHandler { match chunked_service .upload_chunk( &upload_id, - &auth_user.id, + auth_user.id, params.chunk_index, body, checksum, @@ -233,7 +233,7 @@ impl ChunkedUploadHandler { ) -> impl IntoResponse { let chunked_service = &state.core.chunked_upload_service; - match chunked_service.get_status(&upload_id, &auth_user.id).await { + match chunked_service.get_status(&upload_id, auth_user.id).await { Ok(status) => Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, "application/json") @@ -268,7 +268,7 @@ impl ChunkedUploadHandler { // Assemble chunks (hash-on-write: SHA-256 computed during assembly) let (assembled_path, filename, folder_id, content_type, total_size, hash) = match chunked_service - .complete_upload(&upload_id, &auth_user.id) + .complete_upload(&upload_id, auth_user.id) .await { Ok(result) => result, @@ -299,7 +299,7 @@ impl ChunkedUploadHandler { Ok(file) => { // Cleanup session let _ = chunked_service - .finalize_upload(&upload_id, &auth_user.id) + .finalize_upload(&upload_id, auth_user.id) .await; tracing::info!( @@ -338,7 +338,7 @@ impl ChunkedUploadHandler { let chunked_service = &state.core.chunked_upload_service; match chunked_service - .cancel_upload(&upload_id, &auth_user.id) + .cancel_upload(&upload_id, auth_user.id) .await { Ok(_) => StatusCode::NO_CONTENT.into_response(), diff --git a/src/interfaces/api/handlers/dedup_handler.rs b/src/interfaces/api/handlers/dedup_handler.rs index 96d88eb4..aaa52bb2 100755 --- a/src/interfaces/api/handlers/dedup_handler.rs +++ b/src/interfaces/api/handlers/dedup_handler.rs @@ -99,7 +99,7 @@ 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).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 +346,7 @@ impl DedupHandler { } // Verify the user owns at least one file referencing this blob - if !dedup.user_owns_blob_reference(&hash, &auth_user.id).await { + if !dedup.user_owns_blob_reference(&hash, &auth_user.id.to_string()).await { return Response::builder() .status(StatusCode::NOT_FOUND) .header(header::CONTENT_TYPE, "application/json") diff --git a/src/interfaces/api/handlers/device_auth_handler.rs b/src/interfaces/api/handlers/device_auth_handler.rs index b5af6f7a..c83b1d7d 100755 --- a/src/interfaces/api/handlers/device_auth_handler.rs +++ b/src/interfaces/api/handlers/device_auth_handler.rs @@ -16,6 +16,7 @@ use axum::{ routing::{delete, get, post}, }; use std::sync::Arc; +use uuid::Uuid; use crate::application::dtos::device_auth_dto::*; use crate::application::services::device_auth_service::DeviceAuthService; @@ -145,7 +146,7 @@ async fn device_verify_action( match body.action.to_lowercase().as_str() { "approve" | "allow" | "accept" => { device_service - .approve(&body.user_code, &auth_user.id) + .approve(&body.user_code, auth_user.id) .await .map_err(|e| { tracing::error!("Device approve failed: {}", e); @@ -181,7 +182,7 @@ async fn list_devices( let device_service = get_device_service(&state)?; let devices = device_service - .list_user_devices(&auth_user.id) + .list_user_devices(auth_user.id) .await .map_err(|e| { tracing::error!("List devices failed: {}", e); @@ -202,8 +203,10 @@ async fn revoke_device( ) -> Result { let device_service = get_device_service(&state)?; + let device_id = Uuid::parse_str(&device_id).map_err(|_| AppError::bad_request("Invalid device ID"))?; + device_service - .revoke_device(&device_id, &auth_user.id) + .revoke_device(device_id, auth_user.id) .await .map_err(|e| { tracing::error!("Revoke device failed: {}", e); diff --git a/src/interfaces/api/handlers/favorites_handler.rs b/src/interfaces/api/handlers/favorites_handler.rs index e08caea4..551b6753 100755 --- a/src/interfaces/api/handlers/favorites_handler.rs +++ b/src/interfaces/api/handlers/favorites_handler.rs @@ -30,7 +30,7 @@ pub async fn get_favorites( State(favorites_service): State>, auth_user: AuthUser, ) -> impl IntoResponse { - let user_id = &auth_user.id; + let user_id = auth_user.id; match favorites_service.get_favorites(user_id).await { Ok(favorites) => { @@ -56,7 +56,7 @@ pub async fn add_favorite( auth_user: AuthUser, Path((item_type, item_id)): Path<(String, String)>, ) -> impl IntoResponse { - let user_id = &auth_user.id; + let user_id = auth_user.id; // Validate item_type if item_type != "file" && item_type != "folder" { @@ -99,7 +99,7 @@ pub async fn remove_favorite( auth_user: AuthUser, Path((item_type, item_id)): Path<(String, String)>, ) -> impl IntoResponse { - let user_id = &auth_user.id; + let user_id = auth_user.id; match favorites_service .remove_from_favorites(user_id, &item_id, &item_type) @@ -143,7 +143,7 @@ pub async fn batch_add_favorites( auth_user: AuthUser, Json(body): Json, ) -> impl IntoResponse { - let user_id = &auth_user.id; + let user_id = auth_user.id; if body.items.is_empty() { return ( diff --git a/src/interfaces/api/handlers/file_handler.rs b/src/interfaces/api/handlers/file_handler.rs index b527e430..68fa15d0 100755 --- a/src/interfaces/api/handlers/file_handler.rs +++ b/src/interfaces/api/handlers/file_handler.rs @@ -109,7 +109,7 @@ 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, @@ -130,7 +130,7 @@ impl FileHandler { .and_then(|s| s.parse::().ok()) .unwrap_or(0); if let Err(err) = storage_svc - .check_storage_quota(&auth_user.id, estimated_size) + .check_storage_quota(auth_user.id, estimated_size) .await { tracing::warn!( @@ -235,7 +235,7 @@ impl FileHandler { // ── Quota enforcement ──────────────────────────────── if let Some(storage_svc) = state.storage_usage_service.as_ref() && let Err(err) = storage_svc - .check_storage_quota(&auth_user.id, total_size) + .check_storage_quota(auth_user.id, total_size) .await { let _ = tokio::fs::remove_file(&temp_path).await; @@ -321,7 +321,7 @@ impl FileHandler { }; let file = match file_retrieval_service - .get_file_owned(&id, &auth_user.id) + .get_file_owned(&id, auth_user.id) .await { Ok(f) => f, @@ -397,7 +397,7 @@ impl FileHandler { let retrieval = &state.applications.file_retrieval_service; // ── Get file metadata (ownership-scoped) ──────────────────────── - let file_dto = match retrieval.get_file_owned(&id, &auth_user.id).await { + let file_dto = match retrieval.get_file_owned(&id, auth_user.id).await { Ok(f) => f, Err(err) => { return AppError::from(err).into_response(); @@ -455,7 +455,7 @@ impl FileHandler { Self::content_disposition(&file_dto.name, &file_dto.mime_type, ¶ms); match retrieval - .get_file_range_stream_owned(&id, &auth_user.id, start, Some(end + 1)) + .get_file_range_stream_owned(&id, auth_user.id, start, Some(end + 1)) .await { Ok(stream) => { @@ -569,7 +569,7 @@ impl FileHandler { tracing::info!("API: Listing files with folder_id: {:?}", folder_id); let retrieval = &state.applications.file_retrieval_service; - match retrieval.list_files_owned(folder_id, &auth_user.id).await { + match retrieval.list_files_owned(folder_id, auth_user.id).await { Ok(files) => { // Compute lightweight ETag from max modified_at + count let max_mod = files.iter().map(|f| f.modified_at).max().unwrap_or(0); @@ -655,7 +655,7 @@ impl FileHandler { ) -> impl IntoResponse { // Verify ownership let file_read = &state.repositories.file_read_repository; - if let Err(e) = file_read.verify_file_owner(&file_id, &auth_user.id).await { + if let Err(e) = file_read.verify_file_owner(&file_id, auth_user.id).await { let msg = e.to_string(); return ( StatusCode::NOT_FOUND, @@ -703,7 +703,7 @@ impl FileHandler { // Auth required: trash-first with dedup cleanup + ownership verification let result = mgmt - .delete_with_cleanup(&id, &auth_user.id) + .delete_with_cleanup(&id, auth_user.id) .await .map(|was_trashed| { if was_trashed { @@ -745,7 +745,7 @@ impl FileHandler { tracing::info!("Renaming file {} to \"{}\"", id, new_name); let mgmt = &state.applications.file_management_service; - match mgmt.rename_file_owned(&id, &auth_user.id, &new_name).await { + 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() } @@ -763,7 +763,7 @@ impl FileHandler { let mgmt = &state.applications.file_management_service; match mgmt - .move_file_owned(&id, &auth_user.id, payload.folder_id) + .move_file_owned(&id, auth_user.id, payload.folder_id) .await { Ok(file) => (StatusCode::OK, Json(file)).into_response(), @@ -784,7 +784,7 @@ impl FileHandler { .map(|s| s.to_string()); let mgmt = &state.applications.file_management_service; - match mgmt.move_file_owned(&id, &auth_user.id, folder_id).await { + 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() } diff --git a/src/interfaces/api/handlers/folder_handler.rs b/src/interfaces/api/handlers/folder_handler.rs index e7d830f4..331ba6ac 100755 --- a/src/interfaces/api/handlers/folder_handler.rs +++ b/src/interfaces/api/handlers/folder_handler.rs @@ -42,7 +42,7 @@ impl FolderHandler { "create_folder: parent_id is None for user '{}', resolving home folder", auth_user.username ); - match service.list_folders_for_owner(None, &auth_user.id).await { + match service.list_folders_for_owner(None, auth_user.id).await { Ok(folders) => { if let Some(home) = folders.first() { tracing::info!( @@ -71,7 +71,7 @@ impl FolderHandler { if let Some(ref parent_id) = dto.parent_id { use crate::application::ports::inbound::FolderUseCase; if service - .get_folder_owned(parent_id, &auth_user.id) + .get_folder_owned(parent_id, auth_user.id) .await .is_err() { @@ -102,7 +102,7 @@ impl FolderHandler { Ok(folder) => { // Access check: folder must belong to the requesting user if let Some(ref owner) = folder.owner_id - && owner != &auth_user.id + && owner != &auth_user.id.to_string() { tracing::warn!( "get_folder: user '{}' attempted to access folder '{}' owned by '{}'", @@ -155,7 +155,7 @@ impl FolderHandler { pagination: Query, ) -> axum::response::Response { match service - .list_folders_for_owner_paginated(Some(&id), &auth_user.id, &pagination) + .list_folders_for_owner_paginated(Some(&id), auth_user.id, &pagination) .await { Ok(paginated_result) => (StatusCode::OK, Json(paginated_result)).into_response(), @@ -172,7 +172,7 @@ impl FolderHandler { auth_user: &AuthUser, ) -> axum::response::Response { match service - .list_folders_for_owner(parent_id, &auth_user.id) + .list_folders_for_owner(parent_id, auth_user.id) .await { Ok(folders) => (StatusCode::OK, Json(folders)).into_response(), @@ -215,8 +215,8 @@ impl FolderHandler { // Run both queries concurrently — no sequential wait. let (folders_result, files_result) = tokio::join!( - folder_service.list_folders_for_owner(Some(&id), &auth_user.id), - file_service.list_files_owned(Some(&id), &auth_user.id) + folder_service.list_folders_for_owner(Some(&id), auth_user.id), + file_service.list_files_owned(Some(&id), auth_user.id) ); match (folders_result, files_result) { @@ -253,7 +253,7 @@ impl FolderHandler { Path(id): Path, Json(dto): Json, ) -> impl IntoResponse { - match service.rename_folder(&id, dto, &auth_user.id).await { + match service.rename_folder(&id, dto, auth_user.id).await { Ok(folder) => (StatusCode::OK, Json(folder)).into_response(), Err(err) => AppError::from(err).into_response(), } @@ -266,7 +266,7 @@ impl FolderHandler { Path(id): Path, Json(dto): Json, ) -> impl IntoResponse { - match service.move_folder(&id, dto, &auth_user.id).await { + match service.move_folder(&id, dto, auth_user.id).await { Ok(folder) => (StatusCode::OK, Json(folder)).into_response(), Err(err) => AppError::from(err).into_response(), } @@ -278,7 +278,7 @@ impl FolderHandler { auth_user: AuthUser, Path(id): Path, ) -> impl IntoResponse { - match service.delete_folder(&id, &auth_user.id).await { + match service.delete_folder(&id, auth_user.id).await { Ok(_) => StatusCode::NO_CONTENT.into_response(), Err(err) => AppError::from(err).into_response(), } @@ -290,7 +290,7 @@ impl FolderHandler { auth_user: AuthUser, Path(id): Path, ) -> impl IntoResponse { - let user_id = &auth_user.id; + let user_id = auth_user.id; // Check if trash service is available if let Some(trash_service) = &state.trash_service { tracing::info!("Moving folder to trash: {}", id); @@ -337,7 +337,7 @@ impl FolderHandler { match folder_service.get_folder(&id).await { Ok(folder) => { // Access check: folder must belong to the requesting user - if folder.owner_id.as_deref() != Some(&auth_user.id) { + if folder.owner_id.as_deref() != Some(&auth_user.id.to_string()) { tracing::warn!( "download_folder_zip: user '{}' attempted to download folder '{}' owned by '{:?}'", auth_user.id, diff --git a/src/interfaces/api/handlers/photos_handler.rs b/src/interfaces/api/handlers/photos_handler.rs index 3cdaa32c..f660a3a0 100755 --- a/src/interfaces/api/handlers/photos_handler.rs +++ b/src/interfaces/api/handlers/photos_handler.rs @@ -31,7 +31,7 @@ pub async fn list_photos( auth_user: AuthUser, Query(params): Query, ) -> impl IntoResponse { - let user_id = &auth_user.id; + let user_id = auth_user.id; let limit = params.limit.unwrap_or(200).clamp(1, 500); let file_read = &state.repositories.file_read_repository; diff --git a/src/interfaces/api/handlers/recent_handler.rs b/src/interfaces/api/handlers/recent_handler.rs index 0bfde753..b6bfd41f 100755 --- a/src/interfaces/api/handlers/recent_handler.rs +++ b/src/interfaces/api/handlers/recent_handler.rs @@ -25,7 +25,7 @@ pub async fn get_recent_items( auth_user: AuthUser, Query(params): Query, ) -> impl IntoResponse { - let user_id = &auth_user.id; + let user_id = auth_user.id; match recent_service.get_recent_items(user_id, params.limit).await { Ok(items) => { @@ -51,7 +51,7 @@ pub async fn record_item_access( auth_user: AuthUser, Path((item_type, item_id)): Path<(String, String)>, ) -> impl IntoResponse { - let user_id = &auth_user.id; + let user_id = auth_user.id; // Validate item type if item_type != "file" && item_type != "folder" { @@ -97,7 +97,7 @@ pub async fn remove_from_recent( auth_user: AuthUser, Path((item_type, item_id)): Path<(String, String)>, ) -> impl IntoResponse { - let user_id = &auth_user.id; + let user_id = auth_user.id; match recent_service .remove_from_recent(user_id, &item_id, &item_type) @@ -142,7 +142,7 @@ pub async fn clear_recent_items( State(recent_service): State>, auth_user: AuthUser, ) -> impl IntoResponse { - let user_id = &auth_user.id; + let user_id = auth_user.id; match recent_service.clear_recent_items(user_id).await { Ok(_) => { diff --git a/src/interfaces/api/handlers/search_handler.rs b/src/interfaces/api/handlers/search_handler.rs index 32ebabdb..0ee4b622 100755 --- a/src/interfaces/api/handlers/search_handler.rs +++ b/src/interfaces/api/handlers/search_handler.rs @@ -60,7 +60,7 @@ impl SearchHandler { sort_by: params.sort_by.unwrap_or_else(|| "relevance".to_string()), }; - match search_service.search(search_criteria, &auth_user.id).await { + match search_service.search(search_criteria, auth_user.id).await { Ok(results) => { info!( "Search completed in {}ms — {} files, {} folders", @@ -101,7 +101,7 @@ impl SearchHandler { } }; - match search_service.search(criteria, &auth_user.id).await { + match search_service.search(criteria, auth_user.id).await { Ok(results) => { info!( "Advanced search completed in {}ms — {} files, {} folders", diff --git a/src/interfaces/api/handlers/share_handler.rs b/src/interfaces/api/handlers/share_handler.rs index e92db963..3c0853d2 100755 --- a/src/interfaces/api/handlers/share_handler.rs +++ b/src/interfaces/api/handlers/share_handler.rs @@ -1,4 +1,5 @@ use std::sync::Arc; +use uuid::Uuid; use axum::{ Json, @@ -40,7 +41,7 @@ pub async fn create_shared_link( auth_user: AuthUser, Json(dto): Json, ) -> impl IntoResponse { - match share_use_case.create_shared_link(&auth_user.id, dto).await { + match share_use_case.create_shared_link(auth_user.id, dto).await { Ok(share) => (StatusCode::CREATED, Json(share)).into_response(), Err(err) => AppError::from(err).into_response(), } @@ -52,7 +53,11 @@ pub async fn get_shared_link( auth_user: AuthUser, Path(id): Path, ) -> impl IntoResponse { - match share_use_case.get_shared_link(&id, &auth_user.id).await { + let id = match Uuid::parse_str(&id) { + Ok(id) => id, + Err(_) => return AppError::bad_request("Invalid UUID").into_response(), + }; + match share_use_case.get_shared_link(id, auth_user.id).await { Ok(share) => (StatusCode::OK, Json(share)).into_response(), Err(err) => AppError::from(err).into_response(), } @@ -65,7 +70,7 @@ pub async fn get_user_shares( auth_user: AuthUser, Query(query): Query, ) -> impl IntoResponse { - let user_id = &auth_user.id; + let user_id = auth_user.id; // If both item_id and item_type are provided, return shares for that specific item if let (Some(item_id), Some(item_type_str)) = (&query.item_id, &query.item_type) { @@ -108,8 +113,12 @@ pub async fn update_shared_link( Path(id): Path, Json(dto): Json, ) -> impl IntoResponse { + let id = match Uuid::parse_str(&id) { + Ok(id) => id, + Err(_) => return AppError::bad_request("Invalid UUID").into_response(), + }; match share_use_case - .update_shared_link(&id, &auth_user.id, dto) + .update_shared_link(id, auth_user.id, dto) .await { Ok(share) => (StatusCode::OK, Json(share)).into_response(), @@ -123,7 +132,11 @@ pub async fn delete_shared_link( auth_user: AuthUser, Path(id): Path, ) -> impl IntoResponse { - match share_use_case.delete_shared_link(&id, &auth_user.id).await { + let id = match Uuid::parse_str(&id) { + Ok(id) => id, + Err(_) => return AppError::bad_request("Invalid UUID").into_response(), + }; + match share_use_case.delete_shared_link(id, auth_user.id).await { Ok(_) => StatusCode::NO_CONTENT.into_response(), Err(err) => AppError::from(err).into_response(), } diff --git a/src/interfaces/api/handlers/trash_handler.rs b/src/interfaces/api/handlers/trash_handler.rs index 1bffc6d0..3bd02b56 100755 --- a/src/interfaces/api/handlers/trash_handler.rs +++ b/src/interfaces/api/handlers/trash_handler.rs @@ -18,7 +18,7 @@ pub async fn get_trash_items( // SECURITY: Always use the authenticated user's ID from the JWT token. // Never allow user ID override via query parameters to prevent // privilege escalation attacks. - let effective_user = auth_user.id.clone(); + let effective_user = auth_user.id; debug!("Request to list trash items for user {}", effective_user); @@ -34,7 +34,7 @@ pub async fn get_trash_items( } }; - let result = trash_service.get_trash_items(&effective_user).await; + let result = trash_service.get_trash_items(effective_user).await; match result { Ok(items) => { @@ -60,7 +60,7 @@ pub async fn move_file_to_trash( auth_user: AuthUser, Path(item_id): Path, ) -> (StatusCode, Json) { - let user_id = &auth_user.id; + let user_id = auth_user.id; debug!( "Request to move file to trash: id={}, user={}", item_id, user_id @@ -111,7 +111,7 @@ pub async fn move_folder_to_trash( auth_user: AuthUser, Path(item_id): Path, ) -> (StatusCode, Json) { - let user_id = &auth_user.id; + let user_id = auth_user.id; debug!( "Request to move folder to trash: id={}, user={}", item_id, user_id @@ -177,7 +177,7 @@ pub async fn restore_from_trash( ); } }; - let result = trash_service.restore_item(&trash_id, &auth_user.id).await; + let result = trash_service.restore_item(&trash_id, auth_user.id).await; match result { Ok(_) => { @@ -239,7 +239,7 @@ pub async fn delete_permanently( } }; let result = trash_service - .delete_permanently(&trash_id, &auth_user.id) + .delete_permanently(&trash_id, auth_user.id) .await; match result { @@ -300,7 +300,7 @@ pub async fn empty_trash( ); } }; - let result = trash_service.empty_trash(&auth_user.id).await; + let result = trash_service.empty_trash(auth_user.id).await; match result { Ok(_) => { diff --git a/src/interfaces/api/handlers/webdav_handler.rs b/src/interfaces/api/handlers/webdav_handler.rs index 2ab6441c..40f2dcbc 100755 --- a/src/interfaces/api/handlers/webdav_handler.rs +++ b/src/interfaces/api/handlers/webdav_handler.rs @@ -183,7 +183,7 @@ async fn handle_webdav_methods( /// If `path` doesn't already start with the user's home folder name, prepend /// the home folder path so downstream services can find the resource in the DB. /// Returns `None` when the path already includes the prefix or resolution fails. -async fn resolve_webdav_path(state: &Arc, user_id: &str, path: &str) -> Option { +async fn resolve_webdav_path(state: &Arc, user_id: Uuid, path: &str) -> Option { let folder_service = &state.applications.folder_service; let home_folders = folder_service .list_folders_for_owner(None, user_id) @@ -209,9 +209,9 @@ async fn handle_webdav_dispatch( // prefix when the path doesn't already include it. // Extract user_id before any async call to keep the future Send. let path = if !path.is_empty() && method.as_str() != "OPTIONS" { - let user_id = req.extensions().get::>().map(|u| u.id.clone()); + let user_id = req.extensions().get::>().map(|u| u.id); if let Some(uid) = user_id { - resolve_webdav_path(&state, &uid, &path) + resolve_webdav_path(&state, uid, &path) .await .unwrap_or(path) } else { @@ -371,14 +371,14 @@ async fn handle_propfind( propfind_request, folder_service, file_retrieval_service, - &user.id, + user.id, ) .await; } // Single-query path resolution: folder OR file in one DB round-trip if let Some(resolver) = &state.path_resolver { - match resolver.resolve_path_for_user(&path, &user.id).await { + match resolver.resolve_path_for_user(&path, user.id).await { Ok(ResolvedResource::Folder(folder)) => { let folder_id = folder.id.clone(); return build_streaming_propfind_response( @@ -389,7 +389,7 @@ async fn handle_propfind( propfind_request, folder_service, file_retrieval_service, - &user.id, + user.id, ) .await; } @@ -420,7 +420,7 @@ async fn handle_propfind( } else { // Fallback: legacy double-query path when PathResolver is unavailable if let Ok(folder) = folder_service.get_folder_by_path(&path).await { - assert_owner(folder.owner_id.as_deref(), &user.id, &path)?; + assert_owner(folder.owner_id.as_deref(), &user.id.to_string(), &path)?; let folder_id = folder.id.clone(); return build_streaming_propfind_response( folder, @@ -430,12 +430,12 @@ async fn handle_propfind( propfind_request, folder_service, file_retrieval_service, - &user.id, + user.id, ) .await; } if let Ok(file) = file_retrieval_service.get_file_by_path(&path).await { - assert_owner(file.owner_id.as_deref(), &user.id, &path)?; + assert_owner(file.owner_id.as_deref(), &user.id.to_string(), &path)?; let mut buf = Vec::with_capacity(1024); { let mut xml_writer = Writer::new(&mut buf); @@ -477,12 +477,11 @@ async fn build_streaming_propfind_response( propfind_request: PropFindRequest, folder_service: std::sync::Arc, file_retrieval_service: std::sync::Arc, - user_id: &str, + user_id: Uuid, ) -> Result, AppError> { let depth = depth.to_string(); let base_href = base_href.to_string(); let propfind_request = Arc::new(propfind_request); - let user_id = user_id.to_string(); let stream = async_stream::try_stream! { // ── XML header + + folder entry ────────── @@ -512,7 +511,7 @@ async fn build_streaming_propfind_response( page_size: pagination.page_size, }; let result = folder_service - .list_folders_for_owner_paginated(fid_ref, &user_id, &pag) + .list_folders_for_owner_paginated(fid_ref, user_id, &pag) .await .map_err(|e| std::io::Error::other(e.to_string()))?; @@ -542,7 +541,7 @@ async fn build_streaming_propfind_response( let mut offset: i64 = 0; loop { let batch: Vec = file_retrieval_service - .list_files_batch_for_owner(fid_ref, &user_id, offset, PROPFIND_BATCH_SIZE) + .list_files_batch_for_owner(fid_ref, user_id, offset, PROPFIND_BATCH_SIZE) .await .map_err(|e| std::io::Error::other(e.to_string()))?; @@ -675,7 +674,7 @@ async fn handle_get( // Resolve file — user-scoped when PathResolver is available let file = if let Some(resolver) = &state.path_resolver { - match resolver.resolve_path_for_user(&path, &user.id).await { + match resolver.resolve_path_for_user(&path, user.id).await { Ok(ResolvedResource::File(f)) => f, Ok(ResolvedResource::Folder(_)) => { return Err(AppError::bad_request("Cannot GET a directory")); @@ -690,7 +689,7 @@ async fn handle_get( .get_file_by_path(&path) .await .map_err(|_e| AppError::not_found(format!("File not found: {}", path)))?; - assert_owner(f.owner_id.as_deref(), &user.id, &path)?; + assert_owner(f.owner_id.as_deref(), &user.id.to_string(), &path)?; f }; @@ -740,7 +739,7 @@ async fn handle_head( // Single-query path resolution (user-scoped) if let Some(resolver) = &state.path_resolver { - match resolver.resolve_path_for_user(&path, &user.id).await { + match resolver.resolve_path_for_user(&path, user.id).await { Ok(ResolvedResource::Folder(folder)) => { return Ok(Response::builder() .status(StatusCode::OK) @@ -771,7 +770,7 @@ async fn handle_head( // Fallback: legacy double-query path (with ownership check) if let Ok(folder) = folder_service.get_folder_by_path(&path).await { - assert_owner(folder.owner_id.as_deref(), &user.id, &path)?; + assert_owner(folder.owner_id.as_deref(), &user.id.to_string(), &path)?; return Ok(Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, "httpd/unix-directory") @@ -786,7 +785,7 @@ async fn handle_head( .get_file_by_path(&path) .await .map_err(|_e| AppError::not_found(format!("Resource not found: {}", path)))?; - assert_owner(file.owner_id.as_deref(), &user.id, &path)?; + assert_owner(file.owner_id.as_deref(), &user.id.to_string(), &path)?; Ok(Response::builder() .status(StatusCode::OK) @@ -840,7 +839,7 @@ async fn handle_put( // parent folder (create). Without this check a user could // overwrite another user's file via a crafted PUT path. if let Some(resolver) = &state.path_resolver { - match resolver.resolve_path_for_user(&path, &user.id).await { + match resolver.resolve_path_for_user(&path, user.id).await { Ok(ResolvedResource::File(_)) => { /* existing file owned by user — OK */ } Ok(ResolvedResource::Folder(_)) => { return Err(AppError::bad_request("Cannot PUT to a directory")); @@ -854,7 +853,7 @@ async fn handle_put( }; if !parent_path.is_empty() { resolver - .resolve_path_for_user(parent_path, &user.id) + .resolve_path_for_user(parent_path, user.id) .await .map_err(|_| { AppError::not_found(format!("Parent folder not found: {}", parent_path)) @@ -1052,10 +1051,10 @@ async fn handle_delete( // Single-query path resolution (user-scoped) if let Some(resolver) = &state.path_resolver { - match resolver.resolve_path_for_user(&path, &user.id).await { + match resolver.resolve_path_for_user(&path, user.id).await { Ok(ResolvedResource::Folder(folder)) => { folder_service - .delete_folder(&folder.id, &user.id) + .delete_folder(&folder.id, user.id) .await .map_err(|e| { AppError::internal_error(format!("Failed to delete folder: {}", e)) @@ -1076,9 +1075,9 @@ async fn handle_delete( let folder_result = folder_service.get_folder_by_path(&path).await; if let Ok(folder) = folder_result { - assert_owner(folder.owner_id.as_deref(), &user.id, &path)?; + assert_owner(folder.owner_id.as_deref(), &user.id.to_string(), &path)?; folder_service - .delete_folder(&folder.id, &user.id) + .delete_folder(&folder.id, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to delete folder: {}", e)))?; } else { @@ -1086,7 +1085,7 @@ async fn handle_delete( .get_file_by_path(&path) .await .map_err(|_e| AppError::not_found(format!("Resource not found: {}", path)))?; - assert_owner(file.owner_id.as_deref(), &user.id, &path)?; + assert_owner(file.owner_id.as_deref(), &user.id.to_string(), &path)?; file_management_service .delete_file(&file.id) @@ -1157,7 +1156,7 @@ async fn handle_move( if !overwrite { let dest_exists = if let Some(resolver) = &state.path_resolver { resolver - .exists_for_user(&destination_path, &user.id) + .exists_for_user(&destination_path, user.id) .await .unwrap_or(false) } else { @@ -1179,7 +1178,7 @@ async fn handle_move( // Resolve source: single-query when PathResolver is available (user-scoped) if let Some(resolver) = &state.path_resolver { - match resolver.resolve_path_for_user(&source_path, &user.id).await { + match resolver.resolve_path_for_user(&source_path, user.id).await { Ok(ResolvedResource::Folder(folder)) => { let dest_folder_name = destination_path .split('/') @@ -1200,7 +1199,7 @@ async fn handle_move( // SECURITY: verify destination parent belongs to caller (V-08) assert_owner( parent.owner_id.as_deref(), - &user.id, + &user.id.to_string(), dest_parent_path, )?; Some(parent.id) @@ -1211,7 +1210,7 @@ async fn handle_move( }; folder_service - .move_folder(&folder.id, move_dto, &user.id) + .move_folder(&folder.id, move_dto, user.id) .await .map_err(|e| { AppError::internal_error(format!("Failed to move folder: {}", e)) @@ -1222,7 +1221,7 @@ async fn handle_move( name: dest_folder_name.to_string(), }; folder_service - .rename_folder(&folder.id, rename_dto, &user.id) + .rename_folder(&folder.id, rename_dto, user.id) .await .map_err(|e| { AppError::internal_error(format!("Failed to rename folder: {}", e)) @@ -1251,7 +1250,7 @@ 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, 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 +1280,7 @@ 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, &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 +1298,7 @@ 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, dest_parent_path)?; + assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?; Some(parent.id) } Err(_) => None, @@ -1308,7 +1307,7 @@ async fn handle_move( }; folder_service - .move_folder(&folder.id, move_dto, &user.id) + .move_folder(&folder.id, move_dto, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to move folder: {}", e)))?; @@ -1317,7 +1316,7 @@ async fn handle_move( name: dest_folder_name.to_string(), }; folder_service - .rename_folder(&folder.id, rename_dto, &user.id) + .rename_folder(&folder.id, rename_dto, user.id) .await .map_err(|e| { AppError::internal_error(format!("Failed to rename folder: {}", e)) @@ -1330,7 +1329,7 @@ async fn handle_move( .map_err(|_e| { AppError::not_found(format!("Resource not found: {}", source_path)) })?; - assert_owner(file.owner_id.as_deref(), &user.id, &source_path)?; + assert_owner(file.owner_id.as_deref(), &user.id.to_string(), &source_path)?; let dest_filename = destination_path .split('/') @@ -1352,7 +1351,7 @@ 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, 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())) @@ -1438,7 +1437,7 @@ async fn handle_copy( if !overwrite { let dest_exists = if let Some(resolver) = &state.path_resolver { resolver - .exists_for_user(&destination_path, &user.id) + .exists_for_user(&destination_path, user.id) .await .unwrap_or(false) } else { @@ -1460,7 +1459,7 @@ async fn handle_copy( // Resolve source: single-query when PathResolver is available (user-scoped) if let Some(resolver) = &state.path_resolver { - match resolver.resolve_path_for_user(&source_path, &user.id).await { + match resolver.resolve_path_for_user(&source_path, user.id).await { Ok(ResolvedResource::Folder(folder)) => { let recursive = depth != "0"; @@ -1480,7 +1479,7 @@ 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, dest_parent_path)?; + assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?; Some(parent.id) } Err(_) => None, @@ -1528,7 +1527,7 @@ 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, dest_parent_path)?; + assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?; Some(parent.id) } Err(_) => None, @@ -1553,7 +1552,7 @@ 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, &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 +1571,7 @@ 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, dest_parent_path)?; + assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?; Some(parent.id) } Err(_) => None, @@ -1613,7 +1612,7 @@ async fn handle_copy( .map_err(|_e| { AppError::not_found(format!("Resource not found: {}", source_path)) })?; - assert_owner(file.owner_id.as_deref(), &user.id, &source_path)?; + assert_owner(file.owner_id.as_deref(), &user.id.to_string(), &source_path)?; let dest_parent_path = if let Some(idx) = destination_path.rfind('/') { &destination_path[..idx] @@ -1627,7 +1626,7 @@ 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, dest_parent_path)?; + assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?; Some(parent.id) } Err(_) => None, @@ -1737,7 +1736,7 @@ async fn handle_lock( let token = format!("opaquelocktoken:{}", Uuid::new_v4()); let lock_info = LockInfo { token, - owner: owner.or(Some(user.id.clone())), + owner: owner.or(Some(user.id.to_string())), depth: depth.to_string(), timeout, scope, diff --git a/src/interfaces/api/handlers/wopi_handler.rs b/src/interfaces/api/handlers/wopi_handler.rs index 435c43f4..bac0a70c 100755 --- a/src/interfaces/api/handlers/wopi_handler.rs +++ b/src/interfaces/api/handlers/wopi_handler.rs @@ -396,7 +396,7 @@ pub struct EditorUrlResponse { async fn authorize_wopi_access( file_retrieval: &S, file_id: &str, - caller_id: &str, + caller_id: uuid::Uuid, requested_action: &str, ) -> Result<(crate::application::dtos::file_dto::FileDto, bool), StatusCode> { let file = file_retrieval @@ -425,7 +425,7 @@ pub async fn get_editor_url( let (file, can_write) = match authorize_wopi_access( state.app_state.applications.file_retrieval_service.as_ref(), ¶ms.file_id, - &user_id, + user_id, ¶ms.action, ) .await @@ -464,7 +464,7 @@ pub async fn get_editor_url( let (access_token, access_token_ttl) = match state .token_service - .generate_token(¶ms.file_id, &user_id, &username, can_write) + .generate_token(¶ms.file_id, &user_id.to_string(), &username, can_write) { Ok(t) => t, Err(e) => { @@ -503,10 +503,14 @@ async fn host_page( // Re-verify ownership even though the token was valid — defence in depth. let requested_action = if claims.can_write { "edit" } else { "view" }; + let caller_uuid = match uuid::Uuid::parse_str(&claims.sub) { + Ok(u) => u, + Err(_) => return StatusCode::UNAUTHORIZED.into_response(), + }; let file = match authorize_wopi_access( state.app_state.applications.file_retrieval_service.as_ref(), &file_id, - &claims.sub, + caller_uuid, requested_action, ) .await diff --git a/src/interfaces/middleware/auth.rs b/src/interfaces/middleware/auth.rs index e0d3eba9..0b4b5cd0 100755 --- a/src/interfaces/middleware/auth.rs +++ b/src/interfaces/middleware/auth.rs @@ -6,6 +6,7 @@ use axum::{ }; use std::convert::Infallible; use std::sync::Arc; +use uuid::Uuid; use crate::common::di::AppState; @@ -22,7 +23,7 @@ pub struct CookieAuthenticated; // Structure for use in Axum extractors #[derive(Clone, Debug)] pub struct AuthUser { - pub id: String, + pub id: Uuid, pub username: String, pub role: String, } @@ -35,7 +36,7 @@ pub struct AuthUser { /// async fn my_handler(CurrentUserId(user_id): CurrentUserId) -> impl IntoResponse { ... } /// ``` #[derive(Clone, Debug)] -pub struct CurrentUserId(pub String); +pub struct CurrentUserId(pub Uuid); // Implement FromRequestParts for AuthUser — allows using `auth_user: AuthUser` in handlers impl FromRequestParts for AuthUser @@ -49,7 +50,7 @@ where .extensions .get::>() .map(|cu| AuthUser { - id: cu.id.clone(), + id: cu.id, username: cu.username.clone(), role: cu.role.clone(), }) @@ -86,7 +87,7 @@ where parts .extensions .get::>() - .map(|cu| CurrentUserId(cu.id.clone())) + .map(|cu| CurrentUserId(cu.id)) .ok_or(AuthError::UserNotFound) } } @@ -94,7 +95,7 @@ where /// Optional user ID extractor – never fails. /// Yields `Some(id)` when auth middleware ran, `None` otherwise. #[derive(Clone, Debug)] -pub struct OptionalUserId(pub Option); +pub struct OptionalUserId(pub Option); impl FromRequestParts for OptionalUserId where @@ -107,7 +108,7 @@ where parts .extensions .get::>() - .map(|cu| cu.id.clone()), + .map(|cu| cu.id), )) } } @@ -126,7 +127,7 @@ where async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { Ok(OptionalAuthUser(parts.extensions.get::>().map( |cu| AuthUser { - id: cu.id.clone(), + id: cu.id, username: cu.username.clone(), role: cu.role.clone(), }, @@ -216,8 +217,11 @@ pub async fn auth_middleware( "Token validated successfully for user: {}", claims.username ); + let user_id = Uuid::parse_str(&claims.sub).map_err(|_| { + AuthError::InvalidToken("Invalid user ID in token".to_string()) + })?; let current_user = Arc::new(CurrentUser { - id: claims.sub, + id: user_id, username: claims.username, email: claims.email, role: claims.role, @@ -303,8 +307,11 @@ pub async fn auth_middleware( match token_service.validate_token(&token_str) { Ok(claims) => { tracing::debug!("Cookie token validated for user: {}", claims.username); + let user_id = Uuid::parse_str(&claims.sub).map_err(|_| { + AuthError::InvalidToken("Invalid user ID in token".to_string()) + })?; let current_user = Arc::new(CurrentUser { - id: claims.sub, + id: user_id, username: claims.username, email: claims.email, role: claims.role, diff --git a/src/interfaces/nextcloud/login_v2_handler.rs b/src/interfaces/nextcloud/login_v2_handler.rs index 7b247293..4bc4ef4b 100755 --- a/src/interfaces/nextcloud/login_v2_handler.rs +++ b/src/interfaces/nextcloud/login_v2_handler.rs @@ -174,7 +174,7 @@ pub async fn handle_login_submit( let app_password = match nextcloud .app_passwords - .create_nc(¤t_user.id, "Nextcloud") + .create_nc(current_user.id, "Nextcloud") .await { Ok((_id, password)) => password, diff --git a/src/interfaces/nextcloud/ocs_handler.rs b/src/interfaces/nextcloud/ocs_handler.rs index 8b5a28c8..29bb7b1a 100755 --- a/src/interfaces/nextcloud/ocs_handler.rs +++ b/src/interfaces/nextcloud/ocs_handler.rs @@ -47,7 +47,7 @@ pub async fn handle_capabilities_v2(State(state): State>) -> Respo pub async fn handle_user_info(State(state): State>, user: CurrentUser) -> Response { let quota: (i64, i64) = match state.storage_usage_service.as_ref() { - Some(service) => match service.get_user_storage_info(&user.id).await { + Some(service) => match service.get_user_storage_info(user.id).await { Ok((used, total)) => (used, total), Err(_) => (0, 0), }, @@ -151,7 +151,7 @@ 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(&user_dto.id).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), }, @@ -212,7 +212,7 @@ pub async fn handle_revoke_apppassword( if let Err(e) = nextcloud .app_passwords - .revoke_by_password(&user.id, &app_password) + .revoke_by_password(user.id, &app_password) .await { tracing::warn!("Failed to revoke app password for {}: {}", user.id, e); @@ -367,7 +367,7 @@ pub async fn handle_search( ..SearchCriteriaDto::default() }; - let results = match search_service.search(criteria, &user.id).await { + let results = match search_service.search(criteria, user.id).await { Ok(r) => r, Err(_) => return empty_search_response().into_response(), }; diff --git a/src/interfaces/nextcloud/preview_handler.rs b/src/interfaces/nextcloud/preview_handler.rs index 5bdebb9f..f457dc35 100755 --- a/src/interfaces/nextcloud/preview_handler.rs +++ b/src/interfaces/nextcloud/preview_handler.rs @@ -90,7 +90,8 @@ pub async fn handle_preview( }; // Verify the authenticated user owns this file - if file.owner_id.as_deref() != Some(&user.id) { + let user_id_str = user.id.to_string(); + if file.owner_id.as_deref() != Some(user_id_str.as_str()) { return Response::builder() .status(StatusCode::NOT_FOUND) .body(Body::from("File not found")) diff --git a/src/interfaces/nextcloud/report_handler.rs b/src/interfaces/nextcloud/report_handler.rs index 106dbc75..7bbd28ec 100755 --- a/src/interfaces/nextcloud/report_handler.rs +++ b/src/interfaces/nextcloud/report_handler.rs @@ -67,7 +67,7 @@ async fn handle_filter_files( }; let favorites = fav_svc - .get_favorites(&user.id) + .get_favorites(user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to get favorites: {}", e)))?; @@ -179,7 +179,7 @@ async fn handle_search( }; let results = search_svc - .search(criteria, &user.id) + .search(criteria, user.id) .await .map_err(|e| AppError::internal_error(format!("Search failed: {}", e)))?; diff --git a/src/interfaces/nextcloud/trashbin_handler.rs b/src/interfaces/nextcloud/trashbin_handler.rs index 52143074..3891ef07 100755 --- a/src/interfaces/nextcloud/trashbin_handler.rs +++ b/src/interfaces/nextcloud/trashbin_handler.rs @@ -75,7 +75,7 @@ async fn handle_propfind( .ok_or_else(|| AppError::internal_error("Trash service not available"))?; let items = trash_svc - .get_trash_items(&user.id) + .get_trash_items(user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to list trash: {}", e)))?; @@ -109,7 +109,7 @@ async fn handle_restore( .ok_or_else(|| AppError::internal_error("Trash service not available"))?; trash_svc - .restore_item(&id, &user.id) + .restore_item(&id, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to restore item: {}", e)))?; @@ -131,7 +131,7 @@ async fn handle_empty_trash( .ok_or_else(|| AppError::internal_error("Trash service not available"))?; trash_svc - .empty_trash(&user.id) + .empty_trash(user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to empty trash: {}", e)))?; @@ -156,7 +156,7 @@ async fn handle_delete_permanent( .ok_or_else(|| AppError::internal_error("Trash service not available"))?; trash_svc - .delete_permanently(&id, &user.id) + .delete_permanently(&id, user.id) .await .map_err(|e| { AppError::internal_error(format!("Failed to permanently delete item: {}", e)) diff --git a/src/interfaces/nextcloud/webdav_handler.rs b/src/interfaces/nextcloud/webdav_handler.rs index ccf66737..3ab4c02d 100755 --- a/src/interfaces/nextcloud/webdav_handler.rs +++ b/src/interfaces/nextcloud/webdav_handler.rs @@ -193,7 +193,7 @@ async fn handle_propfind( items.push((&sf.id, "folder")); } fav_svc - .batch_check_favorites(&user.id, &items) + .batch_check_favorites(user.id, &items) .await .unwrap_or_default() } else { @@ -234,7 +234,7 @@ async fn handle_propfind( let favorite_ids = if let Some(fav_svc) = state.favorites_service.as_ref() { let items: Vec<(&str, &str)> = vec![(&file.id, "file")]; fav_svc - .batch_check_favorites(&user.id, &items) + .batch_check_favorites(user.id, &items) .await .unwrap_or_default() } else { @@ -414,14 +414,14 @@ async fn handle_proppatch( if let Some(fav_svc) = state.favorites_service.as_ref() { if value == 1 { fav_svc - .add_to_favorites(&user.id, &item_id, item_type) + .add_to_favorites(user.id, &item_id, item_type) .await .map_err(|e| { AppError::internal_error(format!("Failed to add favorite: {}", e)) })?; } else { fav_svc - .remove_from_favorites(&user.id, &item_id, item_type) + .remove_from_favorites(user.id, &item_id, item_type) .await .map_err(|e| { AppError::internal_error(format!("Failed to remove favorite: {}", e)) @@ -685,7 +685,7 @@ async fn handle_delete( if let Some(trash_svc) = state.trash_service.as_ref() { if let Ok(folder) = folder_service.get_folder_by_path(&internal_path).await { trash_svc - .move_to_trash(&folder.id, "folder", &user.id) + .move_to_trash(&folder.id, "folder", user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to trash folder: {}", e)))?; return Ok(Response::builder() @@ -695,7 +695,7 @@ async fn handle_delete( } if let Ok(file) = file_service.get_file_by_path(&internal_path).await { trash_svc - .move_to_trash(&file.id, "file", &user.id) + .move_to_trash(&file.id, "file", user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to trash file: {}", e)))?; return Ok(Response::builder() @@ -711,7 +711,7 @@ async fn handle_delete( if let Ok(folder) = folder_service.get_folder_by_path(&internal_path).await { folder_service - .delete_folder(&folder.id, &user.id) + .delete_folder(&folder.id, user.id) .await .map_err(|e| AppError::internal_error(format!("Failed to delete folder: {}", e)))?; @@ -835,7 +835,7 @@ async fn handle_move( RenameFolderDto { name: dest_name.to_string(), }, - &user.id, + user.id, ) .await .map_err(|e| AppError::internal_error(format!("Rename failed: {}", e)))?; @@ -853,7 +853,7 @@ async fn handle_move( MoveFolderDto { parent_id: Some(dest_parent.id.clone()), }, - &user.id, + user.id, ) .await .map_err(|e| AppError::internal_error(format!("Move failed: {}", e)))?; @@ -867,7 +867,7 @@ async fn handle_move( RenameFolderDto { name: dest_name.to_string(), }, - &user.id, + user.id, ) .await .map_err(|e| AppError::internal_error(format!("Rename failed: {}", e)))?;