perf: migrate all user/session/auth IDs from VARCHAR(36) to native UUID

- Schema: all ~15 VARCHAR(36) columns → UUID with DEFAULT gen_random_uuid()
- Domain entities: User, Session, DeviceCode, AppPassword, Share → id: Uuid
- DTOs: CurrentUser.id → Uuid (API boundary DTOs keep String for JSON)
- Auth middleware: parse JWT claims.sub (String) → Uuid at boundary
- All repository traits, port traits, service impls updated end-to-end
- Handlers: pass Uuid by value (Copy, 16 bytes) instead of String refs
- Settings chain: updated_by column → Uuid (was text, caused setup crash)
- Removed ~650 lines of String↔Uuid conversion boilerplate
- Eliminates per-request heap allocations for ID cloning
- 16-byte binary comparison vs 36-byte string comparison in all queries
- Native UUID indexing in PostgreSQL (btree on 16 bytes vs 36-char text)

85 files changed, 1090 insertions(+), 1739 deletions(-)
This commit is contained in:
Diocrafts
2026-03-07 14:59:32 +01:00
parent 9f08460027
commit 06ed0455ce
85 changed files with 1090 additions and 1739 deletions
+2 -2
View File
@@ -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
+18 -18
View File
@@ -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
);
-735
View File
@@ -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<CurrentUser>` 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<Arc<CurrentUser>>
```
**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<AppConfig>`.
**Fix:** Pass `Arc<AppConfig>` everywhere. Most services already take
`Arc<AppConfig>`; 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<str>` 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<Arc<str>> = 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<str> per item
```
`Arc<str>` 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<String>` 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<String>, // heap alloc
pub message: String, // heap alloc
pub source: Option<Box<dyn StdError + Send + Sync>>, // 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<str>` 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<String, Vec<FileDto>> =
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<Body>) -> 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<Body>, 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<String, CachedValidation>,
```
`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<HashMap<...>>` 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<T>`) 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<String, String>, // file_id → blob_hash
```
Both file IDs and blob hashes are UUIDs/hex strings (~36 bytes). Using
`Arc<str>` 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<T>`.
---
## 11. Dynamic dispatch in hot paths
### 🟡 11a. `Arc<dyn FileUseCaseFactory>` in `ApplicationServices`
**File:** `src/common/di.rs`
```rust
pub struct ApplicationServices {
pub file_use_case_factory: Arc<dyn FileUseCaseFactory>,
...
}
```
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<ConcreteFileUseCaseFactory>` would allow the compiler to
devirtualise and inline.
---
### 🟡 11b. `Box<dyn StdError + Send + Sync>` in every `DomainError`
**File:** `src/domain/errors.rs`
```rust
pub source: Option<Box<dyn StdError + Send + Sync>>,
```
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<AppConfig>`.
---
## 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<str>` → `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<dyn Error>` 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<CurrentUser>`** (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<HashMap>` 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<str>`** 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
+2 -1
View File
@@ -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,
+19 -18
View File
@@ -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<User, DomainError>;
/// Gets a user by ID
async fn get_user_by_id(&self, id: &str) -> Result<User, DomainError>;
async fn get_user_by_id(&self, id: Uuid) -> Result<User, DomainError>;
/// Gets a user by username
async fn get_user_by_username(&self, username: &str) -> Result<User, DomainError>;
@@ -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<Vec<User>, 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<User, DomainError>;
/// 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<Session, DomainError>;
/// 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<u64, DomainError>;
async fn revoke_all_user_sessions(&self, user_id: Uuid) -> Result<u64, DomainError>;
}
// ============================================================================
@@ -224,10 +225,10 @@ pub trait DeviceCodeStoragePort: Send + Sync + 'static {
async fn delete_expired(&self) -> Result<u64, DomainError>;
/// List authorized device codes for a user (for UI management)
async fn list_by_user(&self, user_id: &str) -> Result<Vec<DeviceCode>, DomainError>;
async fn list_by_user(&self, user_id: Uuid) -> Result<Vec<DeviceCode>, 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<AppPassword, DomainError>;
/// Get all active (non-expired) app passwords for a user.
async fn list_by_user(&self, user_id: &str) -> Result<Vec<AppPassword>, DomainError>;
async fn list_by_user(&self, user_id: Uuid) -> Result<Vec<AppPassword>, DomainError>;
/// Get a specific app password by ID.
async fn get_by_id(&self, id: &str) -> Result<AppPassword, DomainError>;
async fn get_by_id(&self, id: Uuid) -> Result<AppPassword, DomainError>;
/// 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<Vec<AppPassword>, DomainError>;
async fn get_active_by_user_id(&self, user_id: Uuid) -> Result<Vec<AppPassword>, 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<Vec<AppPassword>, 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<bool, DomainError>;
async fn delete_by_user_and_id(&self, id: Uuid, user_id: Uuid) -> Result<bool, DomainError>;
/// Hard-delete expired/revoked app passwords (cleanup).
async fn delete_expired(&self) -> Result<u64, DomainError>;
+25 -24
View File
@@ -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<CalendarDto, DomainError>;
async fn update_calendar(
&self,
@@ -22,11 +23,11 @@ pub trait CalendarStoragePort: Send + Sync + 'static {
async fn get_calendar(&self, calendar_id: &str) -> Result<CalendarDto, DomainError>;
async fn list_calendars_by_owner(
&self,
owner_id: &str,
owner_id: Uuid,
) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_calendars_shared_with_user(
&self,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<CalendarDto>, 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<bool, DomainError>;
// 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<CalendarDto, DomainError>;
async fn update_calendar(
&self,
calendar_id: &str,
update: UpdateCalendarDto,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarDto, DomainError>;
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<CalendarDto, DomainError>;
async fn list_my_calendars(&self, user_id: &str) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_shared_calendars(&self, user_id: &str) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_my_calendars(&self, user_id: Uuid) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_shared_calendars(&self, user_id: Uuid) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_public_calendars(
&self,
limit: Option<i64>,
@@ -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<Vec<(String, String)>, DomainError>;
// Event operations
async fn create_event(
&self,
event: CreateEventDto,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarEventDto, DomainError>;
async fn create_event_from_ical(
&self,
event: CreateEventICalDto,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarEventDto, DomainError>;
async fn update_event(
&self,
event_id: &str,
update: UpdateEventDto,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarEventDto, DomainError>;
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<CalendarEventDto, DomainError>;
async fn list_events(
&self,
calendar_id: &str,
limit: Option<i64>,
offset: Option<i64>,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<CalendarEventDto>, DomainError>;
async fn get_events_in_range(
&self,
calendar_id: &str,
start: DateTime<Utc>,
end: DateTime<Utc>,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<CalendarEventDto>, DomainError>;
}
+20 -19
View File
@@ -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<AddressBookDto, DomainError>;
async fn list_user_address_books(
&self,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<AddressBookDto>, DomainError>;
async fn list_public_address_books(&self) -> Result<Vec<AddressBookDto>, 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<Vec<(String, bool)>, DomainError>;
}
@@ -67,19 +68,19 @@ pub trait ContactUseCase: Send + Sync + 'static {
contact_id: &str,
update: UpdateContactDto,
) -> Result<ContactDto, DomainError>;
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<ContactDto, DomainError>;
async fn list_contacts(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactDto>, DomainError>;
async fn search_contacts(
&self,
address_book_id: &str,
query: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactDto>, DomainError>;
// Contact Group operations
@@ -92,49 +93,49 @@ pub trait ContactUseCase: Send + Sync + 'static {
group_id: &str,
update: UpdateContactGroupDto,
) -> Result<ContactGroupDto, DomainError>;
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<ContactGroupDto, DomainError>;
async fn list_groups(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactGroupDto>, 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<Vec<ContactDto>, DomainError>;
async fn list_groups_for_contact(
&self,
contact_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactGroupDto>, DomainError>;
// vCard operations
async fn get_contact_vcard(
&self,
contact_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<String, DomainError>;
async fn get_contacts_as_vcards(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<(String, String)>, DomainError>;
}
@@ -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<String>,
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<String>,
@@ -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<UploadStatusResponseDto, DomainError>;
/// 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>, 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;
+14 -12
View File
@@ -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<Vec<FavoriteItemDto>>;
async fn get_favorites(&self, user_id: Uuid) -> Result<Vec<FavoriteItemDto>>;
/// 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<bool>;
/// Check if an item is in user's favorites
async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
async fn is_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<bool>;
/// 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<BatchFavoritesResult>;
@@ -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<HashSet<String>>;
}
@@ -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<Vec<FavoriteItemDto>>;
async fn get_favorites(&self, user_id: Uuid) -> Result<Vec<FavoriteItemDto>>;
/// 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<bool>;
async fn remove_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<bool>;
/// Checks if an item is in favorites.
async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
async fn is_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<bool>;
/// 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<u64>;
async fn add_favorites_batch(&self, user_id: Uuid, items: &[(String, String)]) -> Result<u64>;
/// 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<HashSet<String>>;
}
+14 -12
View File
@@ -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<FileDto, DomainError>;
async fn get_file_owned(&self, id: &str, caller_id: Uuid) -> Result<FileDto, DomainError>;
/// Gets a file by its path (for WebDAV)
async fn get_file_by_path(&self, path: &str) -> Result<FileDto, DomainError>;
@@ -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<Vec<FileDto>, 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<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + 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<u64>,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + 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<Vec<FileDto>, 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<String>,
) -> Result<FileDto, DomainError>;
@@ -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<String>,
) -> Result<FileDto, DomainError>;
@@ -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<FileDto, DomainError>;
@@ -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<bool, DomainError>;
async fn delete_with_cleanup(&self, id: &str, user_id: Uuid) -> Result<bool, DomainError>;
/// Copies an entire folder subtree atomically (WebDAV COPY Depth: infinity).
///
+10 -8
View File
@@ -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<FolderDto, DomainError>;
async fn get_folder_owned(&self, id: &str, caller_id: Uuid) -> Result<FolderDto, DomainError>;
/// Gets a folder by its path
async fn get_folder_by_path(&self, path: &str) -> Result<FolderDto, DomainError>;
@@ -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<Vec<FolderDto>, 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<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>;
@@ -56,7 +58,7 @@ pub trait FolderUseCase: Send + Sync + 'static {
&self,
id: &str,
dto: RenameFolderDto,
caller_id: &str,
caller_id: Uuid,
) -> Result<FolderDto, DomainError>;
/// 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<FolderDto, DomainError>;
/// 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<FolderDto, DomainError>;
@@ -103,7 +105,7 @@ pub trait SearchUseCase: Send + Sync + 'static {
async fn search(
&self,
criteria: SearchCriteriaDto,
user_id: &str,
user_id: Uuid,
) -> Result<Arc<SearchResultsDto>, DomainError>;
/// Returns quick suggestions for autocomplete (lightweight, fast).
+11 -9
View File
@@ -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<i32>,
) -> Result<Vec<RecentItemDto>>;
/// 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<bool>;
/// 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<Vec<RecentItemDto>>;
async fn get_recent_items(&self, user_id: Uuid, limit: i32) -> Result<Vec<RecentItemDto>>;
/// 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<bool>;
async fn remove_item(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<bool>;
/// 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<()>;
}
+14 -12
View File
@@ -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<ShareDto, DomainError>;
/// Get a shared link by its ID (ownership-verified)
async fn get_shared_link(&self, id: &str, requester_id: &str) -> Result<ShareDto, DomainError>;
async fn get_shared_link(&self, id: Uuid, requester_id: Uuid) -> Result<ShareDto, DomainError>;
/// Get a shared link by its token (for access by non-users)
async fn get_shared_link_by_token(&self, token: &str) -> Result<ShareDto, DomainError>;
@@ -26,24 +28,24 @@ pub trait ShareUseCase: Send + Sync + 'static {
&self,
item_id: &str,
item_type: &ShareItemType,
requester_id: &str,
requester_id: Uuid,
) -> Result<Vec<ShareDto>, 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<ShareDto, DomainError>;
/// 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<PaginatedResponseDto<ShareDto>, 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<crate::domain::entities::share::Share, DomainError>;
/// 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<Vec<crate::domain::entities::share::Share>, 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<crate::domain::entities::share::Share>, usize), DomainError>;
+15 -12
View File
@@ -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<File, DomainError>;
async fn get_file_for_owner(&self, id: &str, owner_id: Uuid) -> Result<File, DomainError>;
/// 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<Vec<File>, 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<Vec<File>, 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<File>, 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<File>, 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<usize, DomainError>;
/// 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<i64, DomainError>;
async fn update_user_storage_usage(&self, user_id: Uuid) -> Result<i64, DomainError>;
/// 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
+7 -5
View File
@@ -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<Vec<TrashedItemDto>>;
async fn get_trash_items(&self, user_id: Uuid) -> Result<Vec<TrashedItemDto>>;
/// 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<()>;
}
@@ -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<bool, DomainError> {
pub async fn try_claim_initialization(&self, admin_user_id: Uuid) -> Result<bool, DomainError> {
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(
@@ -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<AppPasswordCreatedResponseDto, DomainError> {
// 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<AppPasswordListResponseDto, DomainError> {
pub async fn list(&self, user_id: Uuid) -> Result<AppPasswordListResponseDto, DomainError> {
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<AppPasswordRevokeResponseDto, DomainError> {
// 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<Vec<AppPassword>, DomainError> {
pub async fn list_nc(&self, user_id: Uuid) -> Result<Vec<AppPassword>, 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(
@@ -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<u64, DomainError> {
pub async fn logout_all(&self, user_id: Uuid) -> Result<u64, DomainError> {
// 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<UserDto, DomainError> {
pub async fn get_user(&self, user_id: Uuid) -> Result<UserDto, DomainError> {
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<UserDto, DomainError> {
pub async fn get_user_by_id(&self, user_id: Uuid) -> Result<UserDto, DomainError> {
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<UserDto, DomainError> {
pub async fn get_user_admin(&self, user_id: Uuid) -> Result<UserDto, DomainError> {
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<bool, DomainError> {
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
+32 -58
View File
@@ -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<String>,
target_folder_id: Option<String>,
caller_id: &str,
user_id: Uuid,
) -> Result<BatchResult<FileDto>, BatchOperationError> {
info!("Starting batch copy of {} files", file_ids.len());
let start_time = std::time::Instant::now();
@@ -135,17 +136,15 @@ impl BatchOperationService {
// Arc<str> avoids N heap-clones of the same string
let target_folder: Option<Arc<str>> = target_folder_id.map(|s| Arc::from(s.as_str()));
let caller: Arc<str> = 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<String>,
target_folder_id: Option<String>,
caller_id: &str,
user_id: Uuid,
) -> Result<BatchResult<FileDto>, 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<Arc<str>> = target_folder_id.map(|s| Arc::from(s.as_str()));
let caller: Arc<str> = 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<String>,
caller_id: &str,
user_id: Uuid,
) -> Result<BatchResult<String>, 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<str> = 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<String>,
caller_id: &str,
user_id: Uuid,
) -> Result<BatchResult<FileDto>, 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<str> = 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<String>,
_recursive: bool,
caller_id: &str,
user_id: Uuid,
) -> Result<BatchResult<String>, 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<str> avoids N heap-clones of the caller string
let caller: Arc<str> = 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<String>,
user_id: &str,
user_id: Uuid,
) -> Result<BatchResult<String>, BatchOperationError> {
let trash_service = self
.trash_service
@@ -464,14 +448,14 @@ impl BatchOperationService {
},
};
let uid: Arc<str> = 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<String>,
user_id: &str,
user_id: Uuid,
) -> Result<BatchResult<String>, BatchOperationError> {
let trash_service = self
.trash_service
@@ -529,14 +513,14 @@ impl BatchOperationService {
},
};
let uid: Arc<str> = 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<String>,
target_folder_id: Option<String>,
caller_id: &str,
user_id: Uuid,
) -> Result<BatchResult<FolderDto>, 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<Arc<str>> = target_folder_id.map(|s| Arc::from(s.as_str()));
let caller: Arc<str> = 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<String>,
folder_ids: Vec<String>,
caller_id: &str,
user_id: Uuid,
) -> Result<NamedTempFile, BatchOperationError> {
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<tokio_util::compat::Compat<BufWriter<tokio::fs::File>>>,
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<tokio_util::compat::Compat<BufWriter<tokio::fs::File>>>,
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<String>)>, // (name, parent_id)
caller_id: &str,
user_id: Uuid,
) -> Result<BatchResult<FolderDto>, 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<str> = 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<String>,
caller_id: &str,
user_id: Uuid,
) -> Result<BatchResult<FolderDto>, 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<str> = 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)
}
}))
+22 -21
View File
@@ -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<CalendarDto, DomainError> {
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<CalendarDto, DomainError> {
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<CalendarDto, DomainError> {
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<Vec<CalendarDto>, DomainError> {
async fn list_my_calendars(&self, user_id: Uuid) -> Result<Vec<CalendarDto>, DomainError> {
self.calendar_storage.list_calendars_by_owner(user_id).await
}
async fn list_shared_calendars(&self, user_id: &str) -> Result<Vec<CalendarDto>, DomainError> {
async fn list_shared_calendars(&self, user_id: Uuid) -> Result<Vec<CalendarDto>, 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<Vec<(String, String)>, 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<CalendarEventDto, DomainError> {
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<CalendarEventDto, DomainError> {
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<CalendarEventDto, DomainError> {
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<CalendarEventDto, DomainError> {
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<i64>,
offset: Option<i64>,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<CalendarEventDto>, DomainError> {
let has_access = self
.calendar_storage
@@ -313,7 +314,7 @@ impl CalendarUseCase for CalendarService {
calendar_id: &str,
start: DateTime<Utc>,
end: DateTime<Utc>,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<CalendarEventDto>, DomainError> {
let has_access = self
.calendar_storage
+103 -54
View File
@@ -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<AddressBook, DomainError> {
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<AddressBook, DomainError> {
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<AddressBookDto, DomainError> {
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<Vec<AddressBookDto>, 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<Vec<(String, bool)>, 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<ContactDto, DomainError> {
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<Vec<ContactDto>, 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<Vec<ContactDto>, 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<ContactGroupDto, DomainError> {
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<Vec<ContactGroupDto>, 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<Vec<ContactDto>, 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<Vec<ContactGroupDto>, 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<String, DomainError> {
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<Vec<(String, String)>, 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)
@@ -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<Vec<DeviceInfoDto>, 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);
@@ -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<Vec<FavoriteItemDto>> {
async fn get_favorites(&self, user_id: Uuid) -> Result<Vec<FavoriteItemDto>> {
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<bool> {
@@ -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<bool> {
async fn is_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<bool> {
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<BatchFavoritesResult> {
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<HashSet<String>> {
self.repo.batch_check_favorites(user_id, item_ids).await
@@ -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<String>,
) -> Result<FileDto, DomainError> {
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<String>,
) -> Result<FileDto, DomainError> {
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<FileDto, DomainError> {
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<bool, DomainError> {
async fn delete_with_cleanup(&self, id: &str, user_id: Uuid) -> Result<bool, DomainError> {
// 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);
@@ -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<FileDto, DomainError> {
async fn get_file_owned(&self, id: &str, caller_id: Uuid) -> Result<FileDto, DomainError> {
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<Vec<FileDto>, 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<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + 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<u64>,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + 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<Vec<FileDto>, DomainError> {
+31 -23
View File
@@ -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<FolderDto, DomainError> {
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<Vec<FolderDto>, 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<FolderDto>,
@@ -100,7 +101,7 @@ impl FolderService {
&self,
_id: &str,
_dto: RenameFolderDto,
_caller_id: &str,
_caller_id: Uuid,
) -> Result<FolderDto, DomainError> {
Ok(FolderDto::empty())
}
@@ -109,18 +110,18 @@ impl FolderService {
&self,
_id: &str,
_dto: MoveFolderDto,
_caller_id: &str,
_caller_id: Uuid,
) -> Result<FolderDto, DomainError> {
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<FolderDto, DomainError> {
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<FolderDto, DomainError> {
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<FolderDto, DomainError> {
async fn get_folder_owned(&self, id: &str, caller_id: Uuid) -> Result<FolderDto, DomainError> {
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<Vec<FolderDto>, 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<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, 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<FolderDto, DomainError> {
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<FolderDto, DomainError> {
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,
+5 -4
View File
@@ -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<i32>,
) -> Result<Vec<RecentItemDto>> {
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<bool> {
@@ -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);
+7 -5
View File
@@ -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<Arc<SearchResultsDto>> {
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<Arc<SearchResultsDto>> {
Ok(Arc::new(SearchResultsDto::empty()))
}
+33 -30
View File
@@ -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<Share, DomainError> {
async fn fetch_owned_share(&self, id: Uuid, requester_id: Uuid) -> Result<Share, DomainError> {
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<ShareDto, DomainError> {
// 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<ShareDto, DomainError> {
async fn get_shared_link(&self, id: Uuid, requester_id: Uuid) -> Result<ShareDto, DomainError> {
// 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<Vec<ShareDto>, 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<ShareDto, DomainError> {
// 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<PaginatedResponseDto<ShareDto>, DomainError> {
@@ -519,7 +520,7 @@ mod tests {
{
async fn create_shared_link(
&self,
user_id: &str,
user_id: Uuid,
dto: CreateShareDto,
) -> Result<ShareDto, DomainError> {
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<ShareDto, DomainError> {
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<Vec<ShareDto>, 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<ShareDto, DomainError> {
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<PaginatedResponseDto<ShareDto>, 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<Share, DomainError> {
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<Vec<Share>, 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<Share>, 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();
@@ -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<i64, DomainError> {
pub async fn update_user_storage_usage(&self, user_id: Uuid) -> Result<i64, DomainError> {
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<i64, DomainError> {
async fn calculate_user_storage_usage(&self, user_id: Uuid) -> Result<i64, DomainError> {
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<i64, DomainError> {
async fn update_user_storage_usage(&self, user_id: Uuid) -> Result<i64, DomainError> {
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()))
}
+11 -54
View File
@@ -123,13 +123,10 @@ impl TrashService {
impl TrashUseCase for TrashService {
#[instrument(skip(self))]
async fn get_trash_items(&self, user_id: &str) -> Result<Vec<TrashedItemDto>> {
async fn get_trash_items(&self, user_id: Uuid) -> Result<Vec<TrashedItemDto>> {
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(())
+22 -21
View File
@@ -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<File>, 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<usize, DomainError> {
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<File, DomainError> {
async fn get_file_for_owner(&self, _id: &str, _owner_id: Uuid) -> Result<File, DomainError> {
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<FolderDto, DomainError> {
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<Vec<FolderDto>, 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<PaginatedResponseDto<FolderDto>, 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<FolderDto, DomainError> {
Ok(FolderDto::default())
}
@@ -413,18 +414,18 @@ impl FolderUseCase for StubFolderUseCase {
&self,
_id: &str,
_dto: MoveFolderDto,
_caller_id: &str,
_caller_id: Uuid,
) -> Result<FolderDto, DomainError> {
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<FolderDto, DomainError> {
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<Vec<FileDto>, 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<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
let empty_stream = futures::stream::empty::<Result<Bytes, std::io::Error>>();
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<FileDto, DomainError> {
async fn get_file_owned(&self, _id: &str, _caller_id: Uuid) -> Result<FileDto, DomainError> {
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<u64>,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + 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<String>,
) -> Result<FileDto, DomainError> {
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<bool, DomainError> {
async fn delete_with_cleanup(&self, _id: &str, _user_id: Uuid) -> Result<bool, DomainError> {
Ok(false)
}
async fn move_file_owned(
&self,
_file_id: &str,
_caller_id: &str,
_caller_id: Uuid,
_folder_id: Option<String>,
) -> Result<FileDto, DomainError> {
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<FileDto, DomainError> {
Ok(FileDto::default())
@@ -679,7 +680,7 @@ impl SearchUseCase for StubSearchUseCase {
async fn search(
&self,
_criteria: SearchCriteriaDto,
_user_id: &str,
_user_id: Uuid,
) -> Result<Arc<SearchResultsDto>, DomainError> {
Ok(Arc::new(SearchResultsDto::empty()))
}
+4 -4
View File
@@ -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<DateTime<Utc>>,
) -> Self {
Self {
id: Uuid::new_v4().to_string(),
id: Uuid::new_v4(),
user_id,
label,
password_hash,
+10 -10
View File
@@ -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<String>,
user_id: Option<Uuid>,
access_token: Option<String>,
refresh_token: Option<String>,
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<String>,
user_id: Option<Uuid>,
access_token: Option<String>,
refresh_token: Option<String>,
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<Uuid> {
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);
+10 -13
View File
@@ -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<Utc>,
ip_address: Option<String>,
@@ -15,22 +15,19 @@ pub struct Session {
impl Session {
pub fn new(
user_id: String,
user_id: Uuid,
refresh_token: String,
ip_address: Option<String>,
user_agent: Option<String>,
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<Utc>,
ip_address: Option<String>,
@@ -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 {
+21 -16
View File
@@ -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<String>,
item_type: ShareItemType,
@@ -15,7 +15,7 @@ pub struct Share {
expires_at: Option<u64>,
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<String>,
item_type: ShareItemType,
created_by: String,
created_by: Uuid,
permissions: Option<SharePermissions>,
password_hash: Option<String>,
expires_at: Option<u64>,
@@ -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<String>,
item_type: ShareItemType,
@@ -98,7 +98,7 @@ impl Share {
expires_at: Option<u64>,
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,
+7 -7
View File
@@ -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 {
@@ -22,23 +22,23 @@ pub trait AddressBookRepository: Send + Sync + 'static {
) -> AddressBookRepositoryResult<Option<AddressBook>>;
async fn get_address_books_by_owner(
&self,
owner_id: &str,
owner_id: Uuid,
) -> AddressBookRepositoryResult<Vec<AddressBook>>;
async fn get_shared_address_books(
&self,
user_id: &str,
user_id: Uuid,
) -> AddressBookRepositoryResult<Vec<AddressBook>>;
async fn get_public_address_books(&self) -> AddressBookRepositoryResult<Vec<AddressBook>>;
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,
@@ -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<Vec<Calendar>>;
/// 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<Calendar>;
/// Lists calendars shared with a specific user
async fn list_calendars_shared_with_user(
&self,
user_id: &str,
user_id: Uuid,
) -> CalendarRepositoryResult<Vec<Calendar>>;
/// 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<bool>;
/// 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)
@@ -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<Session>;
/// Gets a session by ID
async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult<Session>;
async fn get_session_by_id(&self, id: Uuid) -> SessionRepositoryResult<Session>;
/// Gets a session by refresh token
async fn get_session_by_refresh_token(
@@ -42,14 +43,14 @@ pub trait SessionRepository: Send + Sync + 'static {
) -> SessionRepositoryResult<Session>;
/// 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<Vec<Session>>;
/// 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<u64>;
async fn revoke_all_user_sessions(&self, user_id: Uuid) -> SessionRepositoryResult<u64>;
/// Deletes expired sessions
async fn delete_expired_sessions(&self) -> SessionRepositoryResult<u64>;
@@ -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<Uuid>,
) -> 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<bool, DomainError> {
async fn try_claim_initialization(&self, admin_user_id: Uuid) -> Result<bool, DomainError> {
// Default: non-atomic fallback (overridden by PG implementation)
match self.get("system_initialized").await? {
Some(v) if v == "true" => Ok(false),
+4 -3
View File
@@ -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<Share, ShareRepositoryError>;
/// Find a share by its ID
async fn find_by_id(&self, id: &str) -> Result<Share, ShareRepositoryError>;
async fn find_by_id(&self, id: Uuid) -> Result<Share, ShareRepositoryError>;
/// Find a share by its token
async fn find_by_token(&self, token: &str) -> Result<Share, ShareRepositoryError>;
@@ -37,10 +38,10 @@ pub trait ShareRepository: Send + Sync + 'static {
) -> Result<Vec<Share>, 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<Vec<Share>, ShareRepositoryError>;
async fn find_by_user(&self, user_id: Uuid) -> Result<Vec<Share>, ShareRepositoryError>;
/// Find all shares (admin operation)
async fn find_all(&self) -> Result<Vec<Share>, ShareRepositoryError>;
+9 -8
View File
@@ -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<User>;
/// Gets a user by ID
async fn get_user_by_id(&self, id: &str) -> UserRepositoryResult<User>;
async fn get_user_by_id(&self, id: Uuid) -> UserRepositoryResult<User>;
/// Gets a user by username
async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult<User>;
@@ -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<Vec<User>>;
@@ -73,21 +74,21 @@ pub trait UserRepository: Send + Sync + 'static {
async fn search_users(&self, query: &str, limit: i64) -> UserRepositoryResult<Vec<User>>;
/// 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<Vec<User>>;
/// 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<()>;
@@ -47,7 +47,7 @@ impl CalendarStoragePort for CalendarStorageAdapter {
async fn create_calendar(
&self,
dto: CreateCalendarDto,
owner_id: &str,
owner_id: Uuid,
) -> Result<CalendarDto, DomainError> {
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<Vec<CalendarDto>, 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<Vec<CalendarDto>, 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<bool, DomainError> {
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(
@@ -61,7 +61,7 @@ impl ContactStorageAdapter {
async fn check_address_book_access(
&self,
address_book_id: &Uuid,
user_id: &str,
user_id: Uuid,
) -> Result<AddressBook, DomainError> {
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<AddressBook, DomainError> {
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<AddressBookDto, DomainError> {
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<Vec<AddressBookDto>, 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<Vec<(String, bool)>, 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<ContactDto, DomainError> {
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<Vec<ContactDto>, 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<Vec<ContactDto>, 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<ContactGroupDto, DomainError> {
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<Vec<ContactGroupDto>, 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<Vec<ContactDto>, 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<Vec<ContactGroupDto>, 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<String, DomainError> {
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<Vec<(String, String)>, DomainError> {
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
@@ -144,7 +144,7 @@ impl AddressBookRepository for AddressBookPgRepository {
async fn get_address_books_by_owner(
&self,
owner_id: &str,
owner_id: Uuid,
) -> AddressBookRepositoryResult<Vec<AddressBook>> {
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<Vec<AddressBook>> {
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#"
@@ -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<PgPool>,
@@ -48,7 +49,7 @@ impl AppPasswordStoragePort for AppPasswordPgRepository {
Ok(ap)
}
async fn list_by_user(&self, user_id: &str) -> Result<Vec<AppPassword>, DomainError> {
async fn list_by_user(&self, user_id: Uuid) -> Result<Vec<AppPassword>, 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<AppPassword, DomainError> {
async fn get_by_id(&self, id: Uuid) -> Result<AppPassword, DomainError> {
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<Vec<AppPassword>, DomainError> {
async fn get_active_by_user_id(&self, user_id: Uuid) -> Result<Vec<AppPassword>, 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<Vec<AppPassword>, 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<bool, DomainError> {
async fn delete_by_user_and_id(&self, id: Uuid, user_id: Uuid) -> Result<bool, DomainError> {
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,
@@ -140,7 +140,7 @@ impl CalendarRepository for CalendarPgRepository {
async fn list_calendars_by_owner(
&self,
owner_id: &str,
owner_id: Uuid,
) -> CalendarRepositoryResult<Vec<Calendar>> {
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<Calendar> {
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<Vec<Calendar>> {
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<bool> {
// 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#"
@@ -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<Vec<DeviceCode>, DomainError> {
async fn list_by_user(&self, user_id: Uuid) -> Result<Vec<DeviceCode>, 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())
@@ -20,9 +20,7 @@ impl FavoritesPgRepository {
}
impl FavoritesRepositoryPort for FavoritesPgRepository {
async fn get_favorites(&self, user_id: &str) -> Result<Vec<FavoriteItemDto>> {
let user_uuid = Uuid::parse_str(user_id)?;
async fn get_favorites(&self, user_id: Uuid) -> Result<Vec<FavoriteItemDto>> {
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<bool> {
let user_uuid = Uuid::parse_str(user_id)?;
async fn remove_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<bool> {
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<bool> {
let user_uuid = Uuid::parse_str(user_id)?;
async fn is_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<bool> {
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<u64> {
async fn add_favorites_batch(&self, user_id: Uuid, items: &[(String, String)]) -> Result<u64> {
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<HashSet<String>> {
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<String> = 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
@@ -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<i64>,
limit: i64,
) -> Result<(Vec<File>, Vec<i64>), DomainError> {
@@ -255,7 +256,7 @@ impl FileReadPort for FileBlobReadRepository {
)
}
async fn get_file_for_owner(&self, id: &str, owner_id: &str) -> Result<File, DomainError> {
async fn get_file_for_owner(&self, id: &str, owner_id: Uuid) -> Result<File, DomainError> {
let 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<Vec<File>, DomainError> {
let owner_str = owner_id.to_string();
let rows: Vec<FileRow> = 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<Vec<File>, DomainError> {
let owner_str = owner_id.to_string();
let rows: Vec<FileRow> = 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<File>, 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<File>, 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<usize, DomainError> {
let (_, count) = self
.search_files_paginated(folder_id, criteria, user_id)
@@ -19,9 +19,7 @@ impl RecentItemsPgRepository {
}
impl RecentItemsRepositoryPort for RecentItemsPgRepository {
async fn get_recent_items(&self, user_id: &str, limit: i32) -> Result<Vec<RecentItemDto>> {
let user_uuid = Uuid::parse_str(user_id)?;
async fn get_recent_items(&self, user_id: Uuid, limit: i32) -> Result<Vec<RecentItemDto>> {
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<bool> {
let user_uuid = Uuid::parse_str(user_id)?;
async fn remove_item(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<bool> {
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
@@ -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<Session> {
async fn get_session_by_id(&self, id: Uuid) -> SessionRepositoryResult<Session> {
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<Vec<Session>> {
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<u64> {
let user_id_clone = user_id.to_string(); // Clone for use in closure
async fn revoke_all_user_sessions(&self, user_id: Uuid) -> SessionRepositoryResult<u64> {
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<u64, DomainError> {
async fn revoke_all_user_sessions(&self, user_id: Uuid) -> Result<u64, DomainError> {
SessionRepository::revoke_all_user_sessions(self, user_id)
.await
.map_err(DomainError::from)
@@ -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<Uuid>,
) -> 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<bool, DomainError> {
async fn try_claim_initialization(&self, admin_user_id: Uuid) -> Result<bool, DomainError> {
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())
@@ -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<Share, DomainError> {
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<Share, DomainError> {
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<Share, DomainError> {
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<Vec<Share>, 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<Share>, 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
@@ -50,7 +50,7 @@ impl TrashDbRepository {
id: Uuid,
name: String,
item_type: String,
user_id: String,
user_id: Uuid,
trashed_at: Option<DateTime<Utc>>,
) -> 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<Vec<TrashedItem>> {
let rows = sqlx::query_as::<_, (Uuid, String, String, String, Option<DateTime<Utc>>)>(
let rows = sqlx::query_as::<_, (Uuid, String, String, Uuid, Option<DateTime<Utc>>)>(
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<Option<TrashedItem>> {
let row = sqlx::query_as::<_, (Uuid, String, String, String, Option<DateTime<Utc>>)>(
let row = sqlx::query_as::<_, (Uuid, String, String, Uuid, Option<DateTime<Utc>>)>(
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}")))?;
@@ -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<User> {
async fn get_user_by_id(&self, id: Uuid) -> UserRepositoryResult<User> {
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<User, DomainError> {
async fn get_user_by_id(&self, id: Uuid) -> Result<User, DomainError> {
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)
@@ -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<String>,
content_type: String,
@@ -809,7 +809,7 @@ impl ChunkedUploadPort for ChunkedUploadService {
chunk_size: Option<usize>,
) -> Result<CreateUploadResponseDto, DomainError> {
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<String>,
) -> Result<ChunkUploadResponseDto, DomainError> {
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<UploadStatusResponseDto, DomainError> {
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>, 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))
}
@@ -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<ResolvedResource, DomainError> {
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<bool, DomainError> {
pub async fn exists_for_user(&self, path: &str, user_id: Uuid) -> Result<bool, DomainError> {
let path = path.trim_start_matches('/').trim_end_matches('/');
if path.is_empty() {
return Ok(false);
+23 -10
View File
@@ -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<Arc<AppState>> {
@@ -41,7 +42,7 @@ pub fn admin_routes() -> Router<Arc<AppState>> {
}
/// 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<impl IntoResponse, AppError> {
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<impl IntoResponse, AppError> {
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<impl IntoResponse, AppError> {
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<impl IntoResponse, AppError> {
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<impl IntoResponse, AppError> {
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<impl IntoResponse, AppError> {
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)))?;
@@ -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<Arc<AppState>> {
@@ -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)?;
+10 -8
View File
@@ -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");
+11 -11
View File
@@ -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);
+24 -24
View File
@@ -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)))?;
}
+18 -18
View File
@@ -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 {
@@ -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(),
+2 -2
View File
@@ -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")
@@ -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<impl IntoResponse, AppError> {
let device_service = get_device_service(&state)?;
let device_id = Uuid::parse_str(&device_id).map_err(|_| AppError::bad_request("Invalid device ID"))?;
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);
@@ -30,7 +30,7 @@ pub async fn get_favorites(
State(favorites_service): State<Arc<FavoritesService>>,
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<BatchFavoritesRequest>,
) -> impl IntoResponse {
let user_id = &auth_user.id;
let user_id = auth_user.id;
if body.items.is_empty() {
return (
+12 -12
View File
@@ -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::<u64>().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, &params);
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()
}
+12 -12
View File
@@ -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<PaginationRequestDto>,
) -> 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<String>,
Json(dto): Json<RenameFolderDto>,
) -> 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<String>,
Json(dto): Json<MoveFolderDto>,
) -> 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<String>,
) -> 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<String>,
) -> 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,
@@ -31,7 +31,7 @@ pub async fn list_photos(
auth_user: AuthUser,
Query(params): Query<PhotosQueryParams>,
) -> 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;
@@ -25,7 +25,7 @@ pub async fn get_recent_items(
auth_user: AuthUser,
Query(params): Query<GetRecentParams>,
) -> 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<Arc<RecentService>>,
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(_) => {
@@ -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",
+18 -5
View File
@@ -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<CreateShareDto>,
) -> 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<String>,
) -> 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<GetSharesQuery>,
) -> 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<String>,
Json(dto): Json<UpdateShareDto>,
) -> 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<String>,
) -> 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(),
}
+7 -7
View File
@@ -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<String>,
) -> (StatusCode, Json<serde_json::Value>) {
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<String>,
) -> (StatusCode, Json<serde_json::Value>) {
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(_) => {
+45 -46
View File
@@ -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<AppState>, user_id: &str, path: &str) -> Option<String> {
async fn resolve_webdav_path(state: &Arc<AppState>, user_id: Uuid, path: &str) -> Option<String> {
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::<Arc<CurrentUser>>().map(|u| u.id.clone());
let user_id = req.extensions().get::<Arc<CurrentUser>>().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<FolderService>,
file_retrieval_service: std::sync::Arc<FileRetrievalService>,
user_id: &str,
user_id: Uuid,
) -> Result<Response<Body>, 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 + <D:multistatus> + 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<FileDto> = 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,
+8 -4
View File
@@ -396,7 +396,7 @@ pub struct EditorUrlResponse {
async fn authorize_wopi_access<S: FileRetrievalUseCase>(
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(),
&params.file_id,
&user_id,
user_id,
&params.action,
)
.await
@@ -464,7 +464,7 @@ pub async fn get_editor_url(
let (access_token, access_token_ttl) =
match state
.token_service
.generate_token(&params.file_id, &user_id, &username, can_write)
.generate_token(&params.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
+16 -9
View File
@@ -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<S> FromRequestParts<S> for AuthUser
@@ -49,7 +50,7 @@ where
.extensions
.get::<Arc<CurrentUser>>()
.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::<Arc<CurrentUser>>()
.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<String>);
pub struct OptionalUserId(pub Option<Uuid>);
impl<S> FromRequestParts<S> for OptionalUserId
where
@@ -107,7 +108,7 @@ where
parts
.extensions
.get::<Arc<CurrentUser>>()
.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<Self, Self::Rejection> {
Ok(OptionalAuthUser(parts.extensions.get::<Arc<CurrentUser>>().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,
+1 -1
View File
@@ -174,7 +174,7 @@ pub async fn handle_login_submit(
let app_password = match nextcloud
.app_passwords
.create_nc(&current_user.id, "Nextcloud")
.create_nc(current_user.id, "Nextcloud")
.await
{
Ok((_id, password)) => password,
+4 -4
View File
@@ -47,7 +47,7 @@ pub async fn handle_capabilities_v2(State(state): State<Arc<AppState>>) -> Respo
pub async fn handle_user_info(State(state): State<Arc<AppState>>, 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(),
};
+2 -1
View File
@@ -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"))
+2 -2
View File
@@ -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)))?;
+4 -4
View File
@@ -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))
+10 -10
View File
@@ -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)))?;