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:
+2
-2
@@ -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
@@ -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
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>>;
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
///
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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<()>;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)))?;
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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, ¶ms);
|
||||
|
||||
match retrieval
|
||||
.get_file_range_stream_owned(&id, &auth_user.id, start, Some(end + 1))
|
||||
.get_file_range_stream_owned(&id, auth_user.id, start, Some(end + 1))
|
||||
.await
|
||||
{
|
||||
Ok(stream) => {
|
||||
@@ -569,7 +569,7 @@ impl FileHandler {
|
||||
tracing::info!("API: Listing files with folder_id: {:?}", folder_id);
|
||||
|
||||
let retrieval = &state.applications.file_retrieval_service;
|
||||
match retrieval.list_files_owned(folder_id, &auth_user.id).await {
|
||||
match retrieval.list_files_owned(folder_id, auth_user.id).await {
|
||||
Ok(files) => {
|
||||
// Compute lightweight ETag from max modified_at + count
|
||||
let max_mod = files.iter().map(|f| f.modified_at).max().unwrap_or(0);
|
||||
@@ -655,7 +655,7 @@ impl FileHandler {
|
||||
) -> impl IntoResponse {
|
||||
// Verify ownership
|
||||
let file_read = &state.repositories.file_read_repository;
|
||||
if let Err(e) = file_read.verify_file_owner(&file_id, &auth_user.id).await {
|
||||
if let Err(e) = file_read.verify_file_owner(&file_id, auth_user.id).await {
|
||||
let msg = e.to_string();
|
||||
return (
|
||||
StatusCode::NOT_FOUND,
|
||||
@@ -703,7 +703,7 @@ impl FileHandler {
|
||||
|
||||
// Auth required: trash-first with dedup cleanup + ownership verification
|
||||
let result = mgmt
|
||||
.delete_with_cleanup(&id, &auth_user.id)
|
||||
.delete_with_cleanup(&id, auth_user.id)
|
||||
.await
|
||||
.map(|was_trashed| {
|
||||
if was_trashed {
|
||||
@@ -745,7 +745,7 @@ impl FileHandler {
|
||||
|
||||
tracing::info!("Renaming file {} to \"{}\"", id, new_name);
|
||||
let mgmt = &state.applications.file_management_service;
|
||||
match mgmt.rename_file_owned(&id, &auth_user.id, &new_name).await {
|
||||
match mgmt.rename_file_owned(&id, auth_user.id, &new_name).await {
|
||||
Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(),
|
||||
Err(err) => AppError::from(err).into_response()
|
||||
}
|
||||
@@ -763,7 +763,7 @@ impl FileHandler {
|
||||
let mgmt = &state.applications.file_management_service;
|
||||
|
||||
match mgmt
|
||||
.move_file_owned(&id, &auth_user.id, payload.folder_id)
|
||||
.move_file_owned(&id, auth_user.id, payload.folder_id)
|
||||
.await
|
||||
{
|
||||
Ok(file) => (StatusCode::OK, Json(file)).into_response(),
|
||||
@@ -784,7 +784,7 @@ impl FileHandler {
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let mgmt = &state.applications.file_management_service;
|
||||
match mgmt.move_file_owned(&id, &auth_user.id, folder_id).await {
|
||||
match mgmt.move_file_owned(&id, auth_user.id, folder_id).await {
|
||||
Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(),
|
||||
Err(err) => AppError::from(err).into_response()
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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(_) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
¶ms.file_id,
|
||||
&user_id,
|
||||
user_id,
|
||||
¶ms.action,
|
||||
)
|
||||
.await
|
||||
@@ -464,7 +464,7 @@ pub async fn get_editor_url(
|
||||
let (access_token, access_token_ttl) =
|
||||
match state
|
||||
.token_service
|
||||
.generate_token(¶ms.file_id, &user_id, &username, can_write)
|
||||
.generate_token(¶ms.file_id, &user_id.to_string(), &username, can_write)
|
||||
{
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
@@ -503,10 +503,14 @@ async fn host_page(
|
||||
|
||||
// Re-verify ownership even though the token was valid — defence in depth.
|
||||
let requested_action = if claims.can_write { "edit" } else { "view" };
|
||||
let caller_uuid = match uuid::Uuid::parse_str(&claims.sub) {
|
||||
Ok(u) => u,
|
||||
Err(_) => return StatusCode::UNAUTHORIZED.into_response(),
|
||||
};
|
||||
let file = match authorize_wopi_access(
|
||||
state.app_state.applications.file_retrieval_service.as_ref(),
|
||||
&file_id,
|
||||
&claims.sub,
|
||||
caller_uuid,
|
||||
requested_action,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -174,7 +174,7 @@ pub async fn handle_login_submit(
|
||||
|
||||
let app_password = match nextcloud
|
||||
.app_passwords
|
||||
.create_nc(¤t_user.id, "Nextcloud")
|
||||
.create_nc(current_user.id, "Nextcloud")
|
||||
.await
|
||||
{
|
||||
Ok((_id, password)) => password,
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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)))?;
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)))?;
|
||||
|
||||
Reference in New Issue
Block a user