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
|
# Stage 1: Cache dependencies
|
||||||
FROM rust:1.93.0-alpine3.23 AS cacher
|
FROM rust:1.94.0-alpine3.23 AS cacher
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apk --no-cache upgrade && \
|
RUN apk --no-cache upgrade && \
|
||||||
apk add --no-cache musl-dev pkgconfig postgresql-dev gcc perl make
|
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 && \
|
RUSTFLAGS="-C target-cpu=native" cargo build --release && \
|
||||||
rm -rf src target/release/deps/oxicloud*
|
rm -rf src target/release/deps/oxicloud*
|
||||||
# Stage 2: Build the application
|
# 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
|
WORKDIR /app
|
||||||
RUN apk --no-cache upgrade && \
|
RUN apk --no-cache upgrade && \
|
||||||
apk add --no-cache musl-dev pkgconfig postgresql-dev gcc perl make
|
apk add --no-cache musl-dev pkgconfig postgresql-dev gcc perl make
|
||||||
|
|||||||
+18
-18
@@ -34,7 +34,7 @@ END $BODY$;
|
|||||||
|
|
||||||
-- Users table
|
-- Users table
|
||||||
CREATE TABLE IF NOT EXISTS auth.users (
|
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,
|
username TEXT UNIQUE NOT NULL,
|
||||||
email TEXT UNIQUE NOT NULL,
|
email TEXT UNIQUE NOT NULL,
|
||||||
password_hash TEXT 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
|
-- Sessions table for refresh tokens
|
||||||
CREATE TABLE IF NOT EXISTS auth.sessions (
|
CREATE TABLE IF NOT EXISTS auth.sessions (
|
||||||
id VARCHAR(36) PRIMARY KEY,
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
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,
|
||||||
refresh_token TEXT NOT NULL UNIQUE,
|
refresh_token TEXT NOT NULL UNIQUE,
|
||||||
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
ip_address TEXT,
|
ip_address TEXT,
|
||||||
@@ -80,7 +80,7 @@ WHERE NOT revoked AND auth.is_session_active(expires_at);
|
|||||||
-- File ownership tracking
|
-- File ownership tracking
|
||||||
CREATE TABLE IF NOT EXISTS auth.user_files (
|
CREATE TABLE IF NOT EXISTS auth.user_files (
|
||||||
id SERIAL PRIMARY KEY,
|
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_path TEXT NOT NULL,
|
||||||
file_id TEXT NOT NULL,
|
file_id TEXT NOT NULL,
|
||||||
size_bytes BIGINT NOT NULL DEFAULT 0,
|
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
|
-- User favorites
|
||||||
CREATE TABLE IF NOT EXISTS auth.user_favorites (
|
CREATE TABLE IF NOT EXISTS auth.user_favorites (
|
||||||
id SERIAL PRIMARY KEY,
|
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_id TEXT NOT NULL,
|
||||||
item_type TEXT NOT NULL, -- 'file' or 'folder'
|
item_type TEXT NOT NULL, -- 'file' or 'folder'
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
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
|
-- Recent files
|
||||||
CREATE TABLE IF NOT EXISTS auth.user_recent_files (
|
CREATE TABLE IF NOT EXISTS auth.user_recent_files (
|
||||||
id SERIAL PRIMARY KEY,
|
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_id TEXT NOT NULL,
|
||||||
item_type TEXT NOT NULL, -- 'file' or 'folder'
|
item_type TEXT NOT NULL, -- 'file' or 'folder'
|
||||||
accessed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
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',
|
category VARCHAR(50) NOT NULL DEFAULT 'general',
|
||||||
is_secret BOOLEAN NOT NULL DEFAULT FALSE,
|
is_secret BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
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);
|
CREATE INDEX IF NOT EXISTS idx_admin_settings_category ON auth.admin_settings(category);
|
||||||
@@ -172,13 +172,13 @@ BEGIN
|
|||||||
END $BODY$;
|
END $BODY$;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS auth.device_codes (
|
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,
|
device_code VARCHAR(128) UNIQUE NOT NULL,
|
||||||
user_code VARCHAR(16) UNIQUE NOT NULL,
|
user_code VARCHAR(16) UNIQUE NOT NULL,
|
||||||
client_name VARCHAR(255) NOT NULL DEFAULT 'Unknown Client',
|
client_name VARCHAR(255) NOT NULL DEFAULT 'Unknown Client',
|
||||||
scopes VARCHAR(512) NOT NULL DEFAULT 'webdav,caldav,carddav',
|
scopes VARCHAR(512) NOT NULL DEFAULT 'webdav,caldav,carddav',
|
||||||
status auth.device_code_status NOT NULL DEFAULT 'pending',
|
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,
|
access_token TEXT,
|
||||||
refresh_token TEXT,
|
refresh_token TEXT,
|
||||||
verification_uri TEXT NOT NULL,
|
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)
|
-- App Passwords (application-specific passwords for DAV clients with HTTP Basic Auth)
|
||||||
CREATE TABLE IF NOT EXISTS auth.app_passwords (
|
CREATE TABLE IF NOT EXISTS auth.app_passwords (
|
||||||
id VARCHAR(36) PRIMARY KEY,
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
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,
|
||||||
label VARCHAR(255) NOT NULL,
|
label VARCHAR(255) NOT NULL,
|
||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT NOT NULL,
|
||||||
prefix VARCHAR(50) NOT NULL,
|
prefix VARCHAR(50) NOT NULL,
|
||||||
@@ -231,7 +231,7 @@ CREATE SCHEMA IF NOT EXISTS caldav;
|
|||||||
CREATE TABLE IF NOT EXISTS caldav.calendars (
|
CREATE TABLE IF NOT EXISTS caldav.calendars (
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
name TEXT NOT NULL,
|
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,
|
description TEXT,
|
||||||
color VARCHAR(9), -- #RRGGBB or #RRGGBBAA
|
color VARCHAR(9), -- #RRGGBB or #RRGGBBAA
|
||||||
is_public BOOLEAN NOT NULL DEFAULT FALSE,
|
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 (
|
CREATE TABLE IF NOT EXISTS caldav.calendar_shares (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
calendar_id UUID NOT NULL REFERENCES caldav.calendars(id) ON DELETE CASCADE,
|
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
|
access_level VARCHAR(10) NOT NULL DEFAULT 'read', -- read, write, owner
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
UNIQUE(calendar_id, user_id)
|
UNIQUE(calendar_id, user_id)
|
||||||
@@ -305,7 +305,7 @@ CREATE SCHEMA IF NOT EXISTS carddav;
|
|||||||
CREATE TABLE IF NOT EXISTS carddav.address_books (
|
CREATE TABLE IF NOT EXISTS carddav.address_books (
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
name TEXT NOT NULL,
|
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,
|
description TEXT,
|
||||||
color VARCHAR(9),
|
color VARCHAR(9),
|
||||||
is_public BOOLEAN NOT NULL DEFAULT FALSE,
|
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 (
|
CREATE TABLE IF NOT EXISTS carddav.address_book_shares (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
address_book_id UUID NOT NULL REFERENCES carddav.address_books(id) ON DELETE CASCADE,
|
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,
|
can_write BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
UNIQUE(address_book_id, user_id)
|
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(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
parent_id UUID REFERENCES storage.folders(id) ON DELETE CASCADE,
|
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 '',
|
path TEXT NOT NULL DEFAULT '',
|
||||||
lpath ltree NOT NULL DEFAULT '',
|
lpath ltree NOT NULL DEFAULT '',
|
||||||
is_trashed BOOLEAN NOT NULL DEFAULT FALSE,
|
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(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
folder_id UUID REFERENCES storage.folders(id) ON DELETE CASCADE,
|
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,
|
blob_hash VARCHAR(64) NOT NULL,
|
||||||
size BIGINT NOT NULL DEFAULT 0,
|
size BIGINT NOT NULL DEFAULT 0,
|
||||||
mime_type TEXT NOT NULL DEFAULT 'application/octet-stream',
|
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_write BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
permissions_reshare BOOLEAN NOT NULL DEFAULT FALSE,
|
permissions_reshare BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
created_at BIGINT NOT NULL, -- unix epoch seconds
|
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
|
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 crate::domain::entities::user::User;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct UserDto {
|
pub struct UserDto {
|
||||||
@@ -80,7 +81,7 @@ pub struct RefreshTokenDto {
|
|||||||
/// Authenticated current user data (for use in application services)
|
/// Authenticated current user data (for use in application services)
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct CurrentUser {
|
pub struct CurrentUser {
|
||||||
pub id: String,
|
pub id: Uuid,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub role: 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::device_code::DeviceCode;
|
||||||
use crate::domain::entities::session::Session;
|
use crate::domain::entities::session::Session;
|
||||||
use crate::domain::entities::user::User;
|
use crate::domain::entities::user::User;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Cryptography Ports - Extracted from Domain to maintain Clean Architecture
|
// 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>;
|
async fn create_user(&self, user: User) -> Result<User, DomainError>;
|
||||||
|
|
||||||
/// Gets a user by ID
|
/// 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
|
/// Gets a user by username
|
||||||
async fn get_user_by_username(&self, username: &str) -> Result<User, DomainError>;
|
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
|
/// Updates only the storage usage of a user
|
||||||
async fn update_storage_usage(
|
async fn update_storage_usage(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
usage_bytes: i64,
|
usage_bytes: i64,
|
||||||
) -> Result<(), DomainError>;
|
) -> 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>;
|
async fn list_users_by_role(&self, role: &str) -> Result<Vec<User>, DomainError>;
|
||||||
|
|
||||||
/// Deletes a user by their ID
|
/// 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
|
/// 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
|
/// Finds a user by OIDC provider + subject pair
|
||||||
async fn get_user_by_oidc_subject(
|
async fn get_user_by_oidc_subject(
|
||||||
@@ -113,15 +114,15 @@ pub trait UserStoragePort: Send + Sync + 'static {
|
|||||||
) -> Result<User, DomainError>;
|
) -> Result<User, DomainError>;
|
||||||
|
|
||||||
/// Activates or deactivates a user
|
/// 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
|
/// 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
|
/// Updates a user's storage quota
|
||||||
async fn update_storage_quota(
|
async fn update_storage_quota(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
quota_bytes: i64,
|
quota_bytes: i64,
|
||||||
) -> Result<(), DomainError>;
|
) -> Result<(), DomainError>;
|
||||||
|
|
||||||
@@ -197,10 +198,10 @@ pub trait SessionStoragePort: Send + Sync + 'static {
|
|||||||
) -> Result<Session, DomainError>;
|
) -> Result<Session, DomainError>;
|
||||||
|
|
||||||
/// Revokes a specific session
|
/// 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
|
/// 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>;
|
async fn delete_expired(&self) -> Result<u64, DomainError>;
|
||||||
|
|
||||||
/// List authorized device codes for a user (for UI management)
|
/// 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)
|
/// 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>;
|
async fn create(&self, app_password: AppPassword) -> Result<AppPassword, DomainError>;
|
||||||
|
|
||||||
/// Get all active (non-expired) app passwords for a user.
|
/// 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.
|
/// 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).
|
/// Get all active app passwords for a user ID (for Basic auth verification).
|
||||||
/// This includes the password hash for 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.
|
/// 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).
|
/// 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.
|
/// More efficient than `get_active_by_user_id` when the password prefix is known.
|
||||||
async fn get_active_by_user_prefix(
|
async fn get_active_by_user_prefix(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
) -> Result<Vec<AppPassword>, DomainError>;
|
) -> Result<Vec<AppPassword>, DomainError>;
|
||||||
|
|
||||||
/// Deactivate (soft-delete) an app password, scoped to the owning user.
|
/// 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.
|
/// 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).
|
/// Hard-delete expired/revoked app passwords (cleanup).
|
||||||
async fn delete_expired(&self) -> Result<u64, DomainError>;
|
async fn delete_expired(&self) -> Result<u64, DomainError>;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::application::dtos::calendar_dto::{
|
|||||||
};
|
};
|
||||||
use crate::common::errors::DomainError;
|
use crate::common::errors::DomainError;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Port for external calendar storage mechanisms
|
/// Port for external calendar storage mechanisms
|
||||||
pub trait CalendarStoragePort: Send + Sync + 'static {
|
pub trait CalendarStoragePort: Send + Sync + 'static {
|
||||||
@@ -11,7 +12,7 @@ pub trait CalendarStoragePort: Send + Sync + 'static {
|
|||||||
async fn create_calendar(
|
async fn create_calendar(
|
||||||
&self,
|
&self,
|
||||||
calendar: CreateCalendarDto,
|
calendar: CreateCalendarDto,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
) -> Result<CalendarDto, DomainError>;
|
) -> Result<CalendarDto, DomainError>;
|
||||||
async fn update_calendar(
|
async fn update_calendar(
|
||||||
&self,
|
&self,
|
||||||
@@ -22,11 +23,11 @@ pub trait CalendarStoragePort: Send + Sync + 'static {
|
|||||||
async fn get_calendar(&self, calendar_id: &str) -> Result<CalendarDto, DomainError>;
|
async fn get_calendar(&self, calendar_id: &str) -> Result<CalendarDto, DomainError>;
|
||||||
async fn list_calendars_by_owner(
|
async fn list_calendars_by_owner(
|
||||||
&self,
|
&self,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
) -> Result<Vec<CalendarDto>, DomainError>;
|
) -> Result<Vec<CalendarDto>, DomainError>;
|
||||||
async fn list_calendars_shared_with_user(
|
async fn list_calendars_shared_with_user(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<CalendarDto>, DomainError>;
|
) -> Result<Vec<CalendarDto>, DomainError>;
|
||||||
async fn list_public_calendars(
|
async fn list_public_calendars(
|
||||||
&self,
|
&self,
|
||||||
@@ -36,20 +37,20 @@ pub trait CalendarStoragePort: Send + Sync + 'static {
|
|||||||
async fn check_calendar_access(
|
async fn check_calendar_access(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &str,
|
calendar_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<bool, DomainError>;
|
) -> Result<bool, DomainError>;
|
||||||
|
|
||||||
// Calendar sharing
|
// Calendar sharing
|
||||||
async fn share_calendar(
|
async fn share_calendar(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &str,
|
calendar_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
access_level: &str,
|
access_level: &str,
|
||||||
) -> Result<(), DomainError>;
|
) -> Result<(), DomainError>;
|
||||||
async fn remove_calendar_sharing(
|
async fn remove_calendar_sharing(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &str,
|
calendar_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(), DomainError>;
|
) -> Result<(), DomainError>;
|
||||||
async fn get_calendar_shares(
|
async fn get_calendar_shares(
|
||||||
&self,
|
&self,
|
||||||
@@ -114,22 +115,22 @@ pub trait CalendarUseCase: Send + Sync + 'static {
|
|||||||
async fn create_calendar(
|
async fn create_calendar(
|
||||||
&self,
|
&self,
|
||||||
calendar: CreateCalendarDto,
|
calendar: CreateCalendarDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<CalendarDto, DomainError>;
|
) -> Result<CalendarDto, DomainError>;
|
||||||
async fn update_calendar(
|
async fn update_calendar(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &str,
|
calendar_id: &str,
|
||||||
update: UpdateCalendarDto,
|
update: UpdateCalendarDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<CalendarDto, DomainError>;
|
) -> 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(
|
async fn get_calendar(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &str,
|
calendar_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<CalendarDto, DomainError>;
|
) -> Result<CalendarDto, DomainError>;
|
||||||
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>;
|
||||||
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>;
|
||||||
async fn list_public_calendars(
|
async fn list_public_calendars(
|
||||||
&self,
|
&self,
|
||||||
limit: Option<i64>,
|
limit: Option<i64>,
|
||||||
@@ -140,57 +141,57 @@ pub trait CalendarUseCase: Send + Sync + 'static {
|
|||||||
async fn share_calendar(
|
async fn share_calendar(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &str,
|
calendar_id: &str,
|
||||||
target_user_id: &str,
|
target_user_id: Uuid,
|
||||||
access_level: &str,
|
access_level: &str,
|
||||||
caller_user_id: &str,
|
caller_user_id: Uuid,
|
||||||
) -> Result<(), DomainError>;
|
) -> Result<(), DomainError>;
|
||||||
async fn remove_calendar_sharing(
|
async fn remove_calendar_sharing(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &str,
|
calendar_id: &str,
|
||||||
target_user_id: &str,
|
target_user_id: Uuid,
|
||||||
caller_user_id: &str,
|
caller_user_id: Uuid,
|
||||||
) -> Result<(), DomainError>;
|
) -> Result<(), DomainError>;
|
||||||
async fn get_calendar_shares(
|
async fn get_calendar_shares(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &str,
|
calendar_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<(String, String)>, DomainError>;
|
) -> Result<Vec<(String, String)>, DomainError>;
|
||||||
|
|
||||||
// Event operations
|
// Event operations
|
||||||
async fn create_event(
|
async fn create_event(
|
||||||
&self,
|
&self,
|
||||||
event: CreateEventDto,
|
event: CreateEventDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<CalendarEventDto, DomainError>;
|
) -> Result<CalendarEventDto, DomainError>;
|
||||||
async fn create_event_from_ical(
|
async fn create_event_from_ical(
|
||||||
&self,
|
&self,
|
||||||
event: CreateEventICalDto,
|
event: CreateEventICalDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<CalendarEventDto, DomainError>;
|
) -> Result<CalendarEventDto, DomainError>;
|
||||||
async fn update_event(
|
async fn update_event(
|
||||||
&self,
|
&self,
|
||||||
event_id: &str,
|
event_id: &str,
|
||||||
update: UpdateEventDto,
|
update: UpdateEventDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<CalendarEventDto, DomainError>;
|
) -> 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(
|
async fn get_event(
|
||||||
&self,
|
&self,
|
||||||
event_id: &str,
|
event_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<CalendarEventDto, DomainError>;
|
) -> Result<CalendarEventDto, DomainError>;
|
||||||
async fn list_events(
|
async fn list_events(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &str,
|
calendar_id: &str,
|
||||||
limit: Option<i64>,
|
limit: Option<i64>,
|
||||||
offset: Option<i64>,
|
offset: Option<i64>,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<CalendarEventDto>, DomainError>;
|
) -> Result<Vec<CalendarEventDto>, DomainError>;
|
||||||
async fn get_events_in_range(
|
async fn get_events_in_range(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &str,
|
calendar_id: &str,
|
||||||
start: DateTime<Utc>,
|
start: DateTime<Utc>,
|
||||||
end: DateTime<Utc>,
|
end: DateTime<Utc>,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<CalendarEventDto>, DomainError>;
|
) -> Result<Vec<CalendarEventDto>, DomainError>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use crate::application::dtos::contact_dto::{
|
|||||||
GroupMembershipDto, UpdateContactDto, UpdateContactGroupDto,
|
GroupMembershipDto, UpdateContactDto, UpdateContactGroupDto,
|
||||||
};
|
};
|
||||||
use crate::common::errors::DomainError;
|
use crate::common::errors::DomainError;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub type CardDavRepositoryError = DomainError;
|
pub type CardDavRepositoryError = DomainError;
|
||||||
|
|
||||||
@@ -24,16 +25,16 @@ pub trait AddressBookUseCase: Send + Sync + 'static {
|
|||||||
async fn delete_address_book(
|
async fn delete_address_book(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &str,
|
address_book_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(), DomainError>;
|
) -> Result<(), DomainError>;
|
||||||
async fn get_address_book(
|
async fn get_address_book(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &str,
|
address_book_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<AddressBookDto, DomainError>;
|
) -> Result<AddressBookDto, DomainError>;
|
||||||
async fn list_user_address_books(
|
async fn list_user_address_books(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<AddressBookDto>, DomainError>;
|
) -> Result<Vec<AddressBookDto>, DomainError>;
|
||||||
async fn list_public_address_books(&self) -> 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(
|
async fn share_address_book(
|
||||||
&self,
|
&self,
|
||||||
dto: ShareAddressBookDto,
|
dto: ShareAddressBookDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(), DomainError>;
|
) -> Result<(), DomainError>;
|
||||||
async fn unshare_address_book(
|
async fn unshare_address_book(
|
||||||
&self,
|
&self,
|
||||||
dto: UnshareAddressBookDto,
|
dto: UnshareAddressBookDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(), DomainError>;
|
) -> Result<(), DomainError>;
|
||||||
async fn get_address_book_shares(
|
async fn get_address_book_shares(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &str,
|
address_book_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<(String, bool)>, DomainError>;
|
) -> Result<Vec<(String, bool)>, DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,19 +68,19 @@ pub trait ContactUseCase: Send + Sync + 'static {
|
|||||||
contact_id: &str,
|
contact_id: &str,
|
||||||
update: UpdateContactDto,
|
update: UpdateContactDto,
|
||||||
) -> Result<ContactDto, DomainError>;
|
) -> Result<ContactDto, DomainError>;
|
||||||
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>;
|
||||||
async fn get_contact(&self, contact_id: &str, user_id: &str)
|
async fn get_contact(&self, contact_id: &str, user_id: Uuid)
|
||||||
-> Result<ContactDto, DomainError>;
|
-> Result<ContactDto, DomainError>;
|
||||||
async fn list_contacts(
|
async fn list_contacts(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &str,
|
address_book_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<ContactDto>, DomainError>;
|
) -> Result<Vec<ContactDto>, DomainError>;
|
||||||
async fn search_contacts(
|
async fn search_contacts(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &str,
|
address_book_id: &str,
|
||||||
query: &str,
|
query: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<ContactDto>, DomainError>;
|
) -> Result<Vec<ContactDto>, DomainError>;
|
||||||
|
|
||||||
// Contact Group operations
|
// Contact Group operations
|
||||||
@@ -92,49 +93,49 @@ pub trait ContactUseCase: Send + Sync + 'static {
|
|||||||
group_id: &str,
|
group_id: &str,
|
||||||
update: UpdateContactGroupDto,
|
update: UpdateContactGroupDto,
|
||||||
) -> Result<ContactGroupDto, DomainError>;
|
) -> 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(
|
async fn get_group(
|
||||||
&self,
|
&self,
|
||||||
group_id: &str,
|
group_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<ContactGroupDto, DomainError>;
|
) -> Result<ContactGroupDto, DomainError>;
|
||||||
async fn list_groups(
|
async fn list_groups(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &str,
|
address_book_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<ContactGroupDto>, DomainError>;
|
) -> Result<Vec<ContactGroupDto>, DomainError>;
|
||||||
|
|
||||||
// Group membership
|
// Group membership
|
||||||
async fn add_contact_to_group(
|
async fn add_contact_to_group(
|
||||||
&self,
|
&self,
|
||||||
dto: GroupMembershipDto,
|
dto: GroupMembershipDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(), DomainError>;
|
) -> Result<(), DomainError>;
|
||||||
async fn remove_contact_from_group(
|
async fn remove_contact_from_group(
|
||||||
&self,
|
&self,
|
||||||
dto: GroupMembershipDto,
|
dto: GroupMembershipDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(), DomainError>;
|
) -> Result<(), DomainError>;
|
||||||
async fn list_contacts_in_group(
|
async fn list_contacts_in_group(
|
||||||
&self,
|
&self,
|
||||||
group_id: &str,
|
group_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<ContactDto>, DomainError>;
|
) -> Result<Vec<ContactDto>, DomainError>;
|
||||||
async fn list_groups_for_contact(
|
async fn list_groups_for_contact(
|
||||||
&self,
|
&self,
|
||||||
contact_id: &str,
|
contact_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<ContactGroupDto>, DomainError>;
|
) -> Result<Vec<ContactGroupDto>, DomainError>;
|
||||||
|
|
||||||
// vCard operations
|
// vCard operations
|
||||||
async fn get_contact_vcard(
|
async fn get_contact_vcard(
|
||||||
&self,
|
&self,
|
||||||
contact_id: &str,
|
contact_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<String, DomainError>;
|
) -> Result<String, DomainError>;
|
||||||
async fn get_contacts_as_vcards(
|
async fn get_contacts_as_vcards(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &str,
|
address_book_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<(String, String)>, DomainError>;
|
) -> Result<Vec<(String, String)>, DomainError>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use crate::common::errors::DomainError;
|
|||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Default chunk size (5 MB) — optimised for parallel transfers.
|
/// Default chunk size (5 MB) — optimised for parallel transfers.
|
||||||
pub const DEFAULT_CHUNK_SIZE: usize = 5 * 1024 * 1024;
|
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.
|
/// total number of chunks, and expiration timestamp.
|
||||||
async fn create_session(
|
async fn create_session(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
filename: String,
|
filename: String,
|
||||||
folder_id: Option<String>,
|
folder_id: Option<String>,
|
||||||
content_type: String,
|
content_type: String,
|
||||||
@@ -73,7 +74,7 @@ pub trait ChunkedUploadPort: Send + Sync + 'static {
|
|||||||
async fn upload_chunk(
|
async fn upload_chunk(
|
||||||
&self,
|
&self,
|
||||||
upload_id: &str,
|
upload_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
chunk_index: usize,
|
chunk_index: usize,
|
||||||
data: Bytes,
|
data: Bytes,
|
||||||
checksum: Option<String>,
|
checksum: Option<String>,
|
||||||
@@ -83,7 +84,7 @@ pub trait ChunkedUploadPort: Send + Sync + 'static {
|
|||||||
async fn get_status(
|
async fn get_status(
|
||||||
&self,
|
&self,
|
||||||
upload_id: &str,
|
upload_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<UploadStatusResponseDto, DomainError>;
|
) -> Result<UploadStatusResponseDto, DomainError>;
|
||||||
|
|
||||||
/// Assemble all chunks into the final file.
|
/// Assemble all chunks into the final file.
|
||||||
@@ -94,14 +95,14 @@ pub trait ChunkedUploadPort: Send + Sync + 'static {
|
|||||||
async fn complete_upload(
|
async fn complete_upload(
|
||||||
&self,
|
&self,
|
||||||
upload_id: &str,
|
upload_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(PathBuf, String, Option<String>, String, u64, String), DomainError>;
|
) -> Result<(PathBuf, String, Option<String>, String, u64, String), DomainError>;
|
||||||
|
|
||||||
/// Finalize upload: clean up the session and temporary files.
|
/// 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.
|
/// 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.
|
/// Check if a file size qualifies for chunked upload.
|
||||||
fn should_use_chunked(&self, size: u64) -> bool;
|
fn should_use_chunked(&self, size: u64) -> bool;
|
||||||
|
|||||||
@@ -1,32 +1,34 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::dtos::favorites_dto::{BatchFavoritesResult, FavoriteItemDto};
|
use crate::application::dtos::favorites_dto::{BatchFavoritesResult, FavoriteItemDto};
|
||||||
use crate::common::errors::Result;
|
use crate::common::errors::Result;
|
||||||
|
|
||||||
/// Defines operations for managing user favorites
|
/// Defines operations for managing user favorites
|
||||||
pub trait FavoritesUseCase: Send + Sync {
|
pub trait FavoritesUseCase: Send + Sync {
|
||||||
/// Get all favorites for a user
|
/// 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
|
/// 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
|
/// Remove an item from user's favorites
|
||||||
async fn remove_from_favorites(
|
async fn remove_from_favorites(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
item_id: &str,
|
item_id: &str,
|
||||||
item_type: &str,
|
item_type: &str,
|
||||||
) -> Result<bool>;
|
) -> Result<bool>;
|
||||||
|
|
||||||
/// Check if an item is in user's favorites
|
/// 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.
|
/// Add multiple items to favorites in a single transaction.
|
||||||
/// Returns enriched favourites list so the client can replace its cache.
|
/// Returns enriched favourites list so the client can replace its cache.
|
||||||
async fn batch_add_to_favorites(
|
async fn batch_add_to_favorites(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
items: &[(String, String)],
|
items: &[(String, String)],
|
||||||
) -> Result<BatchFavoritesResult>;
|
) -> Result<BatchFavoritesResult>;
|
||||||
|
|
||||||
@@ -34,7 +36,7 @@ pub trait FavoritesUseCase: Send + Sync {
|
|||||||
/// Returns the set of item_ids that are favorites.
|
/// Returns the set of item_ids that are favorites.
|
||||||
async fn batch_check_favorites(
|
async fn batch_check_favorites(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
item_ids: &[(&str, &str)], // (item_id, item_type) pairs
|
item_ids: &[(&str, &str)], // (item_id, item_type) pairs
|
||||||
) -> Result<HashSet<String>>;
|
) -> Result<HashSet<String>>;
|
||||||
}
|
}
|
||||||
@@ -50,26 +52,26 @@ pub trait FavoritesUseCase: Send + Sync {
|
|||||||
/// lives in `infrastructure::repositories::pg`.
|
/// lives in `infrastructure::repositories::pg`.
|
||||||
pub trait FavoritesRepositoryPort: Send + Sync + 'static {
|
pub trait FavoritesRepositoryPort: Send + Sync + 'static {
|
||||||
/// Gets all favorites for a user.
|
/// 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).
|
/// 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.
|
/// 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.
|
/// 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.
|
/// Insert multiple items in a single transaction.
|
||||||
/// Returns the number of rows actually inserted (ignoring duplicates).
|
/// 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.
|
/// Check which of the given item IDs are favorites for this user.
|
||||||
/// Returns the set of item_ids that are favorites.
|
/// Returns the set of item_ids that are favorites.
|
||||||
async fn batch_check_favorites(
|
async fn batch_check_favorites(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
item_ids: &[(&str, &str)], // (item_id, item_type) pairs
|
item_ids: &[(&str, &str)], // (item_id, item_type) pairs
|
||||||
) -> Result<HashSet<String>>;
|
) -> Result<HashSet<String>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use futures::Stream;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::dtos::file_dto::FileDto;
|
use crate::application::dtos::file_dto::FileDto;
|
||||||
use crate::application::ports::storage_ports::CopyFolderTreeResult;
|
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
|
/// Returns `NotFound` if the file does not exist **or** belongs to
|
||||||
/// another user. All user-facing handlers should use this method.
|
/// 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)
|
/// Gets a file by its path (for WebDAV)
|
||||||
async fn get_file_by_path(&self, path: &str) -> Result<FileDto, DomainError>;
|
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(
|
async fn list_files_owned(
|
||||||
&self,
|
&self,
|
||||||
folder_id: Option<&str>,
|
folder_id: Option<&str>,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
) -> Result<Vec<FileDto>, DomainError>;
|
) -> Result<Vec<FileDto>, DomainError>;
|
||||||
|
|
||||||
/// Gets file content as a stream (for large files)
|
/// 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(
|
async fn get_file_stream_owned(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
caller_id: &str,
|
caller_id: Uuid,
|
||||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
||||||
|
|
||||||
/// Optimized multi-tier download.
|
/// Optimized multi-tier download.
|
||||||
@@ -166,7 +167,7 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
|
|||||||
async fn get_file_optimized_owned(
|
async fn get_file_optimized_owned(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
caller_id: &str,
|
caller_id: Uuid,
|
||||||
accept_webp: bool,
|
accept_webp: bool,
|
||||||
prefer_original: bool,
|
prefer_original: bool,
|
||||||
) -> Result<(FileDto, OptimizedFileContent), DomainError>;
|
) -> Result<(FileDto, OptimizedFileContent), DomainError>;
|
||||||
@@ -198,7 +199,7 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
|
|||||||
async fn get_file_range_stream_owned(
|
async fn get_file_range_stream_owned(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
caller_id: &str,
|
caller_id: Uuid,
|
||||||
start: u64,
|
start: u64,
|
||||||
end: Option<u64>,
|
end: Option<u64>,
|
||||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
) -> 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(
|
async fn list_files_batch_for_owner(
|
||||||
&self,
|
&self,
|
||||||
folder_id: Option<&str>,
|
folder_id: Option<&str>,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
offset: i64,
|
offset: i64,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
) -> Result<Vec<FileDto>, DomainError> {
|
) -> Result<Vec<FileDto>, DomainError> {
|
||||||
let all = self.list_files_batch(folder_id, offset, limit).await?;
|
let all = self.list_files_batch(folder_id, offset, limit).await?;
|
||||||
|
let owner_str = owner_id.to_string();
|
||||||
Ok(all
|
Ok(all
|
||||||
.into_iter()
|
.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())
|
.collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,7 +269,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
|
|||||||
async fn move_file_owned(
|
async fn move_file_owned(
|
||||||
&self,
|
&self,
|
||||||
file_id: &str,
|
file_id: &str,
|
||||||
caller_id: &str,
|
caller_id: Uuid,
|
||||||
folder_id: Option<String>,
|
folder_id: Option<String>,
|
||||||
) -> Result<FileDto, DomainError>;
|
) -> Result<FileDto, DomainError>;
|
||||||
|
|
||||||
@@ -282,7 +284,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
|
|||||||
async fn copy_file_owned(
|
async fn copy_file_owned(
|
||||||
&self,
|
&self,
|
||||||
file_id: &str,
|
file_id: &str,
|
||||||
caller_id: &str,
|
caller_id: Uuid,
|
||||||
target_folder_id: Option<String>,
|
target_folder_id: Option<String>,
|
||||||
) -> Result<FileDto, DomainError>;
|
) -> Result<FileDto, DomainError>;
|
||||||
|
|
||||||
@@ -293,7 +295,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
|
|||||||
async fn rename_file_owned(
|
async fn rename_file_owned(
|
||||||
&self,
|
&self,
|
||||||
file_id: &str,
|
file_id: &str,
|
||||||
caller_id: &str,
|
caller_id: Uuid,
|
||||||
new_name: &str,
|
new_name: &str,
|
||||||
) -> Result<FileDto, DomainError>;
|
) -> Result<FileDto, DomainError>;
|
||||||
|
|
||||||
@@ -301,7 +303,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
|
|||||||
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
|
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Deletes a file, enforcing that `caller_id` is the owner.
|
/// 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.
|
/// 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.
|
/// 3. Decrements the dedup reference count for the content hash.
|
||||||
///
|
///
|
||||||
/// Returns `Ok(true)` when trashed, `Ok(false)` when permanently deleted.
|
/// 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).
|
/// Copies an entire folder subtree atomically (WebDAV COPY Depth: infinity).
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::dtos::folder_dto::{
|
use crate::application::dtos::folder_dto::{
|
||||||
CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto,
|
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
|
/// Returns `NotFound` if the folder does not exist **or** belongs to
|
||||||
/// another user. All user-facing handlers should use this method.
|
/// 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
|
/// Gets a folder by its path
|
||||||
async fn get_folder_by_path(&self, path: &str) -> Result<FolderDto, DomainError>;
|
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(
|
async fn list_folders_for_owner(
|
||||||
&self,
|
&self,
|
||||||
parent_id: Option<&str>,
|
parent_id: Option<&str>,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
) -> Result<Vec<FolderDto>, DomainError>;
|
) -> Result<Vec<FolderDto>, DomainError>;
|
||||||
|
|
||||||
/// Lists folders with pagination
|
/// Lists folders with pagination
|
||||||
@@ -47,7 +49,7 @@ pub trait FolderUseCase: Send + Sync + 'static {
|
|||||||
async fn list_folders_for_owner_paginated(
|
async fn list_folders_for_owner_paginated(
|
||||||
&self,
|
&self,
|
||||||
parent_id: Option<&str>,
|
parent_id: Option<&str>,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
pagination: &crate::application::dtos::pagination::PaginationRequestDto,
|
pagination: &crate::application::dtos::pagination::PaginationRequestDto,
|
||||||
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>;
|
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>;
|
||||||
|
|
||||||
@@ -56,7 +58,7 @@ pub trait FolderUseCase: Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
dto: RenameFolderDto,
|
dto: RenameFolderDto,
|
||||||
caller_id: &str,
|
caller_id: Uuid,
|
||||||
) -> Result<FolderDto, DomainError>;
|
) -> Result<FolderDto, DomainError>;
|
||||||
|
|
||||||
/// Moves a folder to another parent (ownership verified against caller_id)
|
/// Moves a folder to another parent (ownership verified against caller_id)
|
||||||
@@ -64,16 +66,16 @@ pub trait FolderUseCase: Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
dto: MoveFolderDto,
|
dto: MoveFolderDto,
|
||||||
caller_id: &str,
|
caller_id: Uuid,
|
||||||
) -> Result<FolderDto, DomainError>;
|
) -> Result<FolderDto, DomainError>;
|
||||||
|
|
||||||
/// Deletes a folder (ownership verified against caller_id)
|
/// 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.
|
/// Creates a root-level home folder for a user during registration.
|
||||||
async fn create_home_folder(
|
async fn create_home_folder(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
name: String,
|
name: String,
|
||||||
) -> Result<FolderDto, DomainError>;
|
) -> Result<FolderDto, DomainError>;
|
||||||
|
|
||||||
@@ -103,7 +105,7 @@ pub trait SearchUseCase: Send + Sync + 'static {
|
|||||||
async fn search(
|
async fn search(
|
||||||
&self,
|
&self,
|
||||||
criteria: SearchCriteriaDto,
|
criteria: SearchCriteriaDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Arc<SearchResultsDto>, DomainError>;
|
) -> Result<Arc<SearchResultsDto>, DomainError>;
|
||||||
|
|
||||||
/// Returns quick suggestions for autocomplete (lightweight, fast).
|
/// Returns quick suggestions for autocomplete (lightweight, fast).
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::dtos::recent_dto::RecentItemDto;
|
use crate::application::dtos::recent_dto::RecentItemDto;
|
||||||
use crate::common::errors::Result;
|
use crate::common::errors::Result;
|
||||||
|
|
||||||
@@ -6,24 +8,24 @@ pub trait RecentItemsUseCase: Send + Sync {
|
|||||||
/// Get all recent items for a user
|
/// Get all recent items for a user
|
||||||
async fn get_recent_items(
|
async fn get_recent_items(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
limit: Option<i32>,
|
limit: Option<i32>,
|
||||||
) -> Result<Vec<RecentItemDto>>;
|
) -> Result<Vec<RecentItemDto>>;
|
||||||
|
|
||||||
/// Record access to an item
|
/// 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<()>;
|
-> Result<()>;
|
||||||
|
|
||||||
/// Remove an item from recents
|
/// Remove an item from recents
|
||||||
async fn remove_from_recent(
|
async fn remove_from_recent(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
item_id: &str,
|
item_id: &str,
|
||||||
item_type: &str,
|
item_type: &str,
|
||||||
) -> Result<bool>;
|
) -> Result<bool>;
|
||||||
|
|
||||||
/// Clear the entire recent items list
|
/// 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`.
|
/// `RecentService` does not depend directly on `PgPool`.
|
||||||
pub trait RecentItemsRepositoryPort: Send + Sync + 'static {
|
pub trait RecentItemsRepositoryPort: Send + Sync + 'static {
|
||||||
/// Gets the latest recent items for a user (ordered by date desc).
|
/// 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).
|
/// 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.
|
/// 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.
|
/// 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).
|
/// 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::{
|
use crate::{
|
||||||
application::dtos::{
|
application::dtos::{
|
||||||
pagination::PaginatedResponseDto,
|
pagination::PaginatedResponseDto,
|
||||||
@@ -11,12 +13,12 @@ pub trait ShareUseCase: Send + Sync + 'static {
|
|||||||
/// Create a new shared link for a file or folder
|
/// Create a new shared link for a file or folder
|
||||||
async fn create_shared_link(
|
async fn create_shared_link(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
dto: CreateShareDto,
|
dto: CreateShareDto,
|
||||||
) -> Result<ShareDto, DomainError>;
|
) -> Result<ShareDto, DomainError>;
|
||||||
|
|
||||||
/// Get a shared link by its ID (ownership-verified)
|
/// 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)
|
/// 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>;
|
async fn get_shared_link_by_token(&self, token: &str) -> Result<ShareDto, DomainError>;
|
||||||
@@ -26,24 +28,24 @@ pub trait ShareUseCase: Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
item_id: &str,
|
item_id: &str,
|
||||||
item_type: &ShareItemType,
|
item_type: &ShareItemType,
|
||||||
requester_id: &str,
|
requester_id: Uuid,
|
||||||
) -> Result<Vec<ShareDto>, DomainError>;
|
) -> Result<Vec<ShareDto>, DomainError>;
|
||||||
|
|
||||||
/// Update a shared link (ownership-verified)
|
/// Update a shared link (ownership-verified)
|
||||||
async fn update_shared_link(
|
async fn update_shared_link(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: Uuid,
|
||||||
requester_id: &str,
|
requester_id: Uuid,
|
||||||
dto: UpdateShareDto,
|
dto: UpdateShareDto,
|
||||||
) -> Result<ShareDto, DomainError>;
|
) -> Result<ShareDto, DomainError>;
|
||||||
|
|
||||||
/// Delete a shared link (ownership-verified)
|
/// 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
|
/// Get all shared links created by a specific user
|
||||||
async fn get_user_shared_links(
|
async fn get_user_shared_links(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
page: usize,
|
page: usize,
|
||||||
per_page: usize,
|
per_page: usize,
|
||||||
) -> Result<PaginatedResponseDto<ShareDto>, DomainError>;
|
) -> Result<PaginatedResponseDto<ShareDto>, DomainError>;
|
||||||
@@ -77,19 +79,19 @@ pub trait ShareStoragePort: Send + Sync + 'static {
|
|||||||
/// (prevents share-ID enumeration).
|
/// (prevents share-ID enumeration).
|
||||||
async fn find_share_by_id_for_user(
|
async fn find_share_by_id_for_user(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: Uuid,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<crate::domain::entities::share::Share, DomainError>;
|
) -> Result<crate::domain::entities::share::Share, DomainError>;
|
||||||
|
|
||||||
/// Delete a share only if it belongs to the given user.
|
/// 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.
|
/// Find shares for a specific item that belong to the given user.
|
||||||
async fn find_shares_by_item_for_user(
|
async fn find_shares_by_item_for_user(
|
||||||
&self,
|
&self,
|
||||||
item_id: &str,
|
item_id: &str,
|
||||||
item_type: &ShareItemType,
|
item_type: &ShareItemType,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<crate::domain::entities::share::Share>, DomainError>;
|
) -> Result<Vec<crate::domain::entities::share::Share>, DomainError>;
|
||||||
|
|
||||||
async fn update_share(
|
async fn update_share(
|
||||||
@@ -99,7 +101,7 @@ pub trait ShareStoragePort: Send + Sync + 'static {
|
|||||||
|
|
||||||
async fn find_shares_by_user(
|
async fn find_shares_by_user(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
) -> Result<(Vec<crate::domain::entities::share::Share>, usize), DomainError>;
|
) -> Result<(Vec<crate::domain::entities::share::Share>, usize), DomainError>;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use futures::Stream;
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::dtos::search_dto::SearchCriteriaDto;
|
use crate::application::dtos::search_dto::SearchCriteriaDto;
|
||||||
use crate::common::errors::DomainError;
|
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
|
/// Returns `NotFound` if the file does not exist **or** belongs to a
|
||||||
/// different user. This is the primary IDOR-safe accessor — handlers
|
/// different user. This is the primary IDOR-safe accessor — handlers
|
||||||
/// serving end-user requests should always prefer this over `get_file`.
|
/// 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`.
|
/// Verifies that the file identified by `id` belongs to `owner_id`.
|
||||||
///
|
///
|
||||||
/// Returns `Ok(())` on success or `NotFound` when the file does not
|
/// Returns `Ok(())` on success or `NotFound` when the file does not
|
||||||
/// exist or belongs to another user.
|
/// 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(|_| ())
|
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(
|
async fn list_files_for_owner(
|
||||||
&self,
|
&self,
|
||||||
folder_id: Option<&str>,
|
folder_id: Option<&str>,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
) -> Result<Vec<File>, DomainError> {
|
) -> Result<Vec<File>, DomainError> {
|
||||||
let all = self.list_files(folder_id).await?;
|
let all = self.list_files(folder_id).await?;
|
||||||
|
let owner_str = owner_id.to_string();
|
||||||
Ok(all
|
Ok(all
|
||||||
.into_iter()
|
.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())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,15 +136,16 @@ pub trait FileReadPort: Send + Sync + 'static {
|
|||||||
async fn list_files_batch_for_owner(
|
async fn list_files_batch_for_owner(
|
||||||
&self,
|
&self,
|
||||||
folder_id: Option<&str>,
|
folder_id: Option<&str>,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
offset: i64,
|
offset: i64,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
) -> Result<Vec<File>, DomainError> {
|
) -> Result<Vec<File>, DomainError> {
|
||||||
// Default: filter in-memory (repos should override with SQL)
|
// Default: filter in-memory (repos should override with SQL)
|
||||||
let all = self.list_files_batch(folder_id, offset, limit).await?;
|
let all = self.list_files_batch(folder_id, offset, limit).await?;
|
||||||
|
let owner_str = owner_id.to_string();
|
||||||
Ok(all
|
Ok(all
|
||||||
.into_iter()
|
.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())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +178,7 @@ pub trait FileReadPort: Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
folder_id: Option<&str>,
|
folder_id: Option<&str>,
|
||||||
criteria: &SearchCriteriaDto,
|
criteria: &SearchCriteriaDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(Vec<File>, usize), DomainError>;
|
) -> Result<(Vec<File>, usize), DomainError>;
|
||||||
|
|
||||||
/// Search files recursively in a folder subtree using ltree.
|
/// Search files recursively in a folder subtree using ltree.
|
||||||
@@ -190,7 +193,7 @@ pub trait FileReadPort: Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
root_folder_id: Option<&str>,
|
root_folder_id: Option<&str>,
|
||||||
criteria: &SearchCriteriaDto,
|
criteria: &SearchCriteriaDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(Vec<File>, usize), DomainError> {
|
) -> Result<(Vec<File>, usize), DomainError> {
|
||||||
// Default: delegate to paginated search (non-recursive fallback)
|
// Default: delegate to paginated search (non-recursive fallback)
|
||||||
self.search_files_paginated(root_folder_id, criteria, user_id)
|
self.search_files_paginated(root_folder_id, criteria, user_id)
|
||||||
@@ -204,7 +207,7 @@ pub trait FileReadPort: Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
folder_id: Option<&str>,
|
folder_id: Option<&str>,
|
||||||
criteria: &SearchCriteriaDto,
|
criteria: &SearchCriteriaDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<usize, DomainError>;
|
) -> Result<usize, DomainError>;
|
||||||
|
|
||||||
/// Return up to `limit` files whose name contains `query` (case-insensitive).
|
/// 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
|
/// Secondary port for storage usage management
|
||||||
pub trait StorageUsagePort: Send + Sync + 'static {
|
pub trait StorageUsagePort: Send + Sync + 'static {
|
||||||
/// Updates storage usage statistics for a user
|
/// 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
|
/// Updates storage usage statistics for a user, looked up by username
|
||||||
async fn update_user_storage_usage_by_username(
|
async fn update_user_storage_usage_by_username(
|
||||||
@@ -375,12 +378,12 @@ pub trait StorageUsagePort: Send + Sync + 'static {
|
|||||||
/// descriptive message otherwise.
|
/// descriptive message otherwise.
|
||||||
async fn check_storage_quota(
|
async fn check_storage_quota(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
additional_bytes: u64,
|
additional_bytes: u64,
|
||||||
) -> Result<(), DomainError>;
|
) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Returns (used_bytes, quota_bytes) for a user.
|
/// 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
|
/// 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::application::dtos::trash_dto::TrashedItemDto;
|
||||||
use crate::common::errors::Result;
|
use crate::common::errors::Result;
|
||||||
|
|
||||||
/// Port for trash-related use cases
|
/// Port for trash-related use cases
|
||||||
pub trait TrashUseCase: Send + Sync {
|
pub trait TrashUseCase: Send + Sync {
|
||||||
/// List items in the user's trash
|
/// 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
|
/// 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
|
/// 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
|
/// 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
|
/// 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 std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::dtos::settings_dto::{
|
use crate::application::dtos::settings_dto::{
|
||||||
OidcSettingsDto, OidcTestResultDto, SaveOidcSettingsDto, TestOidcConnectionDto,
|
OidcSettingsDto, OidcTestResultDto, SaveOidcSettingsDto, TestOidcConnectionDto,
|
||||||
@@ -181,7 +182,7 @@ impl AdminSettingsService {
|
|||||||
pub async fn save_oidc_settings(
|
pub async fn save_oidc_settings(
|
||||||
&self,
|
&self,
|
||||||
dto: SaveOidcSettingsDto,
|
dto: SaveOidcSettingsDto,
|
||||||
updated_by: &str,
|
updated_by: Uuid,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
let cat = "oidc";
|
let cat = "oidc";
|
||||||
let by = Some(updated_by);
|
let by = Some(updated_by);
|
||||||
@@ -366,7 +367,7 @@ impl AdminSettingsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Mark the system as initialized after the first admin is created.
|
/// 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
|
self.settings_repo
|
||||||
.set(
|
.set(
|
||||||
"system_initialized",
|
"system_initialized",
|
||||||
@@ -384,7 +385,7 @@ impl AdminSettingsService {
|
|||||||
/// initialized (the caller "won" the race), or `Ok(false)` if another
|
/// initialized (the caller "won" the race), or `Ok(false)` if another
|
||||||
/// request already did it. This eliminates the race-condition window
|
/// request already did it. This eliminates the race-condition window
|
||||||
/// between `is_system_initialized()` and `mark_system_initialized()`.
|
/// 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
|
self.settings_repo
|
||||||
.try_claim_initialization(admin_user_id)
|
.try_claim_initialization(admin_user_id)
|
||||||
.await
|
.await
|
||||||
@@ -412,7 +413,7 @@ impl AdminSettingsService {
|
|||||||
pub async fn set_registration_enabled(
|
pub async fn set_registration_enabled(
|
||||||
&self,
|
&self,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
updated_by: &str,
|
updated_by: Uuid,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
self.settings_repo
|
self.settings_repo
|
||||||
.set(
|
.set(
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use moka::future::Cache;
|
|||||||
use rand_core::RngCore;
|
use rand_core::RngCore;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration as StdDuration;
|
use std::time::Duration as StdDuration;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// App password token length (32 random alphanumeric chars after prefix).
|
/// App password token length (32 random alphanumeric chars after prefix).
|
||||||
const TOKEN_LENGTH: usize = 32;
|
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.
|
/// Cached identity returned after a successful Basic Auth verification.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct CachedBasicAuthResult {
|
struct CachedBasicAuthResult {
|
||||||
user_id: String,
|
user_id: Uuid,
|
||||||
username: String,
|
username: String,
|
||||||
email: String,
|
email: String,
|
||||||
role: String,
|
role: String,
|
||||||
@@ -118,7 +119,7 @@ impl AppPasswordService {
|
|||||||
/// Returns the response DTO that includes the plain-text password (shown only once).
|
/// Returns the response DTO that includes the plain-text password (shown only once).
|
||||||
pub async fn create(
|
pub async fn create(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
request: CreateAppPasswordRequestDto,
|
request: CreateAppPasswordRequestDto,
|
||||||
) -> Result<AppPasswordCreatedResponseDto, DomainError> {
|
) -> Result<AppPasswordCreatedResponseDto, DomainError> {
|
||||||
// Validate label
|
// Validate label
|
||||||
@@ -147,7 +148,7 @@ impl AppPasswordService {
|
|||||||
|
|
||||||
// Create entity
|
// Create entity
|
||||||
let app_password = AppPassword::new(
|
let app_password = AppPassword::new(
|
||||||
user_id.to_string(),
|
user_id,
|
||||||
label.clone(),
|
label.clone(),
|
||||||
password_hash,
|
password_hash,
|
||||||
prefix.clone(),
|
prefix.clone(),
|
||||||
@@ -165,7 +166,7 @@ impl AppPasswordService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Ok(AppPasswordCreatedResponseDto {
|
Ok(AppPasswordCreatedResponseDto {
|
||||||
id: saved.id,
|
id: saved.id.to_string(),
|
||||||
label,
|
label,
|
||||||
password: plain_token,
|
password: plain_token,
|
||||||
username: username.clone(),
|
username: username.clone(),
|
||||||
@@ -200,7 +201,7 @@ impl AppPasswordService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// List all app passwords for a user (excludes plain-text passwords).
|
/// 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 passwords = self.repo.list_by_user(user_id).await?;
|
||||||
let total = passwords.len();
|
let total = passwords.len();
|
||||||
|
|
||||||
@@ -209,7 +210,7 @@ impl AppPasswordService {
|
|||||||
.map(|ap| {
|
.map(|ap| {
|
||||||
let is_active = ap.active && !ap.is_expired();
|
let is_active = ap.active && !ap.is_expired();
|
||||||
AppPasswordSummaryDto {
|
AppPasswordSummaryDto {
|
||||||
id: ap.id,
|
id: ap.id.to_string(),
|
||||||
label: ap.label,
|
label: ap.label,
|
||||||
prefix: format!("{}...", ap.prefix),
|
prefix: format!("{}...", ap.prefix),
|
||||||
scopes: ap.scopes,
|
scopes: ap.scopes,
|
||||||
@@ -234,8 +235,8 @@ impl AppPasswordService {
|
|||||||
/// up to `BASIC_AUTH_CACHE_TTL_SECS`).
|
/// up to `BASIC_AUTH_CACHE_TTL_SECS`).
|
||||||
pub async fn revoke(
|
pub async fn revoke(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
id: &str,
|
id: Uuid,
|
||||||
) -> Result<AppPasswordRevokeResponseDto, DomainError> {
|
) -> Result<AppPasswordRevokeResponseDto, DomainError> {
|
||||||
// Ownership enforced at SQL level (WHERE user_id = $2).
|
// Ownership enforced at SQL level (WHERE user_id = $2).
|
||||||
// The get_by_id pre-check gives a clear error message when
|
// 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
|
// Invalidate all cached auth entries for this user so the
|
||||||
// revocation is effective immediately.
|
// revocation is effective immediately.
|
||||||
let uid = user_id.to_string();
|
let uid = user_id;
|
||||||
self.auth_cache
|
self.auth_cache
|
||||||
.invalidate_entries_if(move |_key, val| val.user_id == uid)
|
.invalidate_entries_if(move |_key, val| val.user_id == uid)
|
||||||
.ok();
|
.ok();
|
||||||
@@ -282,7 +283,7 @@ impl AppPasswordService {
|
|||||||
&self,
|
&self,
|
||||||
username: &str,
|
username: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
) -> Result<(String, String, String, String), DomainError> {
|
) -> Result<(Uuid, String, String, String), DomainError> {
|
||||||
// ── 1. Compute cache key = blake3("username:password") ────────
|
// ── 1. Compute cache key = blake3("username:password") ────────
|
||||||
let cache_key: [u8; 32] =
|
let cache_key: [u8; 32] =
|
||||||
blake3::hash(format!("{}:{}", username, password).as_bytes()).into();
|
blake3::hash(format!("{}:{}", username, password).as_bytes()).into();
|
||||||
@@ -344,10 +345,10 @@ impl AppPasswordService {
|
|||||||
.verify_password(&verify_password, &ap.password_hash)
|
.verify_password(&verify_password, &ap.password_hash)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
let _ = self.repo.touch_last_used(&ap.id).await;
|
let _ = self.repo.touch_last_used(ap.id).await;
|
||||||
|
|
||||||
let result = CachedBasicAuthResult {
|
let result = CachedBasicAuthResult {
|
||||||
user_id: user.id().to_string(),
|
user_id: user.id(),
|
||||||
username: user.username().to_string(),
|
username: user.username().to_string(),
|
||||||
email: user.email().to_string(),
|
email: user.email().to_string(),
|
||||||
role: user.role().to_string(),
|
role: user.role().to_string(),
|
||||||
@@ -372,16 +373,16 @@ impl AppPasswordService {
|
|||||||
/// Returns `(id, plain_password)`.
|
/// Returns `(id, plain_password)`.
|
||||||
pub async fn create_nc(
|
pub async fn create_nc(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
label: &str,
|
label: &str,
|
||||||
) -> Result<(String, String), DomainError> {
|
) -> Result<(Uuid, String), DomainError> {
|
||||||
let password = generate_nc_app_password();
|
let password = generate_nc_app_password();
|
||||||
let normalized = nc_normalize_password(&password);
|
let normalized = nc_normalize_password(&password);
|
||||||
let prefix = nc_token_prefix(&normalized)?;
|
let prefix = nc_token_prefix(&normalized)?;
|
||||||
let hash = self.hasher.hash_password(&normalized).await?;
|
let hash = self.hasher.hash_password(&normalized).await?;
|
||||||
|
|
||||||
let ap = AppPassword::new(
|
let ap = AppPassword::new(
|
||||||
user_id.to_string(),
|
user_id,
|
||||||
label.to_string(),
|
label.to_string(),
|
||||||
hash,
|
hash,
|
||||||
prefix,
|
prefix,
|
||||||
@@ -397,7 +398,7 @@ impl AppPasswordService {
|
|||||||
/// Scoped to the authenticated user (fixes I3 — no global prefix search).
|
/// Scoped to the authenticated user (fixes I3 — no global prefix search).
|
||||||
pub async fn revoke_by_password(
|
pub async fn revoke_by_password(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
password: &str,
|
password: &str,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
let normalized = nc_normalize_password(password);
|
let normalized = nc_normalize_password(password);
|
||||||
@@ -417,10 +418,10 @@ impl AppPasswordService {
|
|||||||
.verify_password(&normalized, &ap.password_hash)
|
.verify_password(&normalized, &ap.password_hash)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
self.repo.revoke(&ap.id, user_id).await?;
|
self.repo.revoke(ap.id, user_id).await?;
|
||||||
|
|
||||||
// Invalidate cache for this user
|
// Invalidate cache for this user
|
||||||
let uid = user_id.to_string();
|
let uid = user_id;
|
||||||
self.auth_cache
|
self.auth_cache
|
||||||
.invalidate_entries_if(move |_key, val| val.user_id == uid)
|
.invalidate_entries_if(move |_key, val| val.user_id == uid)
|
||||||
.ok();
|
.ok();
|
||||||
@@ -432,12 +433,12 @@ impl AppPasswordService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// List app passwords for a user (simple summary for NC UI).
|
/// 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
|
self.repo.list_by_user(user_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete an app password by ID, scoped to the owning user.
|
/// 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?;
|
let deleted = self.repo.delete_by_user_and_id(id, user_id).await?;
|
||||||
if !deleted {
|
if !deleted {
|
||||||
return Err(DomainError::new(
|
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::oidc_service::OidcService;
|
||||||
use crate::infrastructure::services::password_hasher::Argon2PasswordHasher;
|
use crate::infrastructure::services::password_hasher::Argon2PasswordHasher;
|
||||||
use moka::sync::Cache;
|
use moka::sync::Cache;
|
||||||
|
use uuid::Uuid;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
@@ -33,7 +34,7 @@ pub enum OidcCallbackResult {
|
|||||||
/// app password and complete the NC login flow.
|
/// app password and complete the NC login flow.
|
||||||
NextcloudLogin {
|
NextcloudLogin {
|
||||||
nc_flow_token: String,
|
nc_flow_token: String,
|
||||||
user_id: String,
|
user_id: Uuid,
|
||||||
username: String,
|
username: String,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -411,7 +412,7 @@ impl AuthApplicationService {
|
|||||||
|
|
||||||
// Save session
|
// Save session
|
||||||
let session = Session::new(
|
let session = Session::new(
|
||||||
user.id().to_string(),
|
user.id(),
|
||||||
refresh_token.clone(),
|
refresh_token.clone(),
|
||||||
None, // IP (can be added from the HTTP layer)
|
None, // IP (can be added from the HTTP layer)
|
||||||
None, // User-Agent (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 {
|
Ok(crate::application::dtos::user_dto::CurrentUser {
|
||||||
id: user.id().to_string(),
|
id: user.id(),
|
||||||
username: user.username().to_string(),
|
username: user.username().to_string(),
|
||||||
email: user.email().to_string(),
|
email: user.email().to_string(),
|
||||||
role: user.role().to_string(),
|
role: user.role().to_string(),
|
||||||
@@ -514,7 +515,7 @@ impl AuthApplicationService {
|
|||||||
|
|
||||||
// Create new session
|
// Create new session
|
||||||
let new_session = Session::new(
|
let new_session = Session::new(
|
||||||
user.id().to_string(),
|
user.id(),
|
||||||
new_refresh_token.clone(),
|
new_refresh_token.clone(),
|
||||||
None,
|
None,
|
||||||
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
|
// Get session
|
||||||
let session = match self
|
let session = match self
|
||||||
.session_storage
|
.session_storage
|
||||||
@@ -559,7 +560,7 @@ impl AuthApplicationService {
|
|||||||
Ok(())
|
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
|
// Revoke all user sessions
|
||||||
let revoked_count = self
|
let revoked_count = self
|
||||||
.session_storage
|
.session_storage
|
||||||
@@ -571,7 +572,7 @@ impl AuthApplicationService {
|
|||||||
|
|
||||||
pub async fn change_password(
|
pub async fn change_password(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
dto: ChangePasswordDto,
|
dto: ChangePasswordDto,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
// Get user
|
// Get user
|
||||||
@@ -627,13 +628,13 @@ impl AuthApplicationService {
|
|||||||
Ok(())
|
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?;
|
let user = self.user_storage.get_user_by_id(user_id).await?;
|
||||||
Ok(UserDto::from(user))
|
Ok(UserDto::from(user))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alias for consistency with handler method
|
// 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
|
self.get_user(user_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -778,7 +779,7 @@ impl AuthApplicationService {
|
|||||||
/// Admin-only: reset a user's password.
|
/// Admin-only: reset a user's password.
|
||||||
pub async fn admin_reset_password(
|
pub async fn admin_reset_password(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
new_password: &str,
|
new_password: &str,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
// Block password reset for OIDC-provisioned users
|
// Block password reset for OIDC-provisioned users
|
||||||
@@ -812,13 +813,13 @@ impl AuthApplicationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get a single user by ID (for admin panel)
|
/// 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?;
|
let user = self.user_storage.get_user_by_id(user_id).await?;
|
||||||
Ok(UserDto::from(user))
|
Ok(UserDto::from(user))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete a user by ID (admin only)
|
/// 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
|
// Prevent deleting yourself
|
||||||
let user = self.user_storage.get_user_by_id(user_id).await?;
|
let user = self.user_storage.get_user_by_id(user_id).await?;
|
||||||
tracing::info!("Admin deleting user: {} ({})", user.username(), user_id);
|
tracing::info!("Admin deleting user: {} ({})", user.username(), user_id);
|
||||||
@@ -826,14 +827,14 @@ impl AuthApplicationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Activate or deactivate a user (admin only)
|
/// 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
|
self.user_storage
|
||||||
.set_user_active_status(user_id, active)
|
.set_user_active_status(user_id, active)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change user role (admin only)
|
/// 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" {
|
if role != "admin" && role != "user" {
|
||||||
return Err(DomainError::new(
|
return Err(DomainError::new(
|
||||||
ErrorKind::InvalidInput,
|
ErrorKind::InvalidInput,
|
||||||
@@ -847,7 +848,7 @@ impl AuthApplicationService {
|
|||||||
/// Update user's storage quota (admin only)
|
/// Update user's storage quota (admin only)
|
||||||
pub async fn update_user_quota(
|
pub async fn update_user_quota(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
quota_bytes: i64,
|
quota_bytes: i64,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
if quota_bytes < 0 {
|
if quota_bytes < 0 {
|
||||||
@@ -865,7 +866,7 @@ impl AuthApplicationService {
|
|||||||
/// Check if a user has enough quota for an upload of the given size
|
/// Check if a user has enough quota for an upload of the given size
|
||||||
pub async fn check_quota(
|
pub async fn check_quota(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
additional_bytes: i64,
|
additional_bytes: i64,
|
||||||
) -> Result<bool, DomainError> {
|
) -> Result<bool, DomainError> {
|
||||||
let user = self.user_storage.get_user_by_id(user_id).await?;
|
let user = self.user_storage.get_user_by_id(user_id).await?;
|
||||||
@@ -1209,7 +1210,7 @@ impl AuthApplicationService {
|
|||||||
);
|
);
|
||||||
return Ok(OidcCallbackResult::NextcloudLogin {
|
return Ok(OidcCallbackResult::NextcloudLogin {
|
||||||
nc_flow_token: nc_token,
|
nc_flow_token: nc_token,
|
||||||
user_id: user.id().to_string(),
|
user_id: user.id(),
|
||||||
username: user.username().to_string(),
|
username: user.username().to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1219,7 +1220,7 @@ impl AuthApplicationService {
|
|||||||
let refresh_token = self.token_service.generate_refresh_token();
|
let refresh_token = self.token_service.generate_refresh_token();
|
||||||
|
|
||||||
let session = Session::new(
|
let session = Session::new(
|
||||||
user.id().to_string(),
|
user.id(),
|
||||||
refresh_token.clone(),
|
refresh_token.clone(),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
@@ -1282,7 +1283,7 @@ impl AuthApplicationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Helper to create a personal folder for a new user
|
/// 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 {
|
if let Some(folder_service) = &self.folder_service {
|
||||||
let folder_name = format!("My Folder - {}", username);
|
let folder_name = format!("My Folder - {}", username);
|
||||||
match folder_service
|
match folder_service
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ use crate::application::services::folder_service::FolderService;
|
|||||||
use crate::application::services::trash_service::TrashService;
|
use crate::application::services::trash_service::TrashService;
|
||||||
use crate::common::config::AppConfig;
|
use crate::common::config::AppConfig;
|
||||||
use crate::common::errors::DomainError;
|
use crate::common::errors::DomainError;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Specific errors for batch operations
|
/// Specific errors for batch operations
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
@@ -117,7 +118,7 @@ impl BatchOperationService {
|
|||||||
&self,
|
&self,
|
||||||
file_ids: Vec<String>,
|
file_ids: Vec<String>,
|
||||||
target_folder_id: Option<String>,
|
target_folder_id: Option<String>,
|
||||||
caller_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<BatchResult<FileDto>, BatchOperationError> {
|
) -> Result<BatchResult<FileDto>, BatchOperationError> {
|
||||||
info!("Starting batch copy of {} files", file_ids.len());
|
info!("Starting batch copy of {} files", file_ids.len());
|
||||||
let start_time = std::time::Instant::now();
|
let start_time = std::time::Instant::now();
|
||||||
@@ -135,17 +136,15 @@ impl BatchOperationService {
|
|||||||
|
|
||||||
// Arc<str> avoids N heap-clones of the same string
|
// 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 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
|
// buffer_unordered materialises only max_concurrent futures at a time
|
||||||
let mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| {
|
let mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| {
|
||||||
let mgmt = self.file_management.clone();
|
let mgmt = self.file_management.clone();
|
||||||
let target_folder = target_folder.clone();
|
let target_folder = target_folder.clone();
|
||||||
let caller = caller.clone();
|
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
let copy_result = mgmt
|
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;
|
.await;
|
||||||
(file_id, copy_result)
|
(file_id, copy_result)
|
||||||
}
|
}
|
||||||
@@ -187,7 +186,7 @@ impl BatchOperationService {
|
|||||||
&self,
|
&self,
|
||||||
file_ids: Vec<String>,
|
file_ids: Vec<String>,
|
||||||
target_folder_id: Option<String>,
|
target_folder_id: Option<String>,
|
||||||
caller_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<BatchResult<FileDto>, BatchOperationError> {
|
) -> Result<BatchResult<FileDto>, BatchOperationError> {
|
||||||
info!("Starting batch move of {} files", file_ids.len());
|
info!("Starting batch move of {} files", file_ids.len());
|
||||||
let start_time = std::time::Instant::now();
|
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 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 mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| {
|
||||||
let mgmt = self.file_management.clone();
|
let mgmt = self.file_management.clone();
|
||||||
let target_folder = target_folder.clone();
|
let target_folder = target_folder.clone();
|
||||||
let caller = caller.clone();
|
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
let move_result = mgmt
|
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;
|
.await;
|
||||||
(file_id, move_result)
|
(file_id, move_result)
|
||||||
}
|
}
|
||||||
@@ -253,7 +250,7 @@ impl BatchOperationService {
|
|||||||
pub async fn delete_files(
|
pub async fn delete_files(
|
||||||
&self,
|
&self,
|
||||||
file_ids: Vec<String>,
|
file_ids: Vec<String>,
|
||||||
caller_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<BatchResult<String>, BatchOperationError> {
|
) -> Result<BatchResult<String>, BatchOperationError> {
|
||||||
info!("Starting batch deletion of {} files", file_ids.len());
|
info!("Starting batch deletion of {} files", file_ids.len());
|
||||||
let start_time = std::time::Instant::now();
|
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 mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| {
|
||||||
let mgmt = self.file_management.clone();
|
let mgmt = self.file_management.clone();
|
||||||
let caller = caller.clone();
|
|
||||||
|
|
||||||
async move {
|
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();
|
let id_for_result = file_id.clone();
|
||||||
(file_id, delete_result.map(|_| id_for_result))
|
(file_id, delete_result.map(|_| id_for_result))
|
||||||
}
|
}
|
||||||
@@ -317,7 +310,7 @@ impl BatchOperationService {
|
|||||||
pub async fn get_multiple_files(
|
pub async fn get_multiple_files(
|
||||||
&self,
|
&self,
|
||||||
file_ids: Vec<String>,
|
file_ids: Vec<String>,
|
||||||
caller_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<BatchResult<FileDto>, BatchOperationError> {
|
) -> Result<BatchResult<FileDto>, BatchOperationError> {
|
||||||
info!("Starting batch load of {} files", file_ids.len());
|
info!("Starting batch load of {} files", file_ids.len());
|
||||||
let start_time = std::time::Instant::now();
|
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 mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| {
|
||||||
let retrieval = self.file_retrieval.clone();
|
let retrieval = self.file_retrieval.clone();
|
||||||
let caller = caller.clone();
|
|
||||||
|
|
||||||
async move {
|
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)
|
(file_id, get_result)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
@@ -381,7 +370,7 @@ impl BatchOperationService {
|
|||||||
&self,
|
&self,
|
||||||
folder_ids: Vec<String>,
|
folder_ids: Vec<String>,
|
||||||
_recursive: bool,
|
_recursive: bool,
|
||||||
caller_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<BatchResult<String>, BatchOperationError> {
|
) -> Result<BatchResult<String>, BatchOperationError> {
|
||||||
info!("Starting batch deletion of {} folders", folder_ids.len());
|
info!("Starting batch deletion of {} folders", folder_ids.len());
|
||||||
let start_time = std::time::Instant::now();
|
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 mut operation_stream = stream::iter(folder_ids.into_iter().map(|folder_id| {
|
||||||
let folder_service = self.folder_service.clone();
|
let folder_service = self.folder_service.clone();
|
||||||
let caller = caller.clone();
|
|
||||||
|
|
||||||
async move {
|
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();
|
let id_for_result = folder_id.clone();
|
||||||
(folder_id, delete_result.map(|_| id_for_result))
|
(folder_id, delete_result.map(|_| id_for_result))
|
||||||
}
|
}
|
||||||
@@ -445,7 +429,7 @@ impl BatchOperationService {
|
|||||||
pub async fn trash_files(
|
pub async fn trash_files(
|
||||||
&self,
|
&self,
|
||||||
file_ids: Vec<String>,
|
file_ids: Vec<String>,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<BatchResult<String>, BatchOperationError> {
|
) -> Result<BatchResult<String>, BatchOperationError> {
|
||||||
let trash_service = self
|
let trash_service = self
|
||||||
.trash_service
|
.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 mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| {
|
||||||
let trash = trash_service.clone();
|
let trash = trash_service.clone();
|
||||||
let uid = uid.clone();
|
let uid = uid;
|
||||||
|
|
||||||
async move {
|
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();
|
let id_for_result = file_id.clone();
|
||||||
(file_id, trash_result.map(|_| id_for_result))
|
(file_id, trash_result.map(|_| id_for_result))
|
||||||
}
|
}
|
||||||
@@ -510,7 +494,7 @@ impl BatchOperationService {
|
|||||||
pub async fn trash_folders(
|
pub async fn trash_folders(
|
||||||
&self,
|
&self,
|
||||||
folder_ids: Vec<String>,
|
folder_ids: Vec<String>,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<BatchResult<String>, BatchOperationError> {
|
) -> Result<BatchResult<String>, BatchOperationError> {
|
||||||
let trash_service = self
|
let trash_service = self
|
||||||
.trash_service
|
.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 mut operation_stream = stream::iter(folder_ids.into_iter().map(|folder_id| {
|
||||||
let trash = trash_service.clone();
|
let trash = trash_service.clone();
|
||||||
let uid = uid.clone();
|
let uid = uid;
|
||||||
|
|
||||||
async move {
|
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();
|
let id_for_result = folder_id.clone();
|
||||||
(folder_id, trash_result.map(|_| id_for_result))
|
(folder_id, trash_result.map(|_| id_for_result))
|
||||||
}
|
}
|
||||||
@@ -576,7 +560,7 @@ impl BatchOperationService {
|
|||||||
&self,
|
&self,
|
||||||
folder_ids: Vec<String>,
|
folder_ids: Vec<String>,
|
||||||
target_folder_id: Option<String>,
|
target_folder_id: Option<String>,
|
||||||
caller_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<BatchResult<FolderDto>, BatchOperationError> {
|
) -> Result<BatchResult<FolderDto>, BatchOperationError> {
|
||||||
info!("Starting batch move of {} folders", folder_ids.len());
|
info!("Starting batch move of {} folders", folder_ids.len());
|
||||||
let start_time = std::time::Instant::now();
|
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 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 mut operation_stream = stream::iter(folder_ids.into_iter().map(|folder_id| {
|
||||||
let folder_service = self.folder_service.clone();
|
let folder_service = self.folder_service.clone();
|
||||||
let target = target.clone();
|
let target = target.clone();
|
||||||
let caller = caller.clone();
|
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
let dto = MoveFolderDto {
|
let dto = MoveFolderDto {
|
||||||
parent_id: target.map(|s| s.to_string()),
|
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)
|
(folder_id, move_result)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
@@ -645,7 +627,7 @@ impl BatchOperationService {
|
|||||||
&self,
|
&self,
|
||||||
file_ids: Vec<String>,
|
file_ids: Vec<String>,
|
||||||
folder_ids: Vec<String>,
|
folder_ids: Vec<String>,
|
||||||
caller_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<NamedTempFile, BatchOperationError> {
|
) -> Result<NamedTempFile, BatchOperationError> {
|
||||||
info!(
|
info!(
|
||||||
"Starting batch download: {} files, {} folders",
|
"Starting batch download: {} files, {} folders",
|
||||||
@@ -665,10 +647,10 @@ impl BatchOperationService {
|
|||||||
|
|
||||||
// ── Add individual files at the root of the ZIP ──────────────────
|
// ── Add individual files at the root of the ZIP ──────────────────
|
||||||
for file_id in &file_ids {
|
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) => {
|
Ok(file_dto) => {
|
||||||
if let Err(e) = self
|
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
|
.await
|
||||||
{
|
{
|
||||||
info!("Could not add file {} to ZIP: {}", file_dto.name, e);
|
info!("Could not add file {} to ZIP: {}", file_dto.name, e);
|
||||||
@@ -684,12 +666,12 @@ impl BatchOperationService {
|
|||||||
for folder_id in &folder_ids {
|
for folder_id in &folder_ids {
|
||||||
match self
|
match self
|
||||||
.folder_service
|
.folder_service
|
||||||
.get_folder_owned(folder_id, caller_id)
|
.get_folder_owned(folder_id, user_id)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(root_folder) => {
|
Ok(root_folder) => {
|
||||||
if let Err(e) = self
|
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
|
.await
|
||||||
{
|
{
|
||||||
info!("Could not add folder {} to ZIP: {}", root_folder.name, e);
|
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>>>,
|
zip: &mut ZipFileWriter<tokio_util::compat::Compat<BufWriter<tokio::fs::File>>>,
|
||||||
file_id: &str,
|
file_id: &str,
|
||||||
entry_name: &str,
|
entry_name: &str,
|
||||||
caller_id: &str,
|
caller_id: Uuid,
|
||||||
) -> Result<(), BatchOperationError> {
|
) -> Result<(), BatchOperationError> {
|
||||||
let entry = ZipEntryBuilder::new(entry_name.to_string().into(), Compression::Deflate);
|
let entry = ZipEntryBuilder::new(entry_name.to_string().into(), Compression::Deflate);
|
||||||
let mut writer = zip
|
let mut writer = zip
|
||||||
@@ -769,7 +751,7 @@ impl BatchOperationService {
|
|||||||
zip: &mut ZipFileWriter<tokio_util::compat::Compat<BufWriter<tokio::fs::File>>>,
|
zip: &mut ZipFileWriter<tokio_util::compat::Compat<BufWriter<tokio::fs::File>>>,
|
||||||
folder_id: &str,
|
folder_id: &str,
|
||||||
root_folder: &FolderDto,
|
root_folder: &FolderDto,
|
||||||
caller_id: &str,
|
caller_id: Uuid,
|
||||||
) -> Result<(), BatchOperationError> {
|
) -> Result<(), BatchOperationError> {
|
||||||
// Bulk-fetch folder tree (small — one entry per folder)
|
// Bulk-fetch folder tree (small — one entry per folder)
|
||||||
let all_folders = self
|
let all_folders = self
|
||||||
@@ -908,7 +890,7 @@ impl BatchOperationService {
|
|||||||
pub async fn create_folders(
|
pub async fn create_folders(
|
||||||
&self,
|
&self,
|
||||||
folders: Vec<(String, Option<String>)>, // (name, parent_id)
|
folders: Vec<(String, Option<String>)>, // (name, parent_id)
|
||||||
caller_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<BatchResult<FolderDto>, BatchOperationError> {
|
) -> Result<BatchResult<FolderDto>, BatchOperationError> {
|
||||||
info!("Starting batch creation of {} folders", folders.len());
|
info!("Starting batch creation of {} folders", folders.len());
|
||||||
let start_time = std::time::Instant::now();
|
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 mut operation_stream = stream::iter(folders.into_iter().map(|(name, parent_id)| {
|
||||||
let folder_service = self.folder_service.clone();
|
let folder_service = self.folder_service.clone();
|
||||||
let caller = caller.clone();
|
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
// If a parent is specified, verify the caller owns it
|
// If a parent is specified, verify the caller owns it
|
||||||
if let Some(ref pid) = parent_id
|
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);
|
let id = format!("{}:{}", name, pid);
|
||||||
return (id, Err(e));
|
return (id, Err(e));
|
||||||
@@ -983,7 +961,7 @@ impl BatchOperationService {
|
|||||||
pub async fn get_multiple_folders(
|
pub async fn get_multiple_folders(
|
||||||
&self,
|
&self,
|
||||||
folder_ids: Vec<String>,
|
folder_ids: Vec<String>,
|
||||||
caller_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<BatchResult<FolderDto>, BatchOperationError> {
|
) -> Result<BatchResult<FolderDto>, BatchOperationError> {
|
||||||
info!("Starting batch load of {} folders", folder_ids.len());
|
info!("Starting batch load of {} folders", folder_ids.len());
|
||||||
let start_time = std::time::Instant::now();
|
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 mut operation_stream = stream::iter(folder_ids.into_iter().map(|folder_id| {
|
||||||
let folder_service = self.folder_service.clone();
|
let folder_service = self.folder_service.clone();
|
||||||
let caller = caller.clone();
|
|
||||||
|
|
||||||
async move {
|
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)
|
(folder_id, get_result)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::dtos::calendar_dto::{
|
use crate::application::dtos::calendar_dto::{
|
||||||
CalendarDto, CalendarEventDto, CreateCalendarDto, CreateEventDto, CreateEventICalDto,
|
CalendarDto, CalendarEventDto, CreateCalendarDto, CreateEventDto, CreateEventICalDto,
|
||||||
@@ -23,7 +24,7 @@ impl CalendarUseCase for CalendarService {
|
|||||||
async fn create_calendar(
|
async fn create_calendar(
|
||||||
&self,
|
&self,
|
||||||
calendar: CreateCalendarDto,
|
calendar: CreateCalendarDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<CalendarDto, DomainError> {
|
) -> Result<CalendarDto, DomainError> {
|
||||||
self.calendar_storage
|
self.calendar_storage
|
||||||
.create_calendar(calendar, user_id)
|
.create_calendar(calendar, user_id)
|
||||||
@@ -34,7 +35,7 @@ impl CalendarUseCase for CalendarService {
|
|||||||
&self,
|
&self,
|
||||||
calendar_id: &str,
|
calendar_id: &str,
|
||||||
update: UpdateCalendarDto,
|
update: UpdateCalendarDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<CalendarDto, DomainError> {
|
) -> Result<CalendarDto, DomainError> {
|
||||||
let has_access = self
|
let has_access = self
|
||||||
.calendar_storage
|
.calendar_storage
|
||||||
@@ -52,7 +53,7 @@ impl CalendarUseCase for CalendarService {
|
|||||||
.await
|
.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
|
let has_access = self
|
||||||
.calendar_storage
|
.calendar_storage
|
||||||
.check_calendar_access(calendar_id, user_id)
|
.check_calendar_access(calendar_id, user_id)
|
||||||
@@ -70,7 +71,7 @@ impl CalendarUseCase for CalendarService {
|
|||||||
async fn get_calendar(
|
async fn get_calendar(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &str,
|
calendar_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<CalendarDto, DomainError> {
|
) -> Result<CalendarDto, DomainError> {
|
||||||
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
|
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
|
||||||
let has_access = self
|
let has_access = self
|
||||||
@@ -87,11 +88,11 @@ impl CalendarUseCase for CalendarService {
|
|||||||
Ok(calendar)
|
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
|
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
|
self.calendar_storage
|
||||||
.list_calendars_shared_with_user(user_id)
|
.list_calendars_shared_with_user(user_id)
|
||||||
.await
|
.await
|
||||||
@@ -112,12 +113,12 @@ impl CalendarUseCase for CalendarService {
|
|||||||
async fn share_calendar(
|
async fn share_calendar(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &str,
|
calendar_id: &str,
|
||||||
target_user_id: &str,
|
target_user_id: Uuid,
|
||||||
access_level: &str,
|
access_level: &str,
|
||||||
caller_user_id: &str,
|
caller_user_id: Uuid,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
|
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(
|
return Err(DomainError::new(
|
||||||
ErrorKind::AccessDenied,
|
ErrorKind::AccessDenied,
|
||||||
"Calendar",
|
"Calendar",
|
||||||
@@ -145,11 +146,11 @@ impl CalendarUseCase for CalendarService {
|
|||||||
async fn remove_calendar_sharing(
|
async fn remove_calendar_sharing(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &str,
|
calendar_id: &str,
|
||||||
target_user_id: &str,
|
target_user_id: Uuid,
|
||||||
caller_user_id: &str,
|
caller_user_id: Uuid,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
|
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(
|
return Err(DomainError::new(
|
||||||
ErrorKind::AccessDenied,
|
ErrorKind::AccessDenied,
|
||||||
"Calendar",
|
"Calendar",
|
||||||
@@ -164,10 +165,10 @@ impl CalendarUseCase for CalendarService {
|
|||||||
async fn get_calendar_shares(
|
async fn get_calendar_shares(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &str,
|
calendar_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<(String, String)>, DomainError> {
|
) -> Result<Vec<(String, String)>, DomainError> {
|
||||||
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
|
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(
|
return Err(DomainError::new(
|
||||||
ErrorKind::AccessDenied,
|
ErrorKind::AccessDenied,
|
||||||
"Calendar",
|
"Calendar",
|
||||||
@@ -180,7 +181,7 @@ impl CalendarUseCase for CalendarService {
|
|||||||
async fn create_event(
|
async fn create_event(
|
||||||
&self,
|
&self,
|
||||||
event: CreateEventDto,
|
event: CreateEventDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<CalendarEventDto, DomainError> {
|
) -> Result<CalendarEventDto, DomainError> {
|
||||||
let has_access = self
|
let has_access = self
|
||||||
.calendar_storage
|
.calendar_storage
|
||||||
@@ -199,7 +200,7 @@ impl CalendarUseCase for CalendarService {
|
|||||||
async fn create_event_from_ical(
|
async fn create_event_from_ical(
|
||||||
&self,
|
&self,
|
||||||
event: CreateEventICalDto,
|
event: CreateEventICalDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<CalendarEventDto, DomainError> {
|
) -> Result<CalendarEventDto, DomainError> {
|
||||||
let has_access = self
|
let has_access = self
|
||||||
.calendar_storage
|
.calendar_storage
|
||||||
@@ -219,7 +220,7 @@ impl CalendarUseCase for CalendarService {
|
|||||||
&self,
|
&self,
|
||||||
event_id: &str,
|
event_id: &str,
|
||||||
update: UpdateEventDto,
|
update: UpdateEventDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<CalendarEventDto, DomainError> {
|
) -> Result<CalendarEventDto, DomainError> {
|
||||||
let event = self.calendar_storage.get_event(event_id).await?;
|
let event = self.calendar_storage.get_event(event_id).await?;
|
||||||
let has_access = self
|
let has_access = self
|
||||||
@@ -236,7 +237,7 @@ impl CalendarUseCase for CalendarService {
|
|||||||
self.calendar_storage.update_event(event_id, update).await
|
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 event = self.calendar_storage.get_event(event_id).await?;
|
||||||
let has_access = self
|
let has_access = self
|
||||||
.calendar_storage
|
.calendar_storage
|
||||||
@@ -255,7 +256,7 @@ impl CalendarUseCase for CalendarService {
|
|||||||
async fn get_event(
|
async fn get_event(
|
||||||
&self,
|
&self,
|
||||||
event_id: &str,
|
event_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<CalendarEventDto, DomainError> {
|
) -> Result<CalendarEventDto, DomainError> {
|
||||||
let event = self.calendar_storage.get_event(event_id).await?;
|
let event = self.calendar_storage.get_event(event_id).await?;
|
||||||
let has_access = self
|
let has_access = self
|
||||||
@@ -281,7 +282,7 @@ impl CalendarUseCase for CalendarService {
|
|||||||
calendar_id: &str,
|
calendar_id: &str,
|
||||||
limit: Option<i64>,
|
limit: Option<i64>,
|
||||||
offset: Option<i64>,
|
offset: Option<i64>,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<CalendarEventDto>, DomainError> {
|
) -> Result<Vec<CalendarEventDto>, DomainError> {
|
||||||
let has_access = self
|
let has_access = self
|
||||||
.calendar_storage
|
.calendar_storage
|
||||||
@@ -313,7 +314,7 @@ impl CalendarUseCase for CalendarService {
|
|||||||
calendar_id: &str,
|
calendar_id: &str,
|
||||||
start: DateTime<Utc>,
|
start: DateTime<Utc>,
|
||||||
end: DateTime<Utc>,
|
end: DateTime<Utc>,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<CalendarEventDto>, DomainError> {
|
) -> Result<Vec<CalendarEventDto>, DomainError> {
|
||||||
let has_access = self
|
let has_access = self
|
||||||
.calendar_storage
|
.calendar_storage
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use sqlx::types::Uuid;
|
use uuid::Uuid;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::application::dtos::address_book_dto::{
|
use crate::application::dtos::address_book_dto::{
|
||||||
@@ -43,7 +43,7 @@ impl ContactService {
|
|||||||
async fn check_address_book_access(
|
async fn check_address_book_access(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &Uuid,
|
address_book_id: &Uuid,
|
||||||
user_id: &str,
|
user_id: &Uuid,
|
||||||
) -> Result<AddressBook, DomainError> {
|
) -> Result<AddressBook, DomainError> {
|
||||||
let address_book = self
|
let address_book = self
|
||||||
.address_book_repository
|
.address_book_repository
|
||||||
@@ -52,7 +52,7 @@ impl ContactService {
|
|||||||
.ok_or_else(|| DomainError::not_found("Address book", "not found"))?;
|
.ok_or_else(|| DomainError::not_found("Address book", "not found"))?;
|
||||||
|
|
||||||
// Check if user is owner
|
// 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);
|
return Ok(address_book);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ impl ContactService {
|
|||||||
.address_book_repository
|
.address_book_repository
|
||||||
.get_address_book_shares(address_book_id)
|
.get_address_book_shares(address_book_id)
|
||||||
.await?;
|
.await?;
|
||||||
if shares.iter().any(|(id, _)| id == user_id) {
|
if shares.iter().any(|(id, _)| id == &user_id.to_string()) {
|
||||||
return Ok(address_book);
|
return Ok(address_book);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ impl ContactService {
|
|||||||
async fn check_address_book_write_access(
|
async fn check_address_book_write_access(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &Uuid,
|
address_book_id: &Uuid,
|
||||||
user_id: &str,
|
user_id: &Uuid,
|
||||||
) -> Result<AddressBook, DomainError> {
|
) -> Result<AddressBook, DomainError> {
|
||||||
let address_book = self
|
let address_book = self
|
||||||
.address_book_repository
|
.address_book_repository
|
||||||
@@ -87,7 +87,7 @@ impl ContactService {
|
|||||||
.ok_or_else(|| DomainError::not_found("Address book", "not found"))?;
|
.ok_or_else(|| DomainError::not_found("Address book", "not found"))?;
|
||||||
|
|
||||||
// Check if user is owner
|
// 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);
|
return Ok(address_book);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ impl ContactService {
|
|||||||
.await?;
|
.await?;
|
||||||
if shares
|
if shares
|
||||||
.iter()
|
.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);
|
return Ok(address_book);
|
||||||
}
|
}
|
||||||
@@ -296,7 +296,8 @@ impl AddressBookUseCase for ContactService {
|
|||||||
|
|
||||||
// Check if user has write access to the address book
|
// Check if user has write access to the address book
|
||||||
let address_book = self
|
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?;
|
.await?;
|
||||||
|
|
||||||
// Apply updates
|
// Apply updates
|
||||||
@@ -327,7 +328,7 @@ impl AddressBookUseCase for ContactService {
|
|||||||
async fn delete_address_book(
|
async fn delete_address_book(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &str,
|
address_book_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
let id = Uuid::parse_str(address_book_id)
|
let id = Uuid::parse_str(address_book_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||||
@@ -339,7 +340,7 @@ impl AddressBookUseCase for ContactService {
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| DomainError::not_found("Address book", "not found"))?;
|
.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(
|
return Err(DomainError::unauthorized(
|
||||||
"Only the owner can delete an address book",
|
"Only the owner can delete an address book",
|
||||||
));
|
));
|
||||||
@@ -354,18 +355,18 @@ impl AddressBookUseCase for ContactService {
|
|||||||
async fn get_address_book(
|
async fn get_address_book(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &str,
|
address_book_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<AddressBookDto, DomainError> {
|
) -> Result<AddressBookDto, DomainError> {
|
||||||
let id = Uuid::parse_str(address_book_id)
|
let id = Uuid::parse_str(address_book_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
.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))
|
Ok(AddressBookDto::from(address_book))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_user_address_books(
|
async fn list_user_address_books(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<AddressBookDto>, DomainError> {
|
) -> Result<Vec<AddressBookDto>, DomainError> {
|
||||||
// Get address books owned by the user
|
// Get address books owned by the user
|
||||||
let owned_address_books = self
|
let owned_address_books = self
|
||||||
@@ -397,7 +398,7 @@ impl AddressBookUseCase for ContactService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for address_book in public_address_books {
|
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.contains_key(address_book.id())
|
||||||
{
|
{
|
||||||
address_book_map.insert(*address_book.id(), address_book);
|
address_book_map.insert(*address_book.id(), address_book);
|
||||||
@@ -428,7 +429,7 @@ impl AddressBookUseCase for ContactService {
|
|||||||
async fn share_address_book(
|
async fn share_address_book(
|
||||||
&self,
|
&self,
|
||||||
dto: ShareAddressBookDto,
|
dto: ShareAddressBookDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
let id = Uuid::parse_str(&dto.address_book_id)
|
let id = Uuid::parse_str(&dto.address_book_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||||
@@ -440,21 +441,23 @@ impl AddressBookUseCase for ContactService {
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| DomainError::not_found("Address book", "not found"))?;
|
.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(
|
return Err(DomainError::unauthorized(
|
||||||
"Only the owner can share an address book",
|
"Only the owner can share an address book",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't allow sharing with yourself
|
// 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(
|
return Err(DomainError::validation_error(
|
||||||
"Cannot share an address book with yourself",
|
"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
|
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?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -462,7 +465,7 @@ impl AddressBookUseCase for ContactService {
|
|||||||
async fn unshare_address_book(
|
async fn unshare_address_book(
|
||||||
&self,
|
&self,
|
||||||
dto: UnshareAddressBookDto,
|
dto: UnshareAddressBookDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
let id = Uuid::parse_str(&dto.address_book_id)
|
let id = Uuid::parse_str(&dto.address_book_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||||
@@ -474,14 +477,16 @@ impl AddressBookUseCase for ContactService {
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| DomainError::not_found("Address book", "not found"))?;
|
.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(
|
return Err(DomainError::unauthorized(
|
||||||
"Only the owner can unshare an address book",
|
"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
|
self.address_book_repository
|
||||||
.unshare_address_book(&id, &dto.user_id)
|
.unshare_address_book(&id, target_user_id)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -489,7 +494,7 @@ impl AddressBookUseCase for ContactService {
|
|||||||
async fn get_address_book_shares(
|
async fn get_address_book_shares(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &str,
|
address_book_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<(String, bool)>, DomainError> {
|
) -> Result<Vec<(String, bool)>, DomainError> {
|
||||||
let id = Uuid::parse_str(address_book_id)
|
let id = Uuid::parse_str(address_book_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||||
@@ -501,7 +506,7 @@ impl AddressBookUseCase for ContactService {
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| DomainError::not_found("Address book", "not found"))?;
|
.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(
|
return Err(DomainError::unauthorized(
|
||||||
"Only the owner can view address book shares",
|
"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"))?;
|
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||||
|
|
||||||
// Check if user has write access to the address book
|
// 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?;
|
.await?;
|
||||||
|
|
||||||
// Convert DTOs to domain entities
|
// Convert DTOs to domain entities
|
||||||
@@ -598,7 +604,8 @@ impl ContactUseCase for ContactService {
|
|||||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||||
|
|
||||||
// Check if user has write access to the address book
|
// 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?;
|
.await?;
|
||||||
|
|
||||||
// Parse vCard data
|
// Parse vCard data
|
||||||
@@ -633,7 +640,9 @@ impl ContactUseCase for ContactService {
|
|||||||
.ok_or_else(|| DomainError::not_found("Contact", "not found"))?;
|
.ok_or_else(|| DomainError::not_found("Contact", "not found"))?;
|
||||||
|
|
||||||
// Check if user has write access to the address book
|
// 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?;
|
.await?;
|
||||||
|
|
||||||
// Destructure contact into owned parts for updates
|
// Destructure contact into owned parts for updates
|
||||||
@@ -720,7 +729,7 @@ impl ContactUseCase for ContactService {
|
|||||||
Ok(ContactDto::from(result))
|
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)
|
let id = Uuid::parse_str(contact_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid contact ID format"))?;
|
.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"))?;
|
.ok_or_else(|| DomainError::not_found("Contact", "not found"))?;
|
||||||
|
|
||||||
// Check if user has write access to the address book
|
// 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?;
|
.await?;
|
||||||
|
|
||||||
// Delete the contact
|
// Delete the contact
|
||||||
@@ -743,7 +752,7 @@ impl ContactUseCase for ContactService {
|
|||||||
async fn get_contact(
|
async fn get_contact(
|
||||||
&self,
|
&self,
|
||||||
contact_id: &str,
|
contact_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<ContactDto, DomainError> {
|
) -> Result<ContactDto, DomainError> {
|
||||||
let id = Uuid::parse_str(contact_id)
|
let id = Uuid::parse_str(contact_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid contact ID format"))?;
|
.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"))?;
|
.ok_or_else(|| DomainError::not_found("Contact", "not found"))?;
|
||||||
|
|
||||||
// Check if user has access to the address book
|
// 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?;
|
.await?;
|
||||||
|
|
||||||
Ok(ContactDto::from(contact))
|
Ok(ContactDto::from(contact))
|
||||||
@@ -765,13 +774,13 @@ impl ContactUseCase for ContactService {
|
|||||||
async fn list_contacts(
|
async fn list_contacts(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &str,
|
address_book_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<ContactDto>, DomainError> {
|
) -> Result<Vec<ContactDto>, DomainError> {
|
||||||
let id = Uuid::parse_str(address_book_id)
|
let id = Uuid::parse_str(address_book_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||||
|
|
||||||
// Check if user has access to the address book
|
// 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
|
// Get contacts
|
||||||
let contacts = self
|
let contacts = self
|
||||||
@@ -787,13 +796,13 @@ impl ContactUseCase for ContactService {
|
|||||||
&self,
|
&self,
|
||||||
address_book_id: &str,
|
address_book_id: &str,
|
||||||
query: &str,
|
query: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<ContactDto>, DomainError> {
|
) -> Result<Vec<ContactDto>, DomainError> {
|
||||||
let id = Uuid::parse_str(address_book_id)
|
let id = Uuid::parse_str(address_book_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||||
|
|
||||||
// Check if user has access to the address book
|
// 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
|
// Search contacts
|
||||||
let contacts = self.contact_repository.search_contacts(&id, query).await?;
|
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"))?;
|
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||||
|
|
||||||
// Check if user has write access to the address book
|
// 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?;
|
.await?;
|
||||||
|
|
||||||
let group = ContactGroup::new(address_book_id, dto.name);
|
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"))?;
|
.ok_or_else(|| DomainError::not_found("Contact group", "not found"))?;
|
||||||
|
|
||||||
// Check if user has write access to the address book
|
// 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?;
|
.await?;
|
||||||
|
|
||||||
// Update the group
|
// Update the group
|
||||||
@@ -854,7 +865,7 @@ impl ContactUseCase for ContactService {
|
|||||||
Ok(ContactGroupDto::from(result))
|
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)
|
let id = Uuid::parse_str(group_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid group ID format"))?;
|
.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"))?;
|
.ok_or_else(|| DomainError::not_found("Contact group", "not found"))?;
|
||||||
|
|
||||||
// Check if user has write access to the address book
|
// 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?;
|
.await?;
|
||||||
|
|
||||||
// Delete the group
|
// Delete the group
|
||||||
@@ -877,7 +888,7 @@ impl ContactUseCase for ContactService {
|
|||||||
async fn get_group(
|
async fn get_group(
|
||||||
&self,
|
&self,
|
||||||
group_id: &str,
|
group_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<ContactGroupDto, DomainError> {
|
) -> Result<ContactGroupDto, DomainError> {
|
||||||
let id = Uuid::parse_str(group_id)
|
let id = Uuid::parse_str(group_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid group ID format"))?;
|
.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"))?;
|
.ok_or_else(|| DomainError::not_found("Contact group", "not found"))?;
|
||||||
|
|
||||||
// Check if user has access to the address book
|
// 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?;
|
.await?;
|
||||||
|
|
||||||
// Get the number of contacts in the group
|
// Get the number of contacts in the group
|
||||||
@@ -908,13 +919,13 @@ impl ContactUseCase for ContactService {
|
|||||||
async fn list_groups(
|
async fn list_groups(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &str,
|
address_book_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<ContactGroupDto>, DomainError> {
|
) -> Result<Vec<ContactGroupDto>, DomainError> {
|
||||||
let id = Uuid::parse_str(address_book_id)
|
let id = Uuid::parse_str(address_book_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||||
|
|
||||||
// Check if user has access to the address book
|
// 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
|
// Get groups
|
||||||
let groups = self
|
let groups = self
|
||||||
@@ -929,7 +940,7 @@ impl ContactUseCase for ContactService {
|
|||||||
async fn add_contact_to_group(
|
async fn add_contact_to_group(
|
||||||
&self,
|
&self,
|
||||||
dto: GroupMembershipDto,
|
dto: GroupMembershipDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
let group_id = Uuid::parse_str(&dto.group_id)
|
let group_id = Uuid::parse_str(&dto.group_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid group ID format"))?;
|
.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"))?;
|
.ok_or_else(|| DomainError::not_found("Contact group", "not found"))?;
|
||||||
|
|
||||||
// Check if user has write access to the address book
|
// 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?;
|
.await?;
|
||||||
|
|
||||||
// Add contact to group
|
// Add contact to group
|
||||||
@@ -958,7 +969,7 @@ impl ContactUseCase for ContactService {
|
|||||||
async fn remove_contact_from_group(
|
async fn remove_contact_from_group(
|
||||||
&self,
|
&self,
|
||||||
dto: GroupMembershipDto,
|
dto: GroupMembershipDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
let group_id = Uuid::parse_str(&dto.group_id)
|
let group_id = Uuid::parse_str(&dto.group_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid group ID format"))?;
|
.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"))?;
|
.ok_or_else(|| DomainError::not_found("Contact group", "not found"))?;
|
||||||
|
|
||||||
// Check if user has write access to the address book
|
// 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?;
|
.await?;
|
||||||
|
|
||||||
// Remove contact from group
|
// Remove contact from group
|
||||||
@@ -987,7 +998,7 @@ impl ContactUseCase for ContactService {
|
|||||||
async fn list_contacts_in_group(
|
async fn list_contacts_in_group(
|
||||||
&self,
|
&self,
|
||||||
group_id: &str,
|
group_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<ContactDto>, DomainError> {
|
) -> Result<Vec<ContactDto>, DomainError> {
|
||||||
let id = Uuid::parse_str(group_id)
|
let id = Uuid::parse_str(group_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid group ID format"))?;
|
.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"))?;
|
.ok_or_else(|| DomainError::not_found("Contact group", "not found"))?;
|
||||||
|
|
||||||
// Check if user has access to the address book
|
// 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?;
|
.await?;
|
||||||
|
|
||||||
// Get contacts in group
|
// Get contacts in group
|
||||||
@@ -1016,7 +1027,7 @@ impl ContactUseCase for ContactService {
|
|||||||
async fn list_groups_for_contact(
|
async fn list_groups_for_contact(
|
||||||
&self,
|
&self,
|
||||||
contact_id: &str,
|
contact_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<ContactGroupDto>, DomainError> {
|
) -> Result<Vec<ContactGroupDto>, DomainError> {
|
||||||
let id = Uuid::parse_str(contact_id)
|
let id = Uuid::parse_str(contact_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid contact ID format"))?;
|
.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"))?;
|
.ok_or_else(|| DomainError::not_found("Contact", "not found"))?;
|
||||||
|
|
||||||
// Check if user has access to the address book
|
// 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?;
|
.await?;
|
||||||
|
|
||||||
// Get groups for contact
|
// Get groups for contact
|
||||||
@@ -1045,7 +1056,7 @@ impl ContactUseCase for ContactService {
|
|||||||
async fn get_contact_vcard(
|
async fn get_contact_vcard(
|
||||||
&self,
|
&self,
|
||||||
contact_id: &str,
|
contact_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<String, DomainError> {
|
) -> Result<String, DomainError> {
|
||||||
let id = Uuid::parse_str(contact_id)
|
let id = Uuid::parse_str(contact_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid contact ID format"))?;
|
.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"))?;
|
.ok_or_else(|| DomainError::not_found("Contact", "not found"))?;
|
||||||
|
|
||||||
// Check if user has access to the address book
|
// 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?;
|
.await?;
|
||||||
|
|
||||||
// Return the vCard data
|
// Return the vCard data
|
||||||
@@ -1068,13 +1079,13 @@ impl ContactUseCase for ContactService {
|
|||||||
async fn get_contacts_as_vcards(
|
async fn get_contacts_as_vcards(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &str,
|
address_book_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<(String, String)>, DomainError> {
|
) -> Result<Vec<(String, String)>, DomainError> {
|
||||||
let id = Uuid::parse_str(address_book_id)
|
let id = Uuid::parse_str(address_book_id)
|
||||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||||
|
|
||||||
// Check if user has access to the address book
|
// 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
|
// Get all contacts in the address book
|
||||||
let contacts = self
|
let contacts = self
|
||||||
@@ -1130,6 +1141,8 @@ impl StorageUseCase for ContactService {
|
|||||||
let user_id = params["user_id"]
|
let user_id = params["user_id"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
.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?;
|
self.delete_address_book(address_book_id, user_id).await?;
|
||||||
Ok(serde_json::Value::Null)
|
Ok(serde_json::Value::Null)
|
||||||
@@ -1142,6 +1155,8 @@ impl StorageUseCase for ContactService {
|
|||||||
let user_id = params["user_id"]
|
let user_id = params["user_id"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
.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?;
|
let result = self.get_address_book(address_book_id, user_id).await?;
|
||||||
Ok(serde_json::to_value(result).unwrap())
|
Ok(serde_json::to_value(result).unwrap())
|
||||||
@@ -1150,6 +1165,8 @@ impl StorageUseCase for ContactService {
|
|||||||
let user_id = params["user_id"]
|
let user_id = params["user_id"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
.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?;
|
let result = self.list_user_address_books(user_id).await?;
|
||||||
Ok(serde_json::to_value(result).unwrap())
|
Ok(serde_json::to_value(result).unwrap())
|
||||||
@@ -1167,6 +1184,8 @@ impl StorageUseCase for ContactService {
|
|||||||
let user_id = params["user_id"]
|
let user_id = params["user_id"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
.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?;
|
self.share_address_book(dto, user_id).await?;
|
||||||
Ok(serde_json::Value::Null)
|
Ok(serde_json::Value::Null)
|
||||||
@@ -1180,6 +1199,8 @@ impl StorageUseCase for ContactService {
|
|||||||
let user_id = params["user_id"]
|
let user_id = params["user_id"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
.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?;
|
self.unshare_address_book(dto, user_id).await?;
|
||||||
Ok(serde_json::Value::Null)
|
Ok(serde_json::Value::Null)
|
||||||
@@ -1192,6 +1213,8 @@ impl StorageUseCase for ContactService {
|
|||||||
let user_id = params["user_id"]
|
let user_id = params["user_id"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
.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
|
let result = self
|
||||||
.get_address_book_shares(address_book_id, user_id)
|
.get_address_book_shares(address_book_id, user_id)
|
||||||
@@ -1239,6 +1262,8 @@ impl StorageUseCase for ContactService {
|
|||||||
let user_id = params["user_id"]
|
let user_id = params["user_id"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
.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?;
|
self.delete_contact(contact_id, user_id).await?;
|
||||||
Ok(serde_json::Value::Null)
|
Ok(serde_json::Value::Null)
|
||||||
@@ -1251,6 +1276,8 @@ impl StorageUseCase for ContactService {
|
|||||||
let user_id = params["user_id"]
|
let user_id = params["user_id"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
.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?;
|
let result = self.get_contact(contact_id, user_id).await?;
|
||||||
Ok(serde_json::to_value(result).unwrap())
|
Ok(serde_json::to_value(result).unwrap())
|
||||||
@@ -1263,6 +1290,8 @@ impl StorageUseCase for ContactService {
|
|||||||
let user_id = params["user_id"]
|
let user_id = params["user_id"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
.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?;
|
let result = self.list_contacts(address_book_id, user_id).await?;
|
||||||
Ok(serde_json::to_value(result).unwrap())
|
Ok(serde_json::to_value(result).unwrap())
|
||||||
@@ -1279,6 +1308,8 @@ impl StorageUseCase for ContactService {
|
|||||||
let user_id = params["user_id"]
|
let user_id = params["user_id"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
.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
|
let result = self
|
||||||
.search_contacts(address_book_id, query, user_id)
|
.search_contacts(address_book_id, query, user_id)
|
||||||
@@ -1317,6 +1348,8 @@ impl StorageUseCase for ContactService {
|
|||||||
let user_id = params["user_id"]
|
let user_id = params["user_id"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
.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?;
|
self.delete_group(group_id, user_id).await?;
|
||||||
Ok(serde_json::Value::Null)
|
Ok(serde_json::Value::Null)
|
||||||
@@ -1329,6 +1362,8 @@ impl StorageUseCase for ContactService {
|
|||||||
let user_id = params["user_id"]
|
let user_id = params["user_id"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
.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?;
|
let result = self.get_group(group_id, user_id).await?;
|
||||||
Ok(serde_json::to_value(result).unwrap())
|
Ok(serde_json::to_value(result).unwrap())
|
||||||
@@ -1341,6 +1376,8 @@ impl StorageUseCase for ContactService {
|
|||||||
let user_id = params["user_id"]
|
let user_id = params["user_id"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
.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?;
|
let result = self.list_groups(address_book_id, user_id).await?;
|
||||||
Ok(serde_json::to_value(result).unwrap())
|
Ok(serde_json::to_value(result).unwrap())
|
||||||
@@ -1356,6 +1393,8 @@ impl StorageUseCase for ContactService {
|
|||||||
let user_id = params["user_id"]
|
let user_id = params["user_id"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
.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?;
|
self.add_contact_to_group(dto, user_id).await?;
|
||||||
Ok(serde_json::Value::Null)
|
Ok(serde_json::Value::Null)
|
||||||
@@ -1369,6 +1408,8 @@ impl StorageUseCase for ContactService {
|
|||||||
let user_id = params["user_id"]
|
let user_id = params["user_id"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
.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?;
|
self.remove_contact_from_group(dto, user_id).await?;
|
||||||
Ok(serde_json::Value::Null)
|
Ok(serde_json::Value::Null)
|
||||||
@@ -1381,6 +1422,8 @@ impl StorageUseCase for ContactService {
|
|||||||
let user_id = params["user_id"]
|
let user_id = params["user_id"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
.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?;
|
let result = self.list_contacts_in_group(group_id, user_id).await?;
|
||||||
Ok(serde_json::to_value(result).unwrap())
|
Ok(serde_json::to_value(result).unwrap())
|
||||||
@@ -1393,6 +1436,8 @@ impl StorageUseCase for ContactService {
|
|||||||
let user_id = params["user_id"]
|
let user_id = params["user_id"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
.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?;
|
let result = self.list_groups_for_contact(contact_id, user_id).await?;
|
||||||
Ok(serde_json::to_value(result).unwrap())
|
Ok(serde_json::to_value(result).unwrap())
|
||||||
@@ -1407,6 +1452,8 @@ impl StorageUseCase for ContactService {
|
|||||||
let user_id = params["user_id"]
|
let user_id = params["user_id"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
.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?;
|
let result = self.get_contact_vcard(contact_id, user_id).await?;
|
||||||
Ok(serde_json::to_value(result).unwrap())
|
Ok(serde_json::to_value(result).unwrap())
|
||||||
@@ -1419,6 +1466,8 @@ impl StorageUseCase for ContactService {
|
|||||||
let user_id = params["user_id"]
|
let user_id = params["user_id"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
.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
|
let result = self
|
||||||
.get_contacts_as_vcards(address_book_id, user_id)
|
.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::SessionPgRepository;
|
||||||
use crate::infrastructure::repositories::pg::UserPgRepository;
|
use crate::infrastructure::repositories::pg::UserPgRepository;
|
||||||
use crate::infrastructure::services::jwt_service::JwtTokenService;
|
use crate::infrastructure::services::jwt_service::JwtTokenService;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Default device code lifetime: 15 minutes (RFC 8628 recommends 5-30 min).
|
/// Default device code lifetime: 15 minutes (RFC 8628 recommends 5-30 min).
|
||||||
const DEVICE_CODE_LIFETIME_SECS: i64 = 900;
|
const DEVICE_CODE_LIFETIME_SECS: i64 = 900;
|
||||||
@@ -155,7 +156,7 @@ impl DeviceAuthService {
|
|||||||
///
|
///
|
||||||
/// * `user_code` — the code from the verification page
|
/// * `user_code` — the code from the verification page
|
||||||
/// * `user_id` — the authenticated user's ID (from session/JWT)
|
/// * `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 normalized = user_code.trim().to_uppercase().replace(' ', "");
|
||||||
|
|
||||||
let mut dc = self
|
let mut dc = self
|
||||||
@@ -180,7 +181,7 @@ impl DeviceAuthService {
|
|||||||
|
|
||||||
// Persist refresh token as a session
|
// Persist refresh token as a session
|
||||||
let session = Session::new(
|
let session = Session::new(
|
||||||
user_id.to_string(),
|
user_id,
|
||||||
refresh_token.clone(),
|
refresh_token.clone(),
|
||||||
None, // ip_address
|
None, // ip_address
|
||||||
Some(format!("device:{}", dc.client_name())), // user_agent
|
Some(format!("device:{}", dc.client_name())), // user_agent
|
||||||
@@ -189,7 +190,7 @@ impl DeviceAuthService {
|
|||||||
self.session_storage.create_session(session).await?;
|
self.session_storage.create_session(session).await?;
|
||||||
|
|
||||||
// Store tokens on the device code entity
|
// 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?;
|
self.device_code_storage.update_device_code(dc).await?;
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
@@ -297,7 +298,7 @@ impl DeviceAuthService {
|
|||||||
|
|
||||||
pub async fn list_user_devices(
|
pub async fn list_user_devices(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<DeviceInfoDto>, DomainError> {
|
) -> Result<Vec<DeviceInfoDto>, DomainError> {
|
||||||
let codes = self.device_code_storage.list_by_user(user_id).await?;
|
let codes = self.device_code_storage.list_by_user(user_id).await?;
|
||||||
Ok(codes
|
Ok(codes
|
||||||
@@ -318,7 +319,7 @@ impl DeviceAuthService {
|
|||||||
// 8. Revoke — user revokes a device authorization
|
// 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
|
// Verify ownership before deleting
|
||||||
let devices = self.device_code_storage.list_by_user(user_id).await?;
|
let devices = self.device_code_storage.list_by_user(user_id).await?;
|
||||||
let found = devices.iter().any(|d| d.id() == device_id);
|
let found = devices.iter().any(|d| d.id() == device_id);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::collections::HashSet;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::dtos::favorites_dto::{
|
use crate::application::dtos::favorites_dto::{
|
||||||
BatchFavoritesResult, BatchFavoritesStats, FavoriteItemDto,
|
BatchFavoritesResult, BatchFavoritesStats, FavoriteItemDto,
|
||||||
@@ -27,7 +28,7 @@ impl FavoritesService {
|
|||||||
|
|
||||||
impl FavoritesUseCase for FavoritesService {
|
impl FavoritesUseCase for FavoritesService {
|
||||||
/// Get all favorites for a user
|
/// 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);
|
info!("Getting favorites for user: {}", user_id);
|
||||||
let favorites = self.repo.get_favorites(user_id).await?;
|
let favorites = self.repo.get_favorites(user_id).await?;
|
||||||
info!(
|
info!(
|
||||||
@@ -39,7 +40,7 @@ impl FavoritesUseCase for FavoritesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Add an item to user's favorites
|
/// 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!(
|
info!(
|
||||||
"Adding {} '{}' to favorites for user {}",
|
"Adding {} '{}' to favorites for user {}",
|
||||||
item_type, item_id, user_id
|
item_type, item_id, user_id
|
||||||
@@ -64,7 +65,7 @@ impl FavoritesUseCase for FavoritesService {
|
|||||||
/// Remove an item from user's favorites
|
/// Remove an item from user's favorites
|
||||||
async fn remove_from_favorites(
|
async fn remove_from_favorites(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
item_id: &str,
|
item_id: &str,
|
||||||
item_type: &str,
|
item_type: &str,
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
@@ -91,7 +92,7 @@ impl FavoritesUseCase for FavoritesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if an item is in user's favorites
|
/// 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!(
|
info!(
|
||||||
"Checking if {} '{}' is favorite for user {}",
|
"Checking if {} '{}' is favorite for user {}",
|
||||||
item_type, item_id, user_id
|
item_type, item_id, user_id
|
||||||
@@ -101,7 +102,7 @@ impl FavoritesUseCase for FavoritesService {
|
|||||||
|
|
||||||
async fn batch_add_to_favorites(
|
async fn batch_add_to_favorites(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
items: &[(String, String)],
|
items: &[(String, String)],
|
||||||
) -> Result<BatchFavoritesResult> {
|
) -> Result<BatchFavoritesResult> {
|
||||||
info!(
|
info!(
|
||||||
@@ -148,7 +149,7 @@ impl FavoritesUseCase for FavoritesService {
|
|||||||
|
|
||||||
async fn batch_check_favorites(
|
async fn batch_check_favorites(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
item_ids: &[(&str, &str)],
|
item_ids: &[(&str, &str)],
|
||||||
) -> Result<HashSet<String>> {
|
) -> Result<HashSet<String>> {
|
||||||
self.repo.batch_check_favorites(user_id, item_ids).await
|
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_read_repository::FileBlobReadRepository;
|
||||||
use crate::infrastructure::repositories::pg::file_blob_write_repository::FileBlobWriteRepository;
|
use crate::infrastructure::repositories::pg::file_blob_write_repository::FileBlobWriteRepository;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Service for file management operations (move, delete).
|
/// Service for file management operations (move, delete).
|
||||||
///
|
///
|
||||||
@@ -46,7 +47,7 @@ impl FileManagementService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Verifies ownership via the read repository.
|
/// 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 {
|
if let Some(read) = &self.file_read {
|
||||||
read.verify_file_owner(file_id, caller_id).await
|
read.verify_file_owner(file_id, caller_id).await
|
||||||
} else {
|
} else {
|
||||||
@@ -92,7 +93,7 @@ impl FileManagementUseCase for FileManagementService {
|
|||||||
async fn move_file_owned(
|
async fn move_file_owned(
|
||||||
&self,
|
&self,
|
||||||
file_id: &str,
|
file_id: &str,
|
||||||
caller_id: &str,
|
caller_id: Uuid,
|
||||||
folder_id: Option<String>,
|
folder_id: Option<String>,
|
||||||
) -> Result<FileDto, DomainError> {
|
) -> Result<FileDto, DomainError> {
|
||||||
self.verify_owner(file_id, caller_id).await?;
|
self.verify_owner(file_id, caller_id).await?;
|
||||||
@@ -131,7 +132,7 @@ impl FileManagementUseCase for FileManagementService {
|
|||||||
async fn copy_file_owned(
|
async fn copy_file_owned(
|
||||||
&self,
|
&self,
|
||||||
file_id: &str,
|
file_id: &str,
|
||||||
caller_id: &str,
|
caller_id: Uuid,
|
||||||
target_folder_id: Option<String>,
|
target_folder_id: Option<String>,
|
||||||
) -> Result<FileDto, DomainError> {
|
) -> Result<FileDto, DomainError> {
|
||||||
self.verify_owner(file_id, caller_id).await?;
|
self.verify_owner(file_id, caller_id).await?;
|
||||||
@@ -162,7 +163,7 @@ impl FileManagementUseCase for FileManagementService {
|
|||||||
async fn rename_file_owned(
|
async fn rename_file_owned(
|
||||||
&self,
|
&self,
|
||||||
file_id: &str,
|
file_id: &str,
|
||||||
caller_id: &str,
|
caller_id: Uuid,
|
||||||
new_name: &str,
|
new_name: &str,
|
||||||
) -> Result<FileDto, DomainError> {
|
) -> Result<FileDto, DomainError> {
|
||||||
self.verify_owner(file_id, caller_id).await?;
|
self.verify_owner(file_id, caller_id).await?;
|
||||||
@@ -173,7 +174,7 @@ impl FileManagementUseCase for FileManagementService {
|
|||||||
self.file_repository.delete_file(id).await
|
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.verify_owner(id, caller_id).await?;
|
||||||
self.delete_file(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.
|
/// `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)
|
/// 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.
|
/// 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)
|
// Step 1: Try trash (soft delete — file row stays, blob stays referenced)
|
||||||
if let Some(trash) = &self.trash_service {
|
if let Some(trash) = &self.trash_service {
|
||||||
info!("Moving file to trash: {}", id);
|
info!("Moving file to trash: {}", id);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use crate::infrastructure::services::image_transcode_service::{
|
|||||||
ImageTranscodeService, OutputFormat,
|
ImageTranscodeService, OutputFormat,
|
||||||
};
|
};
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Threshold below which files are served from RAM cache (10 MB).
|
/// Threshold below which files are served from RAM cache (10 MB).
|
||||||
const CACHE_THRESHOLD: u64 = 10 * 1024 * 1024;
|
const CACHE_THRESHOLD: u64 = 10 * 1024 * 1024;
|
||||||
@@ -201,7 +202,7 @@ impl FileRetrievalUseCase for FileRetrievalService {
|
|||||||
Ok(FileDto::from(file))
|
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?;
|
let file = self.file_read.get_file_for_owner(id, caller_id).await?;
|
||||||
Ok(FileDto::from(file))
|
Ok(FileDto::from(file))
|
||||||
}
|
}
|
||||||
@@ -226,7 +227,7 @@ impl FileRetrievalUseCase for FileRetrievalService {
|
|||||||
async fn list_files_owned(
|
async fn list_files_owned(
|
||||||
&self,
|
&self,
|
||||||
folder_id: Option<&str>,
|
folder_id: Option<&str>,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
) -> Result<Vec<FileDto>, DomainError> {
|
) -> Result<Vec<FileDto>, DomainError> {
|
||||||
let files = self
|
let files = self
|
||||||
.file_read
|
.file_read
|
||||||
@@ -245,7 +246,7 @@ impl FileRetrievalUseCase for FileRetrievalService {
|
|||||||
async fn get_file_stream_owned(
|
async fn get_file_stream_owned(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
caller_id: &str,
|
caller_id: Uuid,
|
||||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
|
) -> 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.verify_file_owner(id, caller_id).await?;
|
||||||
self.file_read.get_file_stream(id).await
|
self.file_read.get_file_stream(id).await
|
||||||
@@ -267,7 +268,7 @@ impl FileRetrievalUseCase for FileRetrievalService {
|
|||||||
async fn get_file_optimized_owned(
|
async fn get_file_optimized_owned(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
caller_id: &str,
|
caller_id: Uuid,
|
||||||
accept_webp: bool,
|
accept_webp: bool,
|
||||||
prefer_original: bool,
|
prefer_original: bool,
|
||||||
) -> Result<(FileDto, OptimizedFileContent), DomainError> {
|
) -> Result<(FileDto, OptimizedFileContent), DomainError> {
|
||||||
@@ -302,7 +303,7 @@ impl FileRetrievalUseCase for FileRetrievalService {
|
|||||||
async fn get_file_range_stream_owned(
|
async fn get_file_range_stream_owned(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
caller_id: &str,
|
caller_id: Uuid,
|
||||||
start: u64,
|
start: u64,
|
||||||
end: Option<u64>,
|
end: Option<u64>,
|
||||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
|
) -> 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(
|
async fn list_files_batch_for_owner(
|
||||||
&self,
|
&self,
|
||||||
folder_id: Option<&str>,
|
folder_id: Option<&str>,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
offset: i64,
|
offset: i64,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
) -> Result<Vec<FileDto>, DomainError> {
|
) -> Result<Vec<FileDto>, DomainError> {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use crate::domain::repositories::folder_repository::FolderRepository;
|
|||||||
use crate::domain::services::path_service::StoragePath;
|
use crate::domain::services::path_service::StoragePath;
|
||||||
use crate::infrastructure::repositories::pg::folder_db_repository::FolderDbRepository;
|
use crate::infrastructure::repositories::pg::folder_db_repository::FolderDbRepository;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Implementation of the use case for folder operations
|
/// Implementation of the use case for folder operations
|
||||||
pub struct FolderService {
|
pub struct FolderService {
|
||||||
@@ -35,7 +36,7 @@ impl FolderService {
|
|||||||
async fn get_folder_owned(
|
async fn get_folder_owned(
|
||||||
&self,
|
&self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
_caller_id: &str,
|
_caller_id: Uuid,
|
||||||
) -> Result<FolderDto, DomainError> {
|
) -> Result<FolderDto, DomainError> {
|
||||||
Ok(FolderDto::empty())
|
Ok(FolderDto::empty())
|
||||||
}
|
}
|
||||||
@@ -54,7 +55,7 @@ impl FolderService {
|
|||||||
async fn list_folders_for_owner(
|
async fn list_folders_for_owner(
|
||||||
&self,
|
&self,
|
||||||
_parent_id: Option<&str>,
|
_parent_id: Option<&str>,
|
||||||
_owner_id: &str,
|
_owner_id: Uuid,
|
||||||
) -> Result<Vec<FolderDto>, DomainError> {
|
) -> Result<Vec<FolderDto>, DomainError> {
|
||||||
Ok(vec![])
|
Ok(vec![])
|
||||||
}
|
}
|
||||||
@@ -80,7 +81,7 @@ impl FolderService {
|
|||||||
async fn list_folders_for_owner_paginated(
|
async fn list_folders_for_owner_paginated(
|
||||||
&self,
|
&self,
|
||||||
_parent_id: Option<&str>,
|
_parent_id: Option<&str>,
|
||||||
_owner_id: &str,
|
_owner_id: Uuid,
|
||||||
_pagination: &crate::application::dtos::pagination::PaginationRequestDto,
|
_pagination: &crate::application::dtos::pagination::PaginationRequestDto,
|
||||||
) -> Result<
|
) -> Result<
|
||||||
crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>,
|
crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>,
|
||||||
@@ -100,7 +101,7 @@ impl FolderService {
|
|||||||
&self,
|
&self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
_dto: RenameFolderDto,
|
_dto: RenameFolderDto,
|
||||||
_caller_id: &str,
|
_caller_id: Uuid,
|
||||||
) -> Result<FolderDto, DomainError> {
|
) -> Result<FolderDto, DomainError> {
|
||||||
Ok(FolderDto::empty())
|
Ok(FolderDto::empty())
|
||||||
}
|
}
|
||||||
@@ -109,18 +110,18 @@ impl FolderService {
|
|||||||
&self,
|
&self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
_dto: MoveFolderDto,
|
_dto: MoveFolderDto,
|
||||||
_caller_id: &str,
|
_caller_id: Uuid,
|
||||||
) -> Result<FolderDto, DomainError> {
|
) -> Result<FolderDto, DomainError> {
|
||||||
Ok(FolderDto::empty())
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_home_folder(
|
async fn create_home_folder(
|
||||||
&self,
|
&self,
|
||||||
_user_id: &str,
|
_user_id: Uuid,
|
||||||
_name: String,
|
_name: String,
|
||||||
) -> Result<FolderDto, DomainError> {
|
) -> Result<FolderDto, DomainError> {
|
||||||
Ok(FolderDto::empty())
|
Ok(FolderDto::empty())
|
||||||
@@ -170,12 +171,13 @@ impl FolderUseCase for FolderService {
|
|||||||
/// Creates a root-level home folder for a user during registration.
|
/// Creates a root-level home folder for a user during registration.
|
||||||
async fn create_home_folder(
|
async fn create_home_folder(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
name: String,
|
name: String,
|
||||||
) -> Result<FolderDto, DomainError> {
|
) -> Result<FolderDto, DomainError> {
|
||||||
|
let user_id_str = user_id.to_string();
|
||||||
let folder = self
|
let folder = self
|
||||||
.folder_storage
|
.folder_storage
|
||||||
.create_home_folder(user_id, name)
|
.create_home_folder(&user_id_str, name)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
DomainError::internal_error(
|
DomainError::internal_error(
|
||||||
@@ -205,9 +207,10 @@ impl FolderUseCase for FolderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Gets a folder by its ID, enforcing that `caller_id` is the owner.
|
/// 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?;
|
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!(
|
tracing::warn!(
|
||||||
"get_folder_owned: user '{}' attempted to access folder '{}' owned by '{:?}'",
|
"get_folder_owned: user '{}' attempted to access folder '{}' owned by '{:?}'",
|
||||||
caller_id,
|
caller_id,
|
||||||
@@ -260,11 +263,12 @@ impl FolderUseCase for FolderService {
|
|||||||
async fn list_folders_for_owner(
|
async fn list_folders_for_owner(
|
||||||
&self,
|
&self,
|
||||||
parent_id: Option<&str>,
|
parent_id: Option<&str>,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
) -> Result<Vec<FolderDto>, DomainError> {
|
) -> Result<Vec<FolderDto>, DomainError> {
|
||||||
|
let owner_id_str = owner_id.to_string();
|
||||||
let folders = self
|
let folders = self
|
||||||
.folder_storage
|
.folder_storage
|
||||||
.list_folders_by_owner(parent_id, owner_id)
|
.list_folders_by_owner(parent_id, &owner_id_str)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
DomainError::internal_error(
|
DomainError::internal_error(
|
||||||
@@ -283,10 +287,10 @@ impl FolderUseCase for FolderService {
|
|||||||
"No root folders found for user {}, creating home folder automatically",
|
"No root folders found for user {}, creating home folder automatically",
|
||||||
owner_id
|
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
|
match self
|
||||||
.folder_storage
|
.folder_storage
|
||||||
.create_home_folder(owner_id, folder_name.clone())
|
.create_home_folder(&owner_id_str, folder_name.clone())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(home_folder) => {
|
Ok(home_folder) => {
|
||||||
@@ -346,17 +350,18 @@ impl FolderUseCase for FolderService {
|
|||||||
async fn list_folders_for_owner_paginated(
|
async fn list_folders_for_owner_paginated(
|
||||||
&self,
|
&self,
|
||||||
parent_id: Option<&str>,
|
parent_id: Option<&str>,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
pagination: &crate::application::dtos::pagination::PaginationRequestDto,
|
pagination: &crate::application::dtos::pagination::PaginationRequestDto,
|
||||||
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>
|
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>
|
||||||
{
|
{
|
||||||
|
let owner_id_str = owner_id.to_string();
|
||||||
let pagination = pagination.validate_and_adjust();
|
let pagination = pagination.validate_and_adjust();
|
||||||
|
|
||||||
let (folders, total_items) = self
|
let (folders, total_items) = self
|
||||||
.folder_storage
|
.folder_storage
|
||||||
.list_folders_by_owner_paginated(
|
.list_folders_by_owner_paginated(
|
||||||
parent_id,
|
parent_id,
|
||||||
owner_id,
|
&owner_id_str,
|
||||||
pagination.offset(),
|
pagination.offset(),
|
||||||
pagination.limit(),
|
pagination.limit(),
|
||||||
true,
|
true,
|
||||||
@@ -389,8 +394,9 @@ impl FolderUseCase for FolderService {
|
|||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
dto: RenameFolderDto,
|
dto: RenameFolderDto,
|
||||||
caller_id: &str,
|
caller_id: Uuid,
|
||||||
) -> Result<FolderDto, DomainError> {
|
) -> Result<FolderDto, DomainError> {
|
||||||
|
let caller_id_str = caller_id.to_string();
|
||||||
// Input validation
|
// Input validation
|
||||||
if dto.name.is_empty() {
|
if dto.name.is_empty() {
|
||||||
return Err(DomainError::new(
|
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!(
|
tracing::warn!(
|
||||||
"rename_folder: user '{}' attempted to rename folder '{}' owned by '{:?}'",
|
"rename_folder: user '{}' attempted to rename folder '{}' owned by '{:?}'",
|
||||||
caller_id,
|
caller_id,
|
||||||
@@ -438,8 +444,9 @@ impl FolderUseCase for FolderService {
|
|||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
dto: MoveFolderDto,
|
dto: MoveFolderDto,
|
||||||
caller_id: &str,
|
caller_id: Uuid,
|
||||||
) -> Result<FolderDto, DomainError> {
|
) -> Result<FolderDto, DomainError> {
|
||||||
|
let caller_id_str = caller_id.to_string();
|
||||||
// Verify the source folder exists and belongs to the caller
|
// Verify the source folder exists and belongs to the caller
|
||||||
let source_folder = self.folder_storage.get_folder(id).await.map_err(|e| {
|
let source_folder = self.folder_storage.get_folder(id).await.map_err(|e| {
|
||||||
DomainError::internal_error(
|
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!(
|
tracing::warn!(
|
||||||
"move_folder: user '{}' attempted to move folder '{}' owned by '{:?}'",
|
"move_folder: user '{}' attempted to move folder '{}' owned by '{:?}'",
|
||||||
caller_id,
|
caller_id,
|
||||||
@@ -495,7 +502,8 @@ impl FolderUseCase for FolderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes a folder after verifying ownership.
|
/// 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
|
// Verify the folder exists and belongs to the caller
|
||||||
let folder = self.folder_storage.get_folder(id).await.map_err(|e| {
|
let folder = self.folder_storage.get_folder(id).await.map_err(|e| {
|
||||||
DomainError::internal_error(
|
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!(
|
tracing::warn!(
|
||||||
"delete_folder: user '{}' attempted to delete folder '{}' owned by '{:?}'",
|
"delete_folder: user '{}' attempted to delete folder '{}' owned by '{:?}'",
|
||||||
caller_id,
|
caller_id,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::common::errors::{DomainError, ErrorKind, Result};
|
|||||||
use crate::infrastructure::repositories::pg::RecentItemsPgRepository;
|
use crate::infrastructure::repositories::pg::RecentItemsPgRepository;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Implementation of the use case for managing recent items.
|
/// Implementation of the use case for managing recent items.
|
||||||
///
|
///
|
||||||
@@ -28,7 +29,7 @@ impl RecentItemsUseCase for RecentService {
|
|||||||
/// Get recent items for a user
|
/// Get recent items for a user
|
||||||
async fn get_recent_items(
|
async fn get_recent_items(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
limit: Option<i32>,
|
limit: Option<i32>,
|
||||||
) -> Result<Vec<RecentItemDto>> {
|
) -> Result<Vec<RecentItemDto>> {
|
||||||
info!("Getting recent items for user: {}", user_id);
|
info!("Getting recent items for user: {}", user_id);
|
||||||
@@ -47,7 +48,7 @@ impl RecentItemsUseCase for RecentService {
|
|||||||
/// Record access to an item
|
/// Record access to an item
|
||||||
async fn record_item_access(
|
async fn record_item_access(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
item_id: &str,
|
item_id: &str,
|
||||||
item_type: &str,
|
item_type: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
@@ -77,7 +78,7 @@ impl RecentItemsUseCase for RecentService {
|
|||||||
/// Remove an item from recent
|
/// Remove an item from recent
|
||||||
async fn remove_from_recent(
|
async fn remove_from_recent(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
item_id: &str,
|
item_id: &str,
|
||||||
item_type: &str,
|
item_type: &str,
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
@@ -101,7 +102,7 @@ impl RecentItemsUseCase for RecentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Clear all recent items
|
/// 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);
|
info!("Clearing all recent items for user {}", user_id);
|
||||||
self.repo.clear_all(user_id).await?;
|
self.repo.clear_all(user_id).await?;
|
||||||
info!("Cleared all recent items for user {}", user_id);
|
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::file_blob_read_repository::FileBlobReadRepository;
|
||||||
use crate::infrastructure::repositories::pg::folder_db_repository::FolderDbRepository;
|
use crate::infrastructure::repositories::pg::folder_db_repository::FolderDbRepository;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* High-performance search service implementation for files and folders.
|
* High-performance search service implementation for files and folders.
|
||||||
@@ -285,12 +286,13 @@ impl SearchUseCase for SearchService {
|
|||||||
async fn search(
|
async fn search(
|
||||||
&self,
|
&self,
|
||||||
criteria: SearchCriteriaDto,
|
criteria: SearchCriteriaDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Arc<SearchResultsDto>> {
|
) -> Result<Arc<SearchResultsDto>> {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
let user_id_str = user_id.to_string();
|
||||||
|
|
||||||
// Try to get from cache
|
// 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 {
|
if let Some(cached_results) = self.get_from_cache(cache_key).await {
|
||||||
return Ok(cached_results);
|
return Ok(cached_results);
|
||||||
}
|
}
|
||||||
@@ -321,7 +323,7 @@ impl SearchUseCase for SearchService {
|
|||||||
.search_folders(
|
.search_folders(
|
||||||
criteria.folder_id.as_deref(),
|
criteria.folder_id.as_deref(),
|
||||||
criteria.name_contains.as_deref(),
|
criteria.name_contains.as_deref(),
|
||||||
user_id,
|
&user_id_str,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -403,7 +405,7 @@ impl SearchUseCase for SearchService {
|
|||||||
.search_folders(
|
.search_folders(
|
||||||
criteria.folder_id.as_deref(),
|
criteria.folder_id.as_deref(),
|
||||||
criteria.name_contains.as_deref(),
|
criteria.name_contains.as_deref(),
|
||||||
user_id,
|
&user_id_str,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -504,7 +506,7 @@ impl SearchService {
|
|||||||
async fn search(
|
async fn search(
|
||||||
&self,
|
&self,
|
||||||
_criteria: SearchCriteriaDto,
|
_criteria: SearchCriteriaDto,
|
||||||
_user_id: &str,
|
_user_id: Uuid,
|
||||||
) -> Result<Arc<SearchResultsDto>> {
|
) -> Result<Arc<SearchResultsDto>> {
|
||||||
Ok(Arc::new(SearchResultsDto::empty()))
|
Ok(Arc::new(SearchResultsDto::empty()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::sync::Semaphore;
|
use tokio::sync::Semaphore;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::domain::repositories::folder_repository::FolderRepository;
|
use crate::domain::repositories::folder_repository::FolderRepository;
|
||||||
use crate::infrastructure::repositories::pg::SharePgRepository;
|
use crate::infrastructure::repositories::pg::SharePgRepository;
|
||||||
@@ -150,7 +151,7 @@ impl ShareService {
|
|||||||
/// but belongs to a different user — this prevents share-ID enumeration
|
/// but belongs to a different user — this prevents share-ID enumeration
|
||||||
/// attacks where an attacker probes IDs and uses 403-vs-404 to learn
|
/// attacks where an attacker probes IDs and uses 403-vs-404 to learn
|
||||||
/// which ones are valid.
|
/// 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
|
let share = self
|
||||||
.share_repository
|
.share_repository
|
||||||
.find_share_by_id_for_user(id, requester_id)
|
.find_share_by_id_for_user(id, requester_id)
|
||||||
@@ -163,7 +164,7 @@ impl ShareService {
|
|||||||
impl ShareUseCase for ShareService {
|
impl ShareUseCase for ShareService {
|
||||||
async fn create_shared_link(
|
async fn create_shared_link(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
dto: CreateShareDto,
|
dto: CreateShareDto,
|
||||||
) -> Result<ShareDto, DomainError> {
|
) -> Result<ShareDto, DomainError> {
|
||||||
// Convert the item type
|
// Convert the item type
|
||||||
@@ -187,7 +188,7 @@ impl ShareUseCase for ShareService {
|
|||||||
dto.item_id.clone(),
|
dto.item_id.clone(),
|
||||||
dto.item_name.clone(),
|
dto.item_name.clone(),
|
||||||
item_type,
|
item_type,
|
||||||
user_id.to_string(),
|
user_id,
|
||||||
permissions,
|
permissions,
|
||||||
password_hash,
|
password_hash,
|
||||||
dto.expires_at,
|
dto.expires_at,
|
||||||
@@ -205,7 +206,7 @@ impl ShareUseCase for ShareService {
|
|||||||
Ok(ShareDto::from_entity(&saved_share, &self.config.base_url()))
|
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
|
// SECURITY: ownership-verified lookup — returns 404 if the share
|
||||||
// doesn't exist OR belongs to another user.
|
// doesn't exist OR belongs to another user.
|
||||||
let share = self.fetch_owned_share(id, requester_id).await?;
|
let share = self.fetch_owned_share(id, requester_id).await?;
|
||||||
@@ -253,7 +254,7 @@ impl ShareUseCase for ShareService {
|
|||||||
&self,
|
&self,
|
||||||
item_id: &str,
|
item_id: &str,
|
||||||
item_type: &ShareItemType,
|
item_type: &ShareItemType,
|
||||||
requester_id: &str,
|
requester_id: Uuid,
|
||||||
) -> Result<Vec<ShareDto>, DomainError> {
|
) -> Result<Vec<ShareDto>, DomainError> {
|
||||||
// SECURITY: only return shares created by the requester
|
// SECURITY: only return shares created by the requester
|
||||||
let shares = self
|
let shares = self
|
||||||
@@ -276,8 +277,8 @@ impl ShareUseCase for ShareService {
|
|||||||
|
|
||||||
async fn update_shared_link(
|
async fn update_shared_link(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: Uuid,
|
||||||
requester_id: &str,
|
requester_id: Uuid,
|
||||||
dto: UpdateShareDto,
|
dto: UpdateShareDto,
|
||||||
) -> Result<ShareDto, DomainError> {
|
) -> Result<ShareDto, DomainError> {
|
||||||
// SECURITY: ownership-verified lookup — prevents IDOR
|
// 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
|
// SECURITY: ownership-verified delete — only the creator can remove
|
||||||
self.share_repository
|
self.share_repository
|
||||||
.delete_share_for_user(id, requester_id)
|
.delete_share_for_user(id, requester_id)
|
||||||
@@ -333,7 +334,7 @@ impl ShareUseCase for ShareService {
|
|||||||
|
|
||||||
async fn get_user_shared_links(
|
async fn get_user_shared_links(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
page: usize,
|
page: usize,
|
||||||
per_page: usize,
|
per_page: usize,
|
||||||
) -> Result<PaginatedResponseDto<ShareDto>, DomainError> {
|
) -> Result<PaginatedResponseDto<ShareDto>, DomainError> {
|
||||||
@@ -519,7 +520,7 @@ mod tests {
|
|||||||
{
|
{
|
||||||
async fn create_shared_link(
|
async fn create_shared_link(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
dto: CreateShareDto,
|
dto: CreateShareDto,
|
||||||
) -> Result<ShareDto, DomainError> {
|
) -> Result<ShareDto, DomainError> {
|
||||||
let item_type = ShareItemType::try_from(dto.item_type.as_str())
|
let item_type = ShareItemType::try_from(dto.item_type.as_str())
|
||||||
@@ -534,7 +535,7 @@ mod tests {
|
|||||||
dto.item_id.clone(),
|
dto.item_id.clone(),
|
||||||
dto.item_name.clone(),
|
dto.item_name.clone(),
|
||||||
item_type,
|
item_type,
|
||||||
user_id.to_string(),
|
user_id,
|
||||||
permissions,
|
permissions,
|
||||||
password_hash,
|
password_hash,
|
||||||
dto.expires_at,
|
dto.expires_at,
|
||||||
@@ -550,8 +551,8 @@ mod tests {
|
|||||||
|
|
||||||
async fn get_shared_link(
|
async fn get_shared_link(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: Uuid,
|
||||||
requester_id: &str,
|
requester_id: Uuid,
|
||||||
) -> Result<ShareDto, DomainError> {
|
) -> Result<ShareDto, DomainError> {
|
||||||
let share = self
|
let share = self
|
||||||
.share_repository
|
.share_repository
|
||||||
@@ -584,7 +585,7 @@ mod tests {
|
|||||||
&self,
|
&self,
|
||||||
item_id: &str,
|
item_id: &str,
|
||||||
item_type: &ShareItemType,
|
item_type: &ShareItemType,
|
||||||
requester_id: &str,
|
requester_id: Uuid,
|
||||||
) -> Result<Vec<ShareDto>, DomainError> {
|
) -> Result<Vec<ShareDto>, DomainError> {
|
||||||
let shares = self
|
let shares = self
|
||||||
.share_repository
|
.share_repository
|
||||||
@@ -600,8 +601,8 @@ mod tests {
|
|||||||
|
|
||||||
async fn update_shared_link(
|
async fn update_shared_link(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: Uuid,
|
||||||
requester_id: &str,
|
requester_id: Uuid,
|
||||||
dto: UpdateShareDto,
|
dto: UpdateShareDto,
|
||||||
) -> Result<ShareDto, DomainError> {
|
) -> Result<ShareDto, DomainError> {
|
||||||
let mut share = self
|
let mut share = self
|
||||||
@@ -635,8 +636,8 @@ mod tests {
|
|||||||
|
|
||||||
async fn delete_shared_link(
|
async fn delete_shared_link(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: Uuid,
|
||||||
requester_id: &str,
|
requester_id: Uuid,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
self.share_repository
|
self.share_repository
|
||||||
.delete_share_for_user(id, requester_id)
|
.delete_share_for_user(id, requester_id)
|
||||||
@@ -647,7 +648,7 @@ mod tests {
|
|||||||
|
|
||||||
async fn get_user_shared_links(
|
async fn get_user_shared_links(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
page: usize,
|
page: usize,
|
||||||
per_page: usize,
|
per_page: usize,
|
||||||
) -> Result<PaginatedResponseDto<ShareDto>, DomainError> {
|
) -> Result<PaginatedResponseDto<ShareDto>, DomainError> {
|
||||||
@@ -1018,28 +1019,30 @@ mod tests {
|
|||||||
|
|
||||||
async fn find_share_by_id_for_user(
|
async fn find_share_by_id_for_user(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: Uuid,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Share, DomainError> {
|
) -> Result<Share, DomainError> {
|
||||||
let shares = self.shares.lock().unwrap();
|
let shares = self.shares.lock().unwrap();
|
||||||
|
let id_str = id.to_string();
|
||||||
shares
|
shares
|
||||||
.get(id)
|
.get(&id_str)
|
||||||
.filter(|s| s.created_by() == user_id)
|
.filter(|s| s.created_by() == user_id)
|
||||||
.cloned()
|
.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 shares = self.shares.lock().unwrap();
|
||||||
let mut tokens = self.tokens.lock().unwrap();
|
let mut tokens = self.tokens.lock().unwrap();
|
||||||
|
let id_str = id.to_string();
|
||||||
|
|
||||||
let share = shares
|
let share = shares
|
||||||
.get(id)
|
.get(&id_str)
|
||||||
.filter(|s| s.created_by() == user_id)
|
.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());
|
tokens.remove(share.token());
|
||||||
shares.remove(id);
|
shares.remove(&id_str);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1047,7 +1050,7 @@ mod tests {
|
|||||||
&self,
|
&self,
|
||||||
item_id: &str,
|
item_id: &str,
|
||||||
item_type: &ShareItemType,
|
item_type: &ShareItemType,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<Share>, DomainError> {
|
) -> Result<Vec<Share>, DomainError> {
|
||||||
let shares = self.shares.lock().unwrap();
|
let shares = self.shares.lock().unwrap();
|
||||||
let type_str = item_type.to_string();
|
let type_str = item_type.to_string();
|
||||||
@@ -1078,7 +1081,7 @@ mod tests {
|
|||||||
|
|
||||||
async fn find_shares_by_user(
|
async fn find_shares_by_user(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
) -> Result<(Vec<Share>, usize), DomainError> {
|
) -> 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());
|
assert!(result.is_ok());
|
||||||
|
|
||||||
let share_dto = result.unwrap();
|
let share_dto = result.unwrap();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use sqlx::PgPool;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for managing and updating user storage usage statistics.
|
* Service for managing and updating user storage usage statistics.
|
||||||
@@ -31,7 +32,7 @@ impl StorageUsageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Calculates and updates storage usage for a specific user
|
/// 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);
|
info!("Updating storage usage for user: {}", user_id);
|
||||||
|
|
||||||
// Calculate storage usage directly from database
|
// Calculate storage usage directly from database
|
||||||
@@ -52,12 +53,11 @@ impl StorageUsageService {
|
|||||||
|
|
||||||
/// Calculates a user's storage usage by summing all their file sizes.
|
/// Calculates a user's storage usage by summing all their file sizes.
|
||||||
/// Uses a direct SQL query for O(1) performance.
|
/// 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);
|
debug!("Calculating storage for user: {}", user_id);
|
||||||
|
|
||||||
// Direct SQL query to sum all file sizes for this user
|
// Direct SQL query to sum all file sizes for this user
|
||||||
// This is much more efficient than recursively walking folders
|
// 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(
|
let total_size: i64 = sqlx::query_scalar(
|
||||||
r#"
|
r#"
|
||||||
SELECT COALESCE(SUM(size), 0)::bigint
|
SELECT COALESCE(SUM(size), 0)::bigint
|
||||||
@@ -88,14 +88,14 @@ impl StorageUsageService {
|
|||||||
info!("Updating storage usage for username: {}", username);
|
info!("Updating storage usage for username: {}", username);
|
||||||
|
|
||||||
let user = self.user_repository.get_user_by_username(username).await?;
|
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
|
// 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
|
// Update the user's storage usage in the database
|
||||||
self.user_repository
|
self.user_repository
|
||||||
.update_storage_usage(&user_id, total_usage)
|
.update_storage_usage(user_id, total_usage)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
@@ -112,7 +112,7 @@ impl StorageUsageService {
|
|||||||
* to the application layer.
|
* to the application layer.
|
||||||
*/
|
*/
|
||||||
impl StorageUsagePort for StorageUsageService {
|
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
|
StorageUsageService::update_user_storage_usage(self, user_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,12 +133,12 @@ impl StorageUsagePort for StorageUsageService {
|
|||||||
|
|
||||||
// Process users in parallel
|
// Process users in parallel
|
||||||
for user in users {
|
for user in users {
|
||||||
let user_id = user.id().to_string();
|
let user_id = user.id();
|
||||||
let service_clone = self.clone();
|
let service_clone = self.clone();
|
||||||
|
|
||||||
// Spawn a background task for each user
|
// Spawn a background task for each user
|
||||||
let task = task::spawn(async move {
|
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) => {
|
Ok(usage) => {
|
||||||
debug!(
|
debug!(
|
||||||
"Updated storage usage for user {}: {} bytes",
|
"Updated storage usage for user {}: {} bytes",
|
||||||
@@ -168,7 +168,7 @@ impl StorageUsagePort for StorageUsageService {
|
|||||||
|
|
||||||
async fn check_storage_quota(
|
async fn check_storage_quota(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
additional_bytes: u64,
|
additional_bytes: u64,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
let user = self.user_repository.get_user_by_id(user_id).await?;
|
let user = self.user_repository.get_user_by_id(user_id).await?;
|
||||||
@@ -206,7 +206,7 @@ impl StorageUsagePort for StorageUsageService {
|
|||||||
Ok(())
|
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?;
|
let user = self.user_repository.get_user_by_id(user_id).await?;
|
||||||
Ok((user.storage_used_bytes(), user.storage_quota_bytes()))
|
Ok((user.storage_used_bytes(), user.storage_quota_bytes()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,13 +123,10 @@ impl TrashService {
|
|||||||
|
|
||||||
impl TrashUseCase for TrashService {
|
impl TrashUseCase for TrashService {
|
||||||
#[instrument(skip(self))]
|
#[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);
|
debug!("Getting trash items for user: {}", user_id);
|
||||||
|
|
||||||
let user_uuid = Uuid::parse_str(user_id)
|
let items = self.trash_repository.get_trash_items(&user_id).await?;
|
||||||
.map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?;
|
|
||||||
|
|
||||||
let items = self.trash_repository.get_trash_items(&user_uuid).await?;
|
|
||||||
|
|
||||||
let dtos = items.into_iter().map(|item| self.to_dto(item)).collect();
|
let dtos = items.into_iter().map(|item| self.to_dto(item)).collect();
|
||||||
|
|
||||||
@@ -137,7 +134,7 @@ impl TrashUseCase for TrashService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[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!(
|
info!(
|
||||||
"Moving to trash: type={}, id={}, user={}",
|
"Moving to trash: type={}, id={}, user={}",
|
||||||
item_type, item_id, user_id
|
item_type, item_id, user_id
|
||||||
@@ -163,20 +160,7 @@ impl TrashUseCase for TrashService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!("Validating user UUID: {}", user_id);
|
let 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
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match item_type {
|
match item_type {
|
||||||
"file" => {
|
"file" => {
|
||||||
@@ -276,7 +260,7 @@ impl TrashUseCase for TrashService {
|
|||||||
|
|
||||||
// Ownership check — return NotFound (not Forbidden) to
|
// Ownership check — return NotFound (not Forbidden) to
|
||||||
// prevent leaking whether the folder exists.
|
// 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(
|
return Err(DomainError::not_found(
|
||||||
"Folder",
|
"Folder",
|
||||||
format!("Folder not found: {}", item_id),
|
format!("Folder not found: {}", item_id),
|
||||||
@@ -331,7 +315,7 @@ impl TrashUseCase for TrashService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[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);
|
info!("Restoring item {} for user {}", trash_id, user_id);
|
||||||
|
|
||||||
let trash_uuid = match Uuid::parse_str(trash_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) {
|
let user_uuid = 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
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the trash item
|
// Get the trash item
|
||||||
info!("Retrieving trash item from repository: ID={}", trash_id);
|
info!("Retrieving trash item from repository: ID={}", trash_id);
|
||||||
@@ -514,7 +486,7 @@ impl TrashUseCase for TrashService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[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!(
|
info!(
|
||||||
"Permanently deleting item {} for user {}",
|
"Permanently deleting item {} for user {}",
|
||||||
trash_id, user_id
|
trash_id, user_id
|
||||||
@@ -534,19 +506,7 @@ impl TrashUseCase for TrashService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_uuid = match Uuid::parse_str(user_id) {
|
let user_uuid = 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
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the trash item
|
// Get the trash item
|
||||||
info!("Retrieving trash item from repository: ID={}", trash_id);
|
info!("Retrieving trash item from repository: ID={}", trash_id);
|
||||||
@@ -684,12 +644,9 @@ impl TrashUseCase for TrashService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[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);
|
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:
|
// clear_trash() already performs bulk SQL DELETEs in 2 queries:
|
||||||
// 1. DELETE FROM storage.files WHERE user_id = $1 AND is_trashed = TRUE
|
// 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
|
// 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.
|
// remove_reference() call is needed.
|
||||||
//
|
//
|
||||||
// Finally it clears the trash_items index for the user.
|
// 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);
|
info!("Trash emptied for user {}", user_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
+22
-21
@@ -12,6 +12,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::dtos::file_dto::FileDto;
|
use crate::application::dtos::file_dto::FileDto;
|
||||||
use crate::application::dtos::folder_dto::{
|
use crate::application::dtos::folder_dto::{
|
||||||
@@ -108,7 +109,7 @@ impl FileReadPort for StubFileReadPort {
|
|||||||
&self,
|
&self,
|
||||||
_folder_id: Option<&str>,
|
_folder_id: Option<&str>,
|
||||||
_criteria: &SearchCriteriaDto,
|
_criteria: &SearchCriteriaDto,
|
||||||
_user_id: &str,
|
_user_id: Uuid,
|
||||||
) -> Result<(Vec<File>, usize), DomainError> {
|
) -> Result<(Vec<File>, usize), DomainError> {
|
||||||
Ok((Vec::new(), 0))
|
Ok((Vec::new(), 0))
|
||||||
}
|
}
|
||||||
@@ -117,7 +118,7 @@ impl FileReadPort for StubFileReadPort {
|
|||||||
&self,
|
&self,
|
||||||
_folder_id: Option<&str>,
|
_folder_id: Option<&str>,
|
||||||
_criteria: &SearchCriteriaDto,
|
_criteria: &SearchCriteriaDto,
|
||||||
_user_id: &str,
|
_user_id: Uuid,
|
||||||
) -> Result<usize, DomainError> {
|
) -> Result<usize, DomainError> {
|
||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
}
|
||||||
@@ -129,7 +130,7 @@ impl FileReadPort for StubFileReadPort {
|
|||||||
Ok(Box::pin(futures::stream::empty()))
|
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())
|
Ok(File::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -362,7 +363,7 @@ impl FolderUseCase for StubFolderUseCase {
|
|||||||
async fn get_folder_owned(
|
async fn get_folder_owned(
|
||||||
&self,
|
&self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
_caller_id: &str,
|
_caller_id: Uuid,
|
||||||
) -> Result<FolderDto, DomainError> {
|
) -> Result<FolderDto, DomainError> {
|
||||||
Ok(FolderDto::default())
|
Ok(FolderDto::default())
|
||||||
}
|
}
|
||||||
@@ -378,7 +379,7 @@ impl FolderUseCase for StubFolderUseCase {
|
|||||||
async fn list_folders_for_owner(
|
async fn list_folders_for_owner(
|
||||||
&self,
|
&self,
|
||||||
_parent_id: Option<&str>,
|
_parent_id: Option<&str>,
|
||||||
_owner_id: &str,
|
_owner_id: Uuid,
|
||||||
) -> Result<Vec<FolderDto>, DomainError> {
|
) -> Result<Vec<FolderDto>, DomainError> {
|
||||||
Ok(Vec::new())
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
@@ -394,7 +395,7 @@ impl FolderUseCase for StubFolderUseCase {
|
|||||||
async fn list_folders_for_owner_paginated(
|
async fn list_folders_for_owner_paginated(
|
||||||
&self,
|
&self,
|
||||||
_parent_id: Option<&str>,
|
_parent_id: Option<&str>,
|
||||||
_owner_id: &str,
|
_owner_id: Uuid,
|
||||||
_pagination: &PaginationRequestDto,
|
_pagination: &PaginationRequestDto,
|
||||||
) -> Result<PaginatedResponseDto<FolderDto>, DomainError> {
|
) -> Result<PaginatedResponseDto<FolderDto>, DomainError> {
|
||||||
Ok(PaginatedResponseDto::new(Vec::new(), 0, 10, 0))
|
Ok(PaginatedResponseDto::new(Vec::new(), 0, 10, 0))
|
||||||
@@ -404,7 +405,7 @@ impl FolderUseCase for StubFolderUseCase {
|
|||||||
&self,
|
&self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
_dto: RenameFolderDto,
|
_dto: RenameFolderDto,
|
||||||
_caller_id: &str,
|
_caller_id: Uuid,
|
||||||
) -> Result<FolderDto, DomainError> {
|
) -> Result<FolderDto, DomainError> {
|
||||||
Ok(FolderDto::default())
|
Ok(FolderDto::default())
|
||||||
}
|
}
|
||||||
@@ -413,18 +414,18 @@ impl FolderUseCase for StubFolderUseCase {
|
|||||||
&self,
|
&self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
_dto: MoveFolderDto,
|
_dto: MoveFolderDto,
|
||||||
_caller_id: &str,
|
_caller_id: Uuid,
|
||||||
) -> Result<FolderDto, DomainError> {
|
) -> Result<FolderDto, DomainError> {
|
||||||
Ok(FolderDto::default())
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_home_folder(
|
async fn create_home_folder(
|
||||||
&self,
|
&self,
|
||||||
_user_id: &str,
|
_user_id: Uuid,
|
||||||
_name: String,
|
_name: String,
|
||||||
) -> Result<FolderDto, DomainError> {
|
) -> Result<FolderDto, DomainError> {
|
||||||
Ok(FolderDto::default())
|
Ok(FolderDto::default())
|
||||||
@@ -510,7 +511,7 @@ impl FileRetrievalUseCase for StubFileRetrievalUseCase {
|
|||||||
async fn list_files_owned(
|
async fn list_files_owned(
|
||||||
&self,
|
&self,
|
||||||
_folder_id: Option<&str>,
|
_folder_id: Option<&str>,
|
||||||
_owner_id: &str,
|
_owner_id: Uuid,
|
||||||
) -> Result<Vec<FileDto>, DomainError> {
|
) -> Result<Vec<FileDto>, DomainError> {
|
||||||
Ok(Vec::new())
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
@@ -526,7 +527,7 @@ impl FileRetrievalUseCase for StubFileRetrievalUseCase {
|
|||||||
async fn get_file_stream_owned(
|
async fn get_file_stream_owned(
|
||||||
&self,
|
&self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
_caller_id: &str,
|
_caller_id: Uuid,
|
||||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
|
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
|
||||||
let empty_stream = futures::stream::empty::<Result<Bytes, std::io::Error>>();
|
let empty_stream = futures::stream::empty::<Result<Bytes, std::io::Error>>();
|
||||||
Ok(Box::new(empty_stream))
|
Ok(Box::new(empty_stream))
|
||||||
@@ -569,14 +570,14 @@ impl FileRetrievalUseCase for StubFileRetrievalUseCase {
|
|||||||
Ok(Box::pin(futures::stream::empty()))
|
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())
|
Ok(FileDto::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_file_optimized_owned(
|
async fn get_file_optimized_owned(
|
||||||
&self,
|
&self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
_caller_id: &str,
|
_caller_id: Uuid,
|
||||||
_accept_webp: bool,
|
_accept_webp: bool,
|
||||||
_prefer_original: bool,
|
_prefer_original: bool,
|
||||||
) -> Result<(FileDto, OptimizedFileContent), DomainError> {
|
) -> Result<(FileDto, OptimizedFileContent), DomainError> {
|
||||||
@@ -593,7 +594,7 @@ impl FileRetrievalUseCase for StubFileRetrievalUseCase {
|
|||||||
async fn get_file_range_stream_owned(
|
async fn get_file_range_stream_owned(
|
||||||
&self,
|
&self,
|
||||||
_id: &str,
|
_id: &str,
|
||||||
_caller_id: &str,
|
_caller_id: Uuid,
|
||||||
_start: u64,
|
_start: u64,
|
||||||
_end: Option<u64>,
|
_end: Option<u64>,
|
||||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
|
) -> 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(
|
async fn copy_file_owned(
|
||||||
&self,
|
&self,
|
||||||
_file_id: &str,
|
_file_id: &str,
|
||||||
_caller_id: &str,
|
_caller_id: Uuid,
|
||||||
_folder_id: Option<String>,
|
_folder_id: Option<String>,
|
||||||
) -> Result<FileDto, DomainError> {
|
) -> Result<FileDto, DomainError> {
|
||||||
Ok(FileDto::default())
|
Ok(FileDto::default())
|
||||||
@@ -642,18 +643,18 @@ impl FileManagementUseCase for StubFileManagementUseCase {
|
|||||||
Ok(())
|
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(())
|
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)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn move_file_owned(
|
async fn move_file_owned(
|
||||||
&self,
|
&self,
|
||||||
_file_id: &str,
|
_file_id: &str,
|
||||||
_caller_id: &str,
|
_caller_id: Uuid,
|
||||||
_folder_id: Option<String>,
|
_folder_id: Option<String>,
|
||||||
) -> Result<FileDto, DomainError> {
|
) -> Result<FileDto, DomainError> {
|
||||||
Ok(FileDto::default())
|
Ok(FileDto::default())
|
||||||
@@ -662,7 +663,7 @@ impl FileManagementUseCase for StubFileManagementUseCase {
|
|||||||
async fn rename_file_owned(
|
async fn rename_file_owned(
|
||||||
&self,
|
&self,
|
||||||
_file_id: &str,
|
_file_id: &str,
|
||||||
_caller_id: &str,
|
_caller_id: Uuid,
|
||||||
_new_name: &str,
|
_new_name: &str,
|
||||||
) -> Result<FileDto, DomainError> {
|
) -> Result<FileDto, DomainError> {
|
||||||
Ok(FileDto::default())
|
Ok(FileDto::default())
|
||||||
@@ -679,7 +680,7 @@ impl SearchUseCase for StubSearchUseCase {
|
|||||||
async fn search(
|
async fn search(
|
||||||
&self,
|
&self,
|
||||||
_criteria: SearchCriteriaDto,
|
_criteria: SearchCriteriaDto,
|
||||||
_user_id: &str,
|
_user_id: Uuid,
|
||||||
) -> Result<Arc<SearchResultsDto>, DomainError> {
|
) -> Result<Arc<SearchResultsDto>, DomainError> {
|
||||||
Ok(Arc::new(SearchResultsDto::empty()))
|
Ok(Arc::new(SearchResultsDto::empty()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ use uuid::Uuid;
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AppPassword {
|
pub struct AppPassword {
|
||||||
/// Unique identifier.
|
/// Unique identifier.
|
||||||
pub id: String,
|
pub id: Uuid,
|
||||||
/// Owner user ID.
|
/// Owner user ID.
|
||||||
pub user_id: String,
|
pub user_id: Uuid,
|
||||||
/// Human-readable label chosen by the user (e.g. "DAVx5 on Pixel 8").
|
/// Human-readable label chosen by the user (e.g. "DAVx5 on Pixel 8").
|
||||||
pub label: String,
|
pub label: String,
|
||||||
/// Argon2 hash of the generated password token.
|
/// 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 caller is responsible for hashing the raw token and passing
|
||||||
/// the hash and prefix.
|
/// the hash and prefix.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
user_id: String,
|
user_id: Uuid,
|
||||||
label: String,
|
label: String,
|
||||||
password_hash: String,
|
password_hash: String,
|
||||||
prefix: String,
|
prefix: String,
|
||||||
@@ -49,7 +49,7 @@ impl AppPassword {
|
|||||||
expires_at: Option<DateTime<Utc>>,
|
expires_at: Option<DateTime<Utc>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: Uuid::new_v4().to_string(),
|
id: Uuid::new_v4(),
|
||||||
user_id,
|
user_id,
|
||||||
label,
|
label,
|
||||||
password_hash,
|
password_hash,
|
||||||
|
|||||||
@@ -48,13 +48,13 @@ impl std::fmt::Display for DeviceCodeStatus {
|
|||||||
/// Domain entity for a Device Authorization flow.
|
/// Domain entity for a Device Authorization flow.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DeviceCode {
|
pub struct DeviceCode {
|
||||||
id: String,
|
id: Uuid,
|
||||||
device_code: String,
|
device_code: String,
|
||||||
user_code: String,
|
user_code: String,
|
||||||
client_name: String,
|
client_name: String,
|
||||||
scopes: String,
|
scopes: String,
|
||||||
status: DeviceCodeStatus,
|
status: DeviceCodeStatus,
|
||||||
user_id: Option<String>,
|
user_id: Option<Uuid>,
|
||||||
access_token: Option<String>,
|
access_token: Option<String>,
|
||||||
refresh_token: Option<String>,
|
refresh_token: Option<String>,
|
||||||
verification_uri: String,
|
verification_uri: String,
|
||||||
@@ -89,7 +89,7 @@ impl DeviceCode {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
Self {
|
Self {
|
||||||
id: Uuid::new_v4().to_string(),
|
id: Uuid::new_v4(),
|
||||||
device_code,
|
device_code,
|
||||||
user_code,
|
user_code,
|
||||||
client_name,
|
client_name,
|
||||||
@@ -111,13 +111,13 @@ impl DeviceCode {
|
|||||||
/// Reconstruct from database row.
|
/// Reconstruct from database row.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn from_raw(
|
pub fn from_raw(
|
||||||
id: String,
|
id: Uuid,
|
||||||
device_code: String,
|
device_code: String,
|
||||||
user_code: String,
|
user_code: String,
|
||||||
client_name: String,
|
client_name: String,
|
||||||
scopes: String,
|
scopes: String,
|
||||||
status: DeviceCodeStatus,
|
status: DeviceCodeStatus,
|
||||||
user_id: Option<String>,
|
user_id: Option<Uuid>,
|
||||||
access_token: Option<String>,
|
access_token: Option<String>,
|
||||||
refresh_token: Option<String>,
|
refresh_token: Option<String>,
|
||||||
verification_uri: String,
|
verification_uri: String,
|
||||||
@@ -150,8 +150,8 @@ impl DeviceCode {
|
|||||||
|
|
||||||
// ── Getters ──────────────────────────────────────────────────
|
// ── Getters ──────────────────────────────────────────────────
|
||||||
|
|
||||||
pub fn id(&self) -> &str {
|
pub fn id(&self) -> Uuid {
|
||||||
&self.id
|
self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn device_code(&self) -> &str {
|
pub fn device_code(&self) -> &str {
|
||||||
@@ -174,8 +174,8 @@ impl DeviceCode {
|
|||||||
self.status
|
self.status
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn user_id(&self) -> Option<&str> {
|
pub fn user_id(&self) -> Option<Uuid> {
|
||||||
self.user_id.as_deref()
|
self.user_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn access_token(&self) -> Option<&str> {
|
pub fn access_token(&self) -> Option<&str> {
|
||||||
@@ -243,7 +243,7 @@ impl DeviceCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Authorize this device code for a specific user, storing the tokens.
|
/// 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.status = DeviceCodeStatus::Authorized;
|
||||||
self.user_id = Some(user_id);
|
self.user_id = Some(user_id);
|
||||||
self.access_token = Some(access_token);
|
self.access_token = Some(access_token);
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Session {
|
pub struct Session {
|
||||||
id: String,
|
id: Uuid,
|
||||||
user_id: String,
|
user_id: Uuid,
|
||||||
refresh_token: String,
|
refresh_token: String,
|
||||||
expires_at: DateTime<Utc>,
|
expires_at: DateTime<Utc>,
|
||||||
ip_address: Option<String>,
|
ip_address: Option<String>,
|
||||||
@@ -15,22 +15,19 @@ pub struct Session {
|
|||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
user_id: String,
|
user_id: Uuid,
|
||||||
refresh_token: String,
|
refresh_token: String,
|
||||||
ip_address: Option<String>,
|
ip_address: Option<String>,
|
||||||
user_agent: Option<String>,
|
user_agent: Option<String>,
|
||||||
expires_in_days: i64,
|
expires_in_days: i64,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
if user_id.is_empty() {
|
|
||||||
panic!("Session user_id cannot be empty");
|
|
||||||
}
|
|
||||||
if refresh_token.is_empty() {
|
if refresh_token.is_empty() {
|
||||||
panic!("Session refresh_token cannot be empty");
|
panic!("Session refresh_token cannot be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
Self {
|
Self {
|
||||||
id: Uuid::new_v4().to_string(),
|
id: Uuid::new_v4(),
|
||||||
user_id,
|
user_id,
|
||||||
refresh_token,
|
refresh_token,
|
||||||
expires_at: now + Duration::days(expires_in_days),
|
expires_at: now + Duration::days(expires_in_days),
|
||||||
@@ -43,8 +40,8 @@ impl Session {
|
|||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn from_raw(
|
pub fn from_raw(
|
||||||
id: String,
|
id: Uuid,
|
||||||
user_id: String,
|
user_id: Uuid,
|
||||||
refresh_token: String,
|
refresh_token: String,
|
||||||
expires_at: DateTime<Utc>,
|
expires_at: DateTime<Utc>,
|
||||||
ip_address: Option<String>,
|
ip_address: Option<String>,
|
||||||
@@ -65,12 +62,12 @@ impl Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
pub fn id(&self) -> &str {
|
pub fn id(&self) -> Uuid {
|
||||||
&self.id
|
self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn user_id(&self) -> &str {
|
pub fn user_id(&self) -> Uuid {
|
||||||
&self.user_id
|
self.user_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_token(&self) -> &str {
|
pub fn refresh_token(&self) -> &str {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ pub use super::entity_errors::ShareError;
|
|||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct Share {
|
pub struct Share {
|
||||||
id: String,
|
id: Uuid,
|
||||||
item_id: String,
|
item_id: String,
|
||||||
item_name: Option<String>,
|
item_name: Option<String>,
|
||||||
item_type: ShareItemType,
|
item_type: ShareItemType,
|
||||||
@@ -15,7 +15,7 @@ pub struct Share {
|
|||||||
expires_at: Option<u64>,
|
expires_at: Option<u64>,
|
||||||
permissions: SharePermissions,
|
permissions: SharePermissions,
|
||||||
created_at: u64,
|
created_at: u64,
|
||||||
created_by: String,
|
created_by: Uuid,
|
||||||
access_count: u64,
|
access_count: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ impl Share {
|
|||||||
item_id: String,
|
item_id: String,
|
||||||
item_name: Option<String>,
|
item_name: Option<String>,
|
||||||
item_type: ShareItemType,
|
item_type: ShareItemType,
|
||||||
created_by: String,
|
created_by: Uuid,
|
||||||
permissions: Option<SharePermissions>,
|
permissions: Option<SharePermissions>,
|
||||||
password_hash: Option<String>,
|
password_hash: Option<String>,
|
||||||
expires_at: Option<u64>,
|
expires_at: Option<u64>,
|
||||||
@@ -69,7 +69,7 @@ impl Share {
|
|||||||
.as_secs();
|
.as_secs();
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: Uuid::new_v4().to_string(),
|
id: Uuid::new_v4(),
|
||||||
item_id,
|
item_id,
|
||||||
item_name,
|
item_name,
|
||||||
item_type,
|
item_type,
|
||||||
@@ -89,7 +89,7 @@ impl Share {
|
|||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn from_raw(
|
pub fn from_raw(
|
||||||
id: String,
|
id: Uuid,
|
||||||
item_id: String,
|
item_id: String,
|
||||||
item_name: Option<String>,
|
item_name: Option<String>,
|
||||||
item_type: ShareItemType,
|
item_type: ShareItemType,
|
||||||
@@ -98,7 +98,7 @@ impl Share {
|
|||||||
expires_at: Option<u64>,
|
expires_at: Option<u64>,
|
||||||
permissions: SharePermissions,
|
permissions: SharePermissions,
|
||||||
created_at: u64,
|
created_at: u64,
|
||||||
created_by: String,
|
created_by: Uuid,
|
||||||
access_count: u64,
|
access_count: u64,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -118,8 +118,8 @@ impl Share {
|
|||||||
|
|
||||||
// ── Getters ──
|
// ── Getters ──
|
||||||
|
|
||||||
pub fn id(&self) -> &str {
|
pub fn id(&self) -> Uuid {
|
||||||
&self.id
|
self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn item_id(&self) -> &str {
|
pub fn item_id(&self) -> &str {
|
||||||
@@ -150,8 +150,8 @@ impl Share {
|
|||||||
self.created_at
|
self.created_at
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn created_by(&self) -> &str {
|
pub fn created_by(&self) -> Uuid {
|
||||||
&self.created_by
|
self.created_by
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn access_count(&self) -> u64 {
|
pub fn access_count(&self) -> u64 {
|
||||||
@@ -262,13 +262,18 @@ impl TryFrom<&str> for ShareItemType {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn test_user_id() -> Uuid {
|
||||||
|
Uuid::new_v4()
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_share() {
|
fn test_create_share() {
|
||||||
|
let uid = test_user_id();
|
||||||
let share = Share::new(
|
let share = Share::new(
|
||||||
"test_file_id".to_string(),
|
"test_file_id".to_string(),
|
||||||
None,
|
None,
|
||||||
ShareItemType::File,
|
ShareItemType::File,
|
||||||
"user123".to_string(),
|
uid,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
@@ -277,7 +282,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(share.item_id(), "test_file_id");
|
assert_eq!(share.item_id(), "test_file_id");
|
||||||
assert_eq!(*share.item_type(), ShareItemType::File);
|
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().read());
|
||||||
assert!(!share.permissions().write());
|
assert!(!share.permissions().write());
|
||||||
assert!(!share.permissions().reshare());
|
assert!(!share.permissions().reshare());
|
||||||
@@ -299,7 +304,7 @@ mod tests {
|
|||||||
"test_file_id".to_string(),
|
"test_file_id".to_string(),
|
||||||
None,
|
None,
|
||||||
ShareItemType::File,
|
ShareItemType::File,
|
||||||
"user123".to_string(),
|
test_user_id(),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
Some(future),
|
Some(future),
|
||||||
@@ -314,7 +319,7 @@ mod tests {
|
|||||||
"test_file_id".to_string(),
|
"test_file_id".to_string(),
|
||||||
None,
|
None,
|
||||||
ShareItemType::File,
|
ShareItemType::File,
|
||||||
"user123".to_string(),
|
test_user_id(),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
Some(past),
|
Some(past),
|
||||||
@@ -349,7 +354,7 @@ mod tests {
|
|||||||
"test_file_id".to_string(),
|
"test_file_id".to_string(),
|
||||||
None,
|
None,
|
||||||
ShareItemType::File,
|
ShareItemType::File,
|
||||||
"user123".to_string(),
|
test_user_id(),
|
||||||
None,
|
None,
|
||||||
Some("some_hash_value".to_string()),
|
Some("some_hash_value".to_string()),
|
||||||
None,
|
None,
|
||||||
@@ -366,7 +371,7 @@ mod tests {
|
|||||||
"test_file_id".to_string(),
|
"test_file_id".to_string(),
|
||||||
None,
|
None,
|
||||||
ShareItemType::File,
|
ShareItemType::File,
|
||||||
"user123".to_string(),
|
test_user_id(),
|
||||||
None,
|
None,
|
||||||
None, // No password
|
None, // No password
|
||||||
None,
|
None,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ impl std::fmt::Display for UserRole {
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
id: String,
|
id: Uuid,
|
||||||
username: String,
|
username: String,
|
||||||
email: String,
|
email: String,
|
||||||
password_hash: String,
|
password_hash: String,
|
||||||
@@ -70,7 +70,7 @@ impl User {
|
|||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: Uuid::new_v4().to_string(),
|
id: Uuid::new_v4(),
|
||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
password_hash,
|
password_hash,
|
||||||
@@ -99,7 +99,7 @@ impl User {
|
|||||||
Self::validate_email(&email)?;
|
Self::validate_email(&email)?;
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: Uuid::new_v4().to_string(),
|
id: Uuid::new_v4(),
|
||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
password_hash: "__OIDC_NO_PASSWORD__".to_string(),
|
password_hash: "__OIDC_NO_PASSWORD__".to_string(),
|
||||||
@@ -117,7 +117,7 @@ impl User {
|
|||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn from_data(
|
pub fn from_data(
|
||||||
id: String,
|
id: Uuid,
|
||||||
username: String,
|
username: String,
|
||||||
email: String,
|
email: String,
|
||||||
password_hash: String,
|
password_hash: String,
|
||||||
@@ -148,7 +148,7 @@ impl User {
|
|||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn from_data_full(
|
pub fn from_data_full(
|
||||||
id: String,
|
id: Uuid,
|
||||||
username: String,
|
username: String,
|
||||||
email: String,
|
email: String,
|
||||||
password_hash: String,
|
password_hash: String,
|
||||||
@@ -180,8 +180,8 @@ impl User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
pub fn id(&self) -> &str {
|
pub fn id(&self) -> Uuid {
|
||||||
&self.id
|
self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn username(&self) -> &str {
|
pub fn username(&self) -> &str {
|
||||||
|
|||||||
@@ -22,23 +22,23 @@ pub trait AddressBookRepository: Send + Sync + 'static {
|
|||||||
) -> AddressBookRepositoryResult<Option<AddressBook>>;
|
) -> AddressBookRepositoryResult<Option<AddressBook>>;
|
||||||
async fn get_address_books_by_owner(
|
async fn get_address_books_by_owner(
|
||||||
&self,
|
&self,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
) -> AddressBookRepositoryResult<Vec<AddressBook>>;
|
) -> AddressBookRepositoryResult<Vec<AddressBook>>;
|
||||||
async fn get_shared_address_books(
|
async fn get_shared_address_books(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> AddressBookRepositoryResult<Vec<AddressBook>>;
|
) -> AddressBookRepositoryResult<Vec<AddressBook>>;
|
||||||
async fn get_public_address_books(&self) -> AddressBookRepositoryResult<Vec<AddressBook>>;
|
async fn get_public_address_books(&self) -> AddressBookRepositoryResult<Vec<AddressBook>>;
|
||||||
async fn share_address_book(
|
async fn share_address_book(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &Uuid,
|
address_book_id: &Uuid,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
can_write: bool,
|
can_write: bool,
|
||||||
) -> AddressBookRepositoryResult<()>;
|
) -> AddressBookRepositoryResult<()>;
|
||||||
async fn unshare_address_book(
|
async fn unshare_address_book(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &Uuid,
|
address_book_id: &Uuid,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> AddressBookRepositoryResult<()>;
|
) -> AddressBookRepositoryResult<()>;
|
||||||
async fn get_address_book_shares(
|
async fn get_address_book_shares(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -21,20 +21,20 @@ pub trait CalendarRepository: Send + Sync + 'static {
|
|||||||
/// Lists all calendars for a specific user
|
/// Lists all calendars for a specific user
|
||||||
async fn list_calendars_by_owner(
|
async fn list_calendars_by_owner(
|
||||||
&self,
|
&self,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
) -> CalendarRepositoryResult<Vec<Calendar>>;
|
) -> CalendarRepositoryResult<Vec<Calendar>>;
|
||||||
|
|
||||||
/// Finds a calendar by name and owner
|
/// Finds a calendar by name and owner
|
||||||
async fn find_calendar_by_name_and_owner(
|
async fn find_calendar_by_name_and_owner(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
) -> CalendarRepositoryResult<Calendar>;
|
) -> CalendarRepositoryResult<Calendar>;
|
||||||
|
|
||||||
/// Lists calendars shared with a specific user
|
/// Lists calendars shared with a specific user
|
||||||
async fn list_calendars_shared_with_user(
|
async fn list_calendars_shared_with_user(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> CalendarRepositoryResult<Vec<Calendar>>;
|
) -> CalendarRepositoryResult<Vec<Calendar>>;
|
||||||
|
|
||||||
/// List public calendars
|
/// List public calendars
|
||||||
@@ -48,7 +48,7 @@ pub trait CalendarRepository: Send + Sync + 'static {
|
|||||||
async fn user_has_calendar_access(
|
async fn user_has_calendar_access(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &Uuid,
|
calendar_id: &Uuid,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> CalendarRepositoryResult<bool>;
|
) -> CalendarRepositoryResult<bool>;
|
||||||
|
|
||||||
/// Gets a custom property for a calendar
|
/// Gets a custom property for a calendar
|
||||||
@@ -83,7 +83,7 @@ pub trait CalendarRepository: Send + Sync + 'static {
|
|||||||
async fn share_calendar(
|
async fn share_calendar(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &Uuid,
|
calendar_id: &Uuid,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
access_level: &str,
|
access_level: &str,
|
||||||
) -> CalendarRepositoryResult<()>;
|
) -> CalendarRepositoryResult<()>;
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ pub trait CalendarRepository: Send + Sync + 'static {
|
|||||||
async fn remove_calendar_sharing(
|
async fn remove_calendar_sharing(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &Uuid,
|
calendar_id: &Uuid,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> CalendarRepositoryResult<()>;
|
) -> CalendarRepositoryResult<()>;
|
||||||
|
|
||||||
/// Get calendar sharing information (who has access to this calendar)
|
/// Get calendar sharing information (who has access to this calendar)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::common::errors::DomainError;
|
use crate::common::errors::DomainError;
|
||||||
use crate::domain::entities::session::Session;
|
use crate::domain::entities::session::Session;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum SessionRepositoryError {
|
pub enum SessionRepositoryError {
|
||||||
@@ -33,7 +34,7 @@ pub trait SessionRepository: Send + Sync + 'static {
|
|||||||
async fn create_session(&self, session: Session) -> SessionRepositoryResult<Session>;
|
async fn create_session(&self, session: Session) -> SessionRepositoryResult<Session>;
|
||||||
|
|
||||||
/// Gets a session by ID
|
/// 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
|
/// Gets a session by refresh token
|
||||||
async fn get_session_by_refresh_token(
|
async fn get_session_by_refresh_token(
|
||||||
@@ -42,14 +43,14 @@ pub trait SessionRepository: Send + Sync + 'static {
|
|||||||
) -> SessionRepositoryResult<Session>;
|
) -> SessionRepositoryResult<Session>;
|
||||||
|
|
||||||
/// Gets all sessions for a user
|
/// 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>>;
|
-> SessionRepositoryResult<Vec<Session>>;
|
||||||
|
|
||||||
/// Revokes a specific 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
|
/// 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
|
/// Deletes expired sessions
|
||||||
async fn delete_expired_sessions(&self) -> SessionRepositoryResult<u64>;
|
async fn delete_expired_sessions(&self) -> SessionRepositoryResult<u64>;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::common::errors::DomainError;
|
use crate::common::errors::DomainError;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Repository for platform settings stored in the database.
|
/// Repository for platform settings stored in the database.
|
||||||
/// Settings are key-value pairs organized by category (e.g., "oidc", "general").
|
/// Settings are key-value pairs organized by category (e.g., "oidc", "general").
|
||||||
@@ -18,7 +19,7 @@ pub trait SettingsRepository: Send + Sync + 'static {
|
|||||||
value: &str,
|
value: &str,
|
||||||
category: &str,
|
category: &str,
|
||||||
is_secret: bool,
|
is_secret: bool,
|
||||||
updated_by: Option<&str>,
|
updated_by: Option<Uuid>,
|
||||||
) -> Result<(), DomainError>;
|
) -> Result<(), DomainError>;
|
||||||
|
|
||||||
/// Delete a setting by key
|
/// Delete a setting by key
|
||||||
@@ -34,7 +35,7 @@ pub trait SettingsRepository: Send + Sync + 'static {
|
|||||||
/// The default implementation falls back to the non-atomic
|
/// The default implementation falls back to the non-atomic
|
||||||
/// get-then-set pattern for repositories that don't support a native
|
/// get-then-set pattern for repositories that don't support a native
|
||||||
/// atomic upsert.
|
/// 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)
|
// Default: non-atomic fallback (overridden by PG implementation)
|
||||||
match self.get("system_initialized").await? {
|
match self.get("system_initialized").await? {
|
||||||
Some(v) if v == "true" => Ok(false),
|
Some(v) if v == "true" => Ok(false),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::domain::{
|
use crate::domain::{
|
||||||
entities::share::{Share, ShareItemType},
|
entities::share::{Share, ShareItemType},
|
||||||
@@ -24,7 +25,7 @@ pub trait ShareRepository: Send + Sync + 'static {
|
|||||||
async fn save(&self, share: &Share) -> Result<Share, ShareRepositoryError>;
|
async fn save(&self, share: &Share) -> Result<Share, ShareRepositoryError>;
|
||||||
|
|
||||||
/// Find a share by its ID
|
/// 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
|
/// Find a share by its token
|
||||||
async fn find_by_token(&self, token: &str) -> Result<Share, ShareRepositoryError>;
|
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>;
|
) -> Result<Vec<Share>, ShareRepositoryError>;
|
||||||
|
|
||||||
/// Delete a share by its ID
|
/// 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
|
/// 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)
|
/// Find all shares (admin operation)
|
||||||
async fn find_all(&self) -> Result<Vec<Share>, ShareRepositoryError>;
|
async fn find_all(&self) -> Result<Vec<Share>, ShareRepositoryError>;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::common::errors::DomainError;
|
use crate::common::errors::DomainError;
|
||||||
use crate::domain::entities::user::{User, UserRole};
|
use crate::domain::entities::user::{User, UserRole};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum UserRepositoryError {
|
pub enum UserRepositoryError {
|
||||||
@@ -45,7 +46,7 @@ pub trait UserRepository: Send + Sync + 'static {
|
|||||||
async fn create_user(&self, user: User) -> UserRepositoryResult<User>;
|
async fn create_user(&self, user: User) -> UserRepositoryResult<User>;
|
||||||
|
|
||||||
/// Gets a user by ID
|
/// 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
|
/// Gets a user by username
|
||||||
async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult<User>;
|
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
|
/// Updates only a user's storage usage
|
||||||
async fn update_storage_usage(
|
async fn update_storage_usage(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
usage_bytes: i64,
|
usage_bytes: i64,
|
||||||
) -> UserRepositoryResult<()>;
|
) -> UserRepositoryResult<()>;
|
||||||
|
|
||||||
/// Updates the last login date
|
/// 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
|
/// Lists users with pagination
|
||||||
async fn list_users(&self, limit: i64, offset: i64) -> UserRepositoryResult<Vec<User>>;
|
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>>;
|
async fn search_users(&self, query: &str, limit: i64) -> UserRepositoryResult<Vec<User>>;
|
||||||
|
|
||||||
/// Activates or deactivates a 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<()>;
|
-> UserRepositoryResult<()>;
|
||||||
|
|
||||||
/// Changes a user's password
|
/// 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<()>;
|
-> UserRepositoryResult<()>;
|
||||||
|
|
||||||
/// Changes a user's role
|
/// 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)
|
/// Lists users by role (admin or user)
|
||||||
async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult<Vec<User>>;
|
async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult<Vec<User>>;
|
||||||
|
|
||||||
/// Deletes a 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
|
/// Finds a user by OIDC provider + subject pair
|
||||||
async fn get_user_by_oidc_subject(
|
async fn get_user_by_oidc_subject(
|
||||||
@@ -99,7 +100,7 @@ pub trait UserRepository: Send + Sync + 'static {
|
|||||||
/// Updates a user's storage quota
|
/// Updates a user's storage quota
|
||||||
async fn update_storage_quota(
|
async fn update_storage_quota(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
quota_bytes: i64,
|
quota_bytes: i64,
|
||||||
) -> UserRepositoryResult<()>;
|
) -> UserRepositoryResult<()>;
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ impl CalendarStoragePort for CalendarStorageAdapter {
|
|||||||
async fn create_calendar(
|
async fn create_calendar(
|
||||||
&self,
|
&self,
|
||||||
dto: CreateCalendarDto,
|
dto: CreateCalendarDto,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
) -> Result<CalendarDto, DomainError> {
|
) -> Result<CalendarDto, DomainError> {
|
||||||
let calendar = Calendar::new(dto.name, owner_id.to_string(), dto.description, dto.color)?;
|
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(
|
async fn list_calendars_by_owner(
|
||||||
&self,
|
&self,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
) -> Result<Vec<CalendarDto>, DomainError> {
|
) -> Result<Vec<CalendarDto>, DomainError> {
|
||||||
let calendars = self
|
let calendars = self
|
||||||
.calendar_repository
|
.calendar_repository
|
||||||
@@ -128,7 +128,7 @@ impl CalendarStoragePort for CalendarStorageAdapter {
|
|||||||
|
|
||||||
async fn list_calendars_shared_with_user(
|
async fn list_calendars_shared_with_user(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<CalendarDto>, DomainError> {
|
) -> Result<Vec<CalendarDto>, DomainError> {
|
||||||
let calendars = self
|
let calendars = self
|
||||||
.calendar_repository
|
.calendar_repository
|
||||||
@@ -152,7 +152,7 @@ impl CalendarStoragePort for CalendarStorageAdapter {
|
|||||||
async fn check_calendar_access(
|
async fn check_calendar_access(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &str,
|
calendar_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<bool, DomainError> {
|
) -> Result<bool, DomainError> {
|
||||||
let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
|
let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
|
||||||
DomainError::new(
|
DomainError::new(
|
||||||
@@ -172,7 +172,7 @@ impl CalendarStoragePort for CalendarStorageAdapter {
|
|||||||
async fn share_calendar(
|
async fn share_calendar(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &str,
|
calendar_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
access_level: &str,
|
access_level: &str,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
|
let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
|
||||||
@@ -191,7 +191,7 @@ impl CalendarStoragePort for CalendarStorageAdapter {
|
|||||||
async fn remove_calendar_sharing(
|
async fn remove_calendar_sharing(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &str,
|
calendar_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
|
let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
|
||||||
DomainError::new(
|
DomainError::new(
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ impl ContactStorageAdapter {
|
|||||||
async fn check_address_book_access(
|
async fn check_address_book_access(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &Uuid,
|
address_book_id: &Uuid,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<AddressBook, DomainError> {
|
) -> Result<AddressBook, DomainError> {
|
||||||
let address_book = self
|
let address_book = self
|
||||||
.address_book_repository
|
.address_book_repository
|
||||||
@@ -72,7 +72,7 @@ impl ContactStorageAdapter {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Check if user is owner
|
// 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);
|
return Ok(address_book);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ impl ContactStorageAdapter {
|
|||||||
.address_book_repository
|
.address_book_repository
|
||||||
.get_address_book_shares(address_book_id)
|
.get_address_book_shares(address_book_id)
|
||||||
.await?;
|
.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);
|
return Ok(address_book);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ impl ContactStorageAdapter {
|
|||||||
async fn check_write_access(
|
async fn check_write_access(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &Uuid,
|
address_book_id: &Uuid,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<AddressBook, DomainError> {
|
) -> Result<AddressBook, DomainError> {
|
||||||
let address_book = self
|
let address_book = self
|
||||||
.address_book_repository
|
.address_book_repository
|
||||||
@@ -112,7 +112,7 @@ impl ContactStorageAdapter {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Owner always has write access
|
// 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);
|
return Ok(address_book);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ impl ContactStorageAdapter {
|
|||||||
.await?;
|
.await?;
|
||||||
if shares
|
if shares
|
||||||
.iter()
|
.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);
|
return Ok(address_book);
|
||||||
}
|
}
|
||||||
@@ -247,7 +247,10 @@ impl AddressBookUseCase for ContactStorageAdapter {
|
|||||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
||||||
|
|
||||||
// Check write access
|
// 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 {
|
if let Some(name) = update.name {
|
||||||
address_book.set_name(name);
|
address_book.set_name(name);
|
||||||
@@ -273,7 +276,7 @@ impl AddressBookUseCase for ContactStorageAdapter {
|
|||||||
async fn delete_address_book(
|
async fn delete_address_book(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &str,
|
address_book_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
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")
|
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(
|
return Err(DomainError::new(
|
||||||
ErrorKind::AccessDenied,
|
ErrorKind::AccessDenied,
|
||||||
"AddressBook",
|
"AddressBook",
|
||||||
@@ -302,7 +305,7 @@ impl AddressBookUseCase for ContactStorageAdapter {
|
|||||||
async fn get_address_book(
|
async fn get_address_book(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &str,
|
address_book_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<AddressBookDto, DomainError> {
|
) -> Result<AddressBookDto, DomainError> {
|
||||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
||||||
let address_book = self.check_address_book_access(&uuid, user_id).await?;
|
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(
|
async fn list_user_address_books(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<AddressBookDto>, DomainError> {
|
) -> Result<Vec<AddressBookDto>, DomainError> {
|
||||||
let owned = self
|
let owned = self
|
||||||
.address_book_repository
|
.address_book_repository
|
||||||
@@ -339,7 +342,7 @@ impl AddressBookUseCase for ContactStorageAdapter {
|
|||||||
async fn share_address_book(
|
async fn share_address_book(
|
||||||
&self,
|
&self,
|
||||||
dto: ShareAddressBookDto,
|
dto: ShareAddressBookDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
let uuid = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
|
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")
|
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(
|
return Err(DomainError::new(
|
||||||
ErrorKind::AccessDenied,
|
ErrorKind::AccessDenied,
|
||||||
"AddressBook",
|
"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
|
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
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn unshare_address_book(
|
async fn unshare_address_book(
|
||||||
&self,
|
&self,
|
||||||
dto: UnshareAddressBookDto,
|
dto: UnshareAddressBookDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
let uuid = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
|
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")
|
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(
|
return Err(DomainError::new(
|
||||||
ErrorKind::AccessDenied,
|
ErrorKind::AccessDenied,
|
||||||
"AddressBook",
|
"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
|
self.address_book_repository
|
||||||
.unshare_address_book(&uuid, &dto.user_id)
|
.unshare_address_book(&uuid, target_user_id)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_address_book_shares(
|
async fn get_address_book_shares(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &str,
|
address_book_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<(String, bool)>, DomainError> {
|
) -> Result<Vec<(String, bool)>, DomainError> {
|
||||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
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")
|
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(
|
return Err(DomainError::new(
|
||||||
ErrorKind::AccessDenied,
|
ErrorKind::AccessDenied,
|
||||||
"AddressBook",
|
"AddressBook",
|
||||||
@@ -429,7 +440,10 @@ impl ContactUseCase for ContactStorageAdapter {
|
|||||||
let address_book_id = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
|
let address_book_id = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
|
||||||
|
|
||||||
// Check write access
|
// 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?;
|
.await?;
|
||||||
|
|
||||||
let now = chrono::Utc::now();
|
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")?;
|
let address_book_id = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
|
||||||
|
|
||||||
// Check write access
|
// 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?;
|
.await?;
|
||||||
|
|
||||||
// Parse vCard fields
|
// Parse vCard fields
|
||||||
@@ -591,7 +608,10 @@ impl ContactUseCase for ContactStorageAdapter {
|
|||||||
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?;
|
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?;
|
||||||
|
|
||||||
// Check write access to the address book
|
// 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?;
|
.await?;
|
||||||
|
|
||||||
if let Some(full_name) = update.full_name {
|
if let Some(full_name) = update.full_name {
|
||||||
@@ -643,7 +663,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
|||||||
Ok(ContactDto::from(updated))
|
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 uuid = Self::parse_uuid(contact_id, "Contact")?;
|
||||||
|
|
||||||
let contact = self
|
let contact = self
|
||||||
@@ -662,7 +682,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
|||||||
async fn get_contact(
|
async fn get_contact(
|
||||||
&self,
|
&self,
|
||||||
contact_id: &str,
|
contact_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<ContactDto, DomainError> {
|
) -> Result<ContactDto, DomainError> {
|
||||||
let uuid = Self::parse_uuid(contact_id, "Contact")?;
|
let uuid = Self::parse_uuid(contact_id, "Contact")?;
|
||||||
|
|
||||||
@@ -682,7 +702,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
|||||||
async fn list_contacts(
|
async fn list_contacts(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &str,
|
address_book_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<ContactDto>, DomainError> {
|
) -> Result<Vec<ContactDto>, DomainError> {
|
||||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
||||||
|
|
||||||
@@ -700,7 +720,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
|||||||
&self,
|
&self,
|
||||||
address_book_id: &str,
|
address_book_id: &str,
|
||||||
query: &str,
|
query: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<ContactDto>, DomainError> {
|
) -> Result<Vec<ContactDto>, DomainError> {
|
||||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
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")?;
|
let address_book_id = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
|
||||||
|
|
||||||
// Check write access
|
// 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?;
|
.await?;
|
||||||
|
|
||||||
let group = ContactGroup::new(address_book_id, dto.name);
|
let group = ContactGroup::new(address_book_id, dto.name);
|
||||||
@@ -746,7 +769,10 @@ impl ContactUseCase for ContactStorageAdapter {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Check write access
|
// 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?;
|
.await?;
|
||||||
|
|
||||||
group.set_name(update.name);
|
group.set_name(update.name);
|
||||||
@@ -756,7 +782,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
|||||||
Ok(ContactGroupDto::from(updated))
|
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 uuid = Self::parse_uuid(group_id, "ContactGroup")?;
|
||||||
|
|
||||||
let group = self
|
let group = self
|
||||||
@@ -777,7 +803,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
|||||||
async fn get_group(
|
async fn get_group(
|
||||||
&self,
|
&self,
|
||||||
group_id: &str,
|
group_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<ContactGroupDto, DomainError> {
|
) -> Result<ContactGroupDto, DomainError> {
|
||||||
let uuid = Self::parse_uuid(group_id, "ContactGroup")?;
|
let uuid = Self::parse_uuid(group_id, "ContactGroup")?;
|
||||||
|
|
||||||
@@ -799,7 +825,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
|||||||
async fn list_groups(
|
async fn list_groups(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &str,
|
address_book_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<ContactGroupDto>, DomainError> {
|
) -> Result<Vec<ContactGroupDto>, DomainError> {
|
||||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
||||||
|
|
||||||
@@ -816,7 +842,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
|||||||
async fn add_contact_to_group(
|
async fn add_contact_to_group(
|
||||||
&self,
|
&self,
|
||||||
dto: GroupMembershipDto,
|
dto: GroupMembershipDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
let group_id = Self::parse_uuid(&dto.group_id, "ContactGroup")?;
|
let group_id = Self::parse_uuid(&dto.group_id, "ContactGroup")?;
|
||||||
let contact_id = Self::parse_uuid(&dto.contact_id, "Contact")?;
|
let contact_id = Self::parse_uuid(&dto.contact_id, "Contact")?;
|
||||||
@@ -841,7 +867,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
|||||||
async fn remove_contact_from_group(
|
async fn remove_contact_from_group(
|
||||||
&self,
|
&self,
|
||||||
dto: GroupMembershipDto,
|
dto: GroupMembershipDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
let group_id = Self::parse_uuid(&dto.group_id, "ContactGroup")?;
|
let group_id = Self::parse_uuid(&dto.group_id, "ContactGroup")?;
|
||||||
let contact_id = Self::parse_uuid(&dto.contact_id, "Contact")?;
|
let contact_id = Self::parse_uuid(&dto.contact_id, "Contact")?;
|
||||||
@@ -866,7 +892,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
|||||||
async fn list_contacts_in_group(
|
async fn list_contacts_in_group(
|
||||||
&self,
|
&self,
|
||||||
group_id: &str,
|
group_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<ContactDto>, DomainError> {
|
) -> Result<Vec<ContactDto>, DomainError> {
|
||||||
let uuid = Self::parse_uuid(group_id, "ContactGroup")?;
|
let uuid = Self::parse_uuid(group_id, "ContactGroup")?;
|
||||||
|
|
||||||
@@ -889,7 +915,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
|||||||
async fn list_groups_for_contact(
|
async fn list_groups_for_contact(
|
||||||
&self,
|
&self,
|
||||||
contact_id: &str,
|
contact_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<ContactGroupDto>, DomainError> {
|
) -> Result<Vec<ContactGroupDto>, DomainError> {
|
||||||
let uuid = Self::parse_uuid(contact_id, "Contact")?;
|
let uuid = Self::parse_uuid(contact_id, "Contact")?;
|
||||||
|
|
||||||
@@ -910,7 +936,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
|||||||
async fn get_contact_vcard(
|
async fn get_contact_vcard(
|
||||||
&self,
|
&self,
|
||||||
contact_id: &str,
|
contact_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<String, DomainError> {
|
) -> Result<String, DomainError> {
|
||||||
let uuid = Self::parse_uuid(contact_id, "Contact")?;
|
let uuid = Self::parse_uuid(contact_id, "Contact")?;
|
||||||
|
|
||||||
@@ -930,7 +956,7 @@ impl ContactUseCase for ContactStorageAdapter {
|
|||||||
async fn get_contacts_as_vcards(
|
async fn get_contacts_as_vcards(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &str,
|
address_book_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<(String, String)>, DomainError> {
|
) -> Result<Vec<(String, String)>, DomainError> {
|
||||||
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ impl AddressBookRepository for AddressBookPgRepository {
|
|||||||
|
|
||||||
async fn get_address_books_by_owner(
|
async fn get_address_books_by_owner(
|
||||||
&self,
|
&self,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
) -> AddressBookRepositoryResult<Vec<AddressBook>> {
|
) -> AddressBookRepositoryResult<Vec<AddressBook>> {
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -182,7 +182,7 @@ impl AddressBookRepository for AddressBookPgRepository {
|
|||||||
|
|
||||||
async fn get_shared_address_books(
|
async fn get_shared_address_books(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> AddressBookRepositoryResult<Vec<AddressBook>> {
|
) -> AddressBookRepositoryResult<Vec<AddressBook>> {
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -254,7 +254,7 @@ impl AddressBookRepository for AddressBookPgRepository {
|
|||||||
async fn share_address_book(
|
async fn share_address_book(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &Uuid,
|
address_book_id: &Uuid,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
can_write: bool,
|
can_write: bool,
|
||||||
) -> AddressBookRepositoryResult<()> {
|
) -> AddressBookRepositoryResult<()> {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
@@ -277,7 +277,7 @@ impl AddressBookRepository for AddressBookPgRepository {
|
|||||||
async fn unshare_address_book(
|
async fn unshare_address_book(
|
||||||
&self,
|
&self,
|
||||||
address_book_id: &Uuid,
|
address_book_id: &Uuid,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> AddressBookRepositoryResult<()> {
|
) -> AddressBookRepositoryResult<()> {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use crate::domain::entities::app_password::AppPassword;
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub struct AppPasswordPgRepository {
|
pub struct AppPasswordPgRepository {
|
||||||
pool: Arc<PgPool>,
|
pool: Arc<PgPool>,
|
||||||
@@ -48,7 +49,7 @@ impl AppPasswordStoragePort for AppPasswordPgRepository {
|
|||||||
Ok(ap)
|
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>(
|
let rows = sqlx::query_as::<_, AppPasswordRow>(
|
||||||
r#"
|
r#"
|
||||||
SELECT id, user_id, label, password_hash, prefix, scopes,
|
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())
|
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>(
|
let row = sqlx::query_as::<_, AppPasswordRow>(
|
||||||
r#"
|
r#"
|
||||||
SELECT id, user_id, label, password_hash, prefix, scopes,
|
SELECT id, user_id, label, password_hash, prefix, scopes,
|
||||||
@@ -79,12 +80,12 @@ impl AppPasswordStoragePort for AppPasswordPgRepository {
|
|||||||
.fetch_optional(self.pool())
|
.fetch_optional(self.pool())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::internal_error("AppPasswordPg", format!("get_by_id: {e}")))?
|
.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())
|
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>(
|
let rows = sqlx::query_as::<_, AppPasswordRow>(
|
||||||
r#"
|
r#"
|
||||||
SELECT id, user_id, label, password_hash, prefix, scopes,
|
SELECT id, user_id, label, password_hash, prefix, scopes,
|
||||||
@@ -105,7 +106,7 @@ impl AppPasswordStoragePort for AppPasswordPgRepository {
|
|||||||
|
|
||||||
async fn get_active_by_user_prefix(
|
async fn get_active_by_user_prefix(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
) -> Result<Vec<AppPassword>, DomainError> {
|
) -> Result<Vec<AppPassword>, DomainError> {
|
||||||
let rows = sqlx::query_as::<_, AppPasswordRow>(
|
let rows = sqlx::query_as::<_, AppPasswordRow>(
|
||||||
@@ -131,7 +132,7 @@ impl AppPasswordStoragePort for AppPasswordPgRepository {
|
|||||||
Ok(rows.into_iter().map(|r| r.into()).collect())
|
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")
|
sqlx::query("UPDATE auth.app_passwords SET last_used_at = NOW() WHERE id = $1")
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.execute(self.pool())
|
.execute(self.pool())
|
||||||
@@ -140,7 +141,7 @@ impl AppPasswordStoragePort for AppPasswordPgRepository {
|
|||||||
Ok(())
|
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(
|
let result = sqlx::query(
|
||||||
"UPDATE auth.app_passwords SET active = FALSE WHERE id = $1 AND user_id = $2",
|
"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}")))?;
|
.map_err(|e| DomainError::internal_error("AppPasswordPg", format!("revoke: {e}")))?;
|
||||||
|
|
||||||
if result.rows_affected() == 0 {
|
if result.rows_affected() == 0 {
|
||||||
return Err(DomainError::not_found("AppPassword", id));
|
return Err(DomainError::not_found("AppPassword", id.to_string()));
|
||||||
}
|
}
|
||||||
Ok(())
|
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")
|
let result = sqlx::query("DELETE FROM auth.app_passwords WHERE id = $1 AND user_id = $2")
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
@@ -190,8 +191,8 @@ impl AppPasswordStoragePort for AppPasswordPgRepository {
|
|||||||
/// Internal row struct for sqlx mapping.
|
/// Internal row struct for sqlx mapping.
|
||||||
#[derive(sqlx::FromRow)]
|
#[derive(sqlx::FromRow)]
|
||||||
struct AppPasswordRow {
|
struct AppPasswordRow {
|
||||||
id: String,
|
id: Uuid,
|
||||||
user_id: String,
|
user_id: Uuid,
|
||||||
label: String,
|
label: String,
|
||||||
password_hash: String,
|
password_hash: String,
|
||||||
prefix: String,
|
prefix: String,
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ impl CalendarRepository for CalendarPgRepository {
|
|||||||
|
|
||||||
async fn list_calendars_by_owner(
|
async fn list_calendars_by_owner(
|
||||||
&self,
|
&self,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
) -> CalendarRepositoryResult<Vec<Calendar>> {
|
) -> CalendarRepositoryResult<Vec<Calendar>> {
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -180,7 +180,7 @@ impl CalendarRepository for CalendarPgRepository {
|
|||||||
async fn find_calendar_by_name_and_owner(
|
async fn find_calendar_by_name_and_owner(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
) -> CalendarRepositoryResult<Calendar> {
|
) -> CalendarRepositoryResult<Calendar> {
|
||||||
let row = sqlx::query(
|
let row = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -218,7 +218,7 @@ impl CalendarRepository for CalendarPgRepository {
|
|||||||
|
|
||||||
async fn list_calendars_shared_with_user(
|
async fn list_calendars_shared_with_user(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> CalendarRepositoryResult<Vec<Calendar>> {
|
) -> CalendarRepositoryResult<Vec<Calendar>> {
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -299,7 +299,7 @@ impl CalendarRepository for CalendarPgRepository {
|
|||||||
async fn user_has_calendar_access(
|
async fn user_has_calendar_access(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &Uuid,
|
calendar_id: &Uuid,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> CalendarRepositoryResult<bool> {
|
) -> CalendarRepositoryResult<bool> {
|
||||||
// Check if the user is the owner of the calendar or has a share
|
// Check if the user is the owner of the calendar or has a share
|
||||||
let row = sqlx::query(
|
let row = sqlx::query(
|
||||||
@@ -327,7 +327,7 @@ impl CalendarRepository for CalendarPgRepository {
|
|||||||
async fn share_calendar(
|
async fn share_calendar(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &Uuid,
|
calendar_id: &Uuid,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
access_level: &str,
|
access_level: &str,
|
||||||
) -> CalendarRepositoryResult<()> {
|
) -> CalendarRepositoryResult<()> {
|
||||||
// Validate access level
|
// Validate access level
|
||||||
@@ -358,7 +358,7 @@ impl CalendarRepository for CalendarPgRepository {
|
|||||||
async fn remove_calendar_sharing(
|
async fn remove_calendar_sharing(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &Uuid,
|
calendar_id: &Uuid,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> CalendarRepositoryResult<()> {
|
) -> CalendarRepositoryResult<()> {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use sqlx::{PgPool, Row};
|
use sqlx::{PgPool, Row};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::ports::auth_ports::DeviceCodeStoragePort;
|
use crate::application::ports::auth_ports::DeviceCodeStoragePort;
|
||||||
use crate::common::errors::{DomainError, ErrorKind};
|
use crate::common::errors::{DomainError, ErrorKind};
|
||||||
@@ -28,7 +29,7 @@ impl DeviceCodePgRepository {
|
|||||||
let status = DeviceCodeStatus::parse(&status_str).unwrap_or(DeviceCodeStatus::Expired);
|
let status = DeviceCodeStatus::parse(&status_str).unwrap_or(DeviceCodeStatus::Expired);
|
||||||
|
|
||||||
Ok(DeviceCode::from_raw(
|
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("device_code").unwrap_or_default(),
|
||||||
row.try_get("user_code").unwrap_or_default(),
|
row.try_get("user_code").unwrap_or_default(),
|
||||||
row.try_get("client_name").unwrap_or_default(),
|
row.try_get("client_name").unwrap_or_default(),
|
||||||
@@ -212,7 +213,7 @@ impl DeviceCodeStoragePort for DeviceCodePgRepository {
|
|||||||
Ok(result.rows_affected())
|
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(
|
let rows = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
SELECT id, device_code, user_code, client_name, scopes,
|
SELECT id, device_code, user_code, client_name, scopes,
|
||||||
@@ -239,7 +240,7 @@ impl DeviceCodeStoragePort for DeviceCodePgRepository {
|
|||||||
rows.iter().map(Self::map_row).collect()
|
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")
|
sqlx::query("DELETE FROM auth.device_codes WHERE id = $1")
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.execute(self.pool.as_ref())
|
.execute(self.pool.as_ref())
|
||||||
|
|||||||
@@ -20,9 +20,7 @@ impl FavoritesPgRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FavoritesRepositoryPort for FavoritesPgRepository {
|
impl FavoritesRepositoryPort for FavoritesPgRepository {
|
||||||
async fn get_favorites(&self, user_id: &str) -> Result<Vec<FavoriteItemDto>> {
|
async fn get_favorites(&self, user_id: Uuid) -> Result<Vec<FavoriteItemDto>> {
|
||||||
let user_uuid = Uuid::parse_str(user_id)?;
|
|
||||||
|
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -41,12 +39,12 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
|
|||||||
AND f.id = uf.item_id::UUID
|
AND f.id = uf.item_id::UUID
|
||||||
LEFT JOIN storage.folders fld ON uf.item_type = 'folder'
|
LEFT JOIN storage.folders fld ON uf.item_type = 'folder'
|
||||||
AND fld.id = uf.item_id::UUID
|
AND fld.id = uf.item_id::UUID
|
||||||
WHERE uf.user_id = $1::TEXT
|
WHERE uf.user_id = $1
|
||||||
ORDER BY uf.created_at DESC
|
ORDER BY uf.created_at DESC
|
||||||
LIMIT 500
|
LIMIT 500
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(user_uuid)
|
.bind(user_id)
|
||||||
.fetch_all(&*self.db_pool)
|
.fetch_all(&*self.db_pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
@@ -85,17 +83,15 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
|
|||||||
Ok(favorites)
|
Ok(favorites)
|
||||||
}
|
}
|
||||||
|
|
||||||
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<()> {
|
||||||
let user_uuid = Uuid::parse_str(user_id)?;
|
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO auth.user_favorites (user_id, item_id, item_type)
|
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
|
ON CONFLICT (user_id, item_id, item_type) DO NOTHING
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(user_uuid)
|
.bind(user_id)
|
||||||
.bind(item_id)
|
.bind(item_id)
|
||||||
.bind(item_type)
|
.bind(item_type)
|
||||||
.execute(&*self.db_pool)
|
.execute(&*self.db_pool)
|
||||||
@@ -112,16 +108,14 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
||||||
let user_uuid = Uuid::parse_str(user_id)?;
|
|
||||||
|
|
||||||
let result = sqlx::query(
|
let result = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
DELETE FROM auth.user_favorites
|
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_id)
|
||||||
.bind(item_type)
|
.bind(item_type)
|
||||||
.execute(&*self.db_pool)
|
.execute(&*self.db_pool)
|
||||||
@@ -138,18 +132,16 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
|
|||||||
Ok(result.rows_affected() > 0)
|
Ok(result.rows_affected() > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
||||||
let user_uuid = Uuid::parse_str(user_id)?;
|
|
||||||
|
|
||||||
let row = sqlx::query(
|
let row = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
SELECT EXISTS (
|
SELECT EXISTS (
|
||||||
SELECT 1 FROM auth.user_favorites
|
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"
|
) AS "is_favorite"
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(user_uuid)
|
.bind(user_id)
|
||||||
.bind(item_id)
|
.bind(item_id)
|
||||||
.bind(item_type)
|
.bind(item_type)
|
||||||
.fetch_one(&*self.db_pool)
|
.fetch_one(&*self.db_pool)
|
||||||
@@ -166,13 +158,11 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
|
|||||||
Ok(row.try_get("is_favorite").unwrap_or(false))
|
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() {
|
if items.is_empty() {
|
||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_uuid = Uuid::parse_str(user_id)?;
|
|
||||||
|
|
||||||
// Validate all item_types upfront
|
// Validate all item_types upfront
|
||||||
for (_, item_type) in items {
|
for (_, item_type) in items {
|
||||||
if item_type != "file" && item_type != "folder" {
|
if item_type != "file" && item_type != "folder" {
|
||||||
@@ -210,7 +200,7 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
|
|||||||
query.push_str(", ");
|
query.push_str(", ");
|
||||||
}
|
}
|
||||||
query.push_str(&format!(
|
query.push_str(&format!(
|
||||||
"(${}::TEXT, ${}, ${})",
|
"(${}, ${}, ${})",
|
||||||
param_idx,
|
param_idx,
|
||||||
param_idx + 1,
|
param_idx + 1,
|
||||||
param_idx + 2
|
param_idx + 2
|
||||||
@@ -222,7 +212,7 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
|
|||||||
|
|
||||||
let mut q = sqlx::query(&query);
|
let mut q = sqlx::query(&query);
|
||||||
for (item_id, item_type) in chunk {
|
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| {
|
let result = q.execute(&mut *tx).await.map_err(|e| {
|
||||||
@@ -251,22 +241,20 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
|
|||||||
|
|
||||||
async fn batch_check_favorites(
|
async fn batch_check_favorites(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
item_ids: &[(&str, &str)],
|
item_ids: &[(&str, &str)],
|
||||||
) -> Result<HashSet<String>> {
|
) -> Result<HashSet<String>> {
|
||||||
if item_ids.is_empty() {
|
if item_ids.is_empty() {
|
||||||
return Ok(HashSet::new());
|
return Ok(HashSet::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_uuid = Uuid::parse_str(user_id)?;
|
|
||||||
|
|
||||||
// Collect just the IDs for the IN clause
|
// Collect just the IDs for the IN clause
|
||||||
let ids: Vec<String> = item_ids.iter().map(|(id, _)| id.to_string()).collect();
|
let ids: Vec<String> = item_ids.iter().map(|(id, _)| id.to_string()).collect();
|
||||||
|
|
||||||
let rows = sqlx::query(
|
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)
|
.bind(&ids)
|
||||||
.fetch_all(&*self.db_pool)
|
.fetch_all(&*self.db_pool)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ use crate::common::errors::DomainError;
|
|||||||
use crate::domain::entities::file::File;
|
use crate::domain::entities::file::File;
|
||||||
use crate::domain::services::path_service::StoragePath;
|
use crate::domain::services::path_service::StoragePath;
|
||||||
use crate::infrastructure::services::dedup_service::DedupService;
|
use crate::infrastructure::services::dedup_service::DedupService;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Type alias for file metadata rows from SQL queries.
|
/// Type alias for file metadata rows from SQL queries.
|
||||||
type FileRow = (
|
type FileRow = (
|
||||||
@@ -166,7 +167,7 @@ impl FileBlobReadRepository {
|
|||||||
/// `sort_date` epoch for each file (used as pagination cursor).
|
/// `sort_date` epoch for each file (used as pagination cursor).
|
||||||
pub async fn list_media_files(
|
pub async fn list_media_files(
|
||||||
&self,
|
&self,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
before: Option<i64>,
|
before: Option<i64>,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
) -> Result<(Vec<File>, Vec<i64>), DomainError> {
|
) -> 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::<
|
let row = sqlx::query_as::<
|
||||||
_,
|
_,
|
||||||
(
|
(
|
||||||
@@ -286,7 +287,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.bind(owner_id)
|
.bind(owner_id.to_string())
|
||||||
.fetch_optional(self.pool.as_ref())
|
.fetch_optional(self.pool.as_ref())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::internal_error("FileBlobRead", format!("get_for_owner: {e}")))?
|
.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(
|
async fn list_files_for_owner(
|
||||||
&self,
|
&self,
|
||||||
folder_id: Option<&str>,
|
folder_id: Option<&str>,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
) -> Result<Vec<File>, DomainError> {
|
) -> Result<Vec<File>, DomainError> {
|
||||||
|
let owner_str = owner_id.to_string();
|
||||||
let rows: Vec<FileRow> = if let Some(fid) = folder_id {
|
let rows: Vec<FileRow> = if let Some(fid) = folder_id {
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
r#"
|
r#"
|
||||||
@@ -368,7 +370,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(fid)
|
.bind(fid)
|
||||||
.bind(owner_id)
|
.bind(&owner_str)
|
||||||
.fetch_all(self.pool.as_ref())
|
.fetch_all(self.pool.as_ref())
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
@@ -386,7 +388,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
ORDER BY fi.name
|
ORDER BY fi.name
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(owner_id)
|
.bind(&owner_str)
|
||||||
.fetch_all(self.pool.as_ref())
|
.fetch_all(self.pool.as_ref())
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -468,10 +470,11 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
async fn list_files_batch_for_owner(
|
async fn list_files_batch_for_owner(
|
||||||
&self,
|
&self,
|
||||||
folder_id: Option<&str>,
|
folder_id: Option<&str>,
|
||||||
owner_id: &str,
|
owner_id: Uuid,
|
||||||
offset: i64,
|
offset: i64,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
) -> Result<Vec<File>, DomainError> {
|
) -> Result<Vec<File>, DomainError> {
|
||||||
|
let owner_str = owner_id.to_string();
|
||||||
let rows: Vec<FileRow> = if let Some(fid) = folder_id {
|
let rows: Vec<FileRow> = if let Some(fid) = folder_id {
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
r#"
|
r#"
|
||||||
@@ -491,7 +494,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
.bind(fid)
|
.bind(fid)
|
||||||
.bind(limit)
|
.bind(limit)
|
||||||
.bind(offset)
|
.bind(offset)
|
||||||
.bind(owner_id)
|
.bind(&owner_str)
|
||||||
.fetch_all(self.pool.as_ref())
|
.fetch_all(self.pool.as_ref())
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
@@ -512,7 +515,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
)
|
)
|
||||||
.bind(limit)
|
.bind(limit)
|
||||||
.bind(offset)
|
.bind(offset)
|
||||||
.bind(owner_id)
|
.bind(&owner_str)
|
||||||
.fetch_all(self.pool.as_ref())
|
.fetch_all(self.pool.as_ref())
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -753,7 +756,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
&self,
|
&self,
|
||||||
folder_id: Option<&str>,
|
folder_id: Option<&str>,
|
||||||
criteria: &SearchCriteriaDto,
|
criteria: &SearchCriteriaDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(Vec<File>, usize), DomainError> {
|
) -> Result<(Vec<File>, usize), DomainError> {
|
||||||
let offset = criteria.offset as i64;
|
let offset = criteria.offset as i64;
|
||||||
let limit = criteria.limit as i64;
|
let limit = criteria.limit as i64;
|
||||||
@@ -822,7 +825,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
i64,
|
i64,
|
||||||
),
|
),
|
||||||
>(&sql)
|
>(&sql)
|
||||||
.bind(user_id);
|
.bind(user_id.to_string());
|
||||||
|
|
||||||
if let Some(fid) = folder_id {
|
if let Some(fid) = folder_id {
|
||||||
query = query.bind(fid);
|
query = query.bind(fid);
|
||||||
@@ -867,7 +870,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
&self,
|
&self,
|
||||||
root_folder_id: Option<&str>,
|
root_folder_id: Option<&str>,
|
||||||
criteria: &SearchCriteriaDto,
|
criteria: &SearchCriteriaDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(Vec<File>, usize), DomainError> {
|
) -> Result<(Vec<File>, usize), DomainError> {
|
||||||
// When no root folder specified, delegate to existing paginated search
|
// When no root folder specified, delegate to existing paginated search
|
||||||
let root_id = match root_folder_id {
|
let root_id = match root_folder_id {
|
||||||
@@ -983,7 +986,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
i64,
|
i64,
|
||||||
),
|
),
|
||||||
>(&sql)
|
>(&sql)
|
||||||
.bind(user_id)
|
.bind(user_id.to_string())
|
||||||
.bind(root_id);
|
.bind(root_id);
|
||||||
|
|
||||||
if let Some(name) = &criteria.name_contains
|
if let Some(name) = &criteria.name_contains
|
||||||
@@ -1043,7 +1046,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
&self,
|
&self,
|
||||||
folder_id: Option<&str>,
|
folder_id: Option<&str>,
|
||||||
criteria: &SearchCriteriaDto,
|
criteria: &SearchCriteriaDto,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<usize, DomainError> {
|
) -> Result<usize, DomainError> {
|
||||||
let (_, count) = self
|
let (_, count) = self
|
||||||
.search_files_paginated(folder_id, criteria, user_id)
|
.search_files_paginated(folder_id, criteria, user_id)
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ impl RecentItemsPgRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RecentItemsRepositoryPort for RecentItemsPgRepository {
|
impl RecentItemsRepositoryPort for RecentItemsPgRepository {
|
||||||
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>> {
|
||||||
let user_uuid = Uuid::parse_str(user_id)?;
|
|
||||||
|
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -39,12 +37,12 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
|
|||||||
AND f.id = ur.item_id::UUID
|
AND f.id = ur.item_id::UUID
|
||||||
LEFT JOIN storage.folders fld ON ur.item_type = 'folder'
|
LEFT JOIN storage.folders fld ON ur.item_type = 'folder'
|
||||||
AND fld.id = ur.item_id::UUID
|
AND fld.id = ur.item_id::UUID
|
||||||
WHERE ur.user_id = $1::TEXT
|
WHERE ur.user_id = $1
|
||||||
ORDER BY ur.accessed_at DESC
|
ORDER BY ur.accessed_at DESC
|
||||||
LIMIT $2
|
LIMIT $2
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(user_uuid)
|
.bind(user_id)
|
||||||
.bind(limit)
|
.bind(limit)
|
||||||
.fetch_all(&*self.db_pool)
|
.fetch_all(&*self.db_pool)
|
||||||
.await
|
.await
|
||||||
@@ -83,18 +81,16 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
|
|||||||
Ok(items)
|
Ok(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
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<()> {
|
||||||
let user_uuid = Uuid::parse_str(user_id)?;
|
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO auth.user_recent_files (user_id, item_id, item_type, accessed_at)
|
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)
|
ON CONFLICT (user_id, item_id, item_type)
|
||||||
DO UPDATE SET accessed_at = CURRENT_TIMESTAMP
|
DO UPDATE SET accessed_at = CURRENT_TIMESTAMP
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(user_uuid)
|
.bind(user_id)
|
||||||
.bind(item_id)
|
.bind(item_id)
|
||||||
.bind(item_type)
|
.bind(item_type)
|
||||||
.execute(&*self.db_pool)
|
.execute(&*self.db_pool)
|
||||||
@@ -111,16 +107,14 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
||||||
let user_uuid = Uuid::parse_str(user_id)?;
|
|
||||||
|
|
||||||
let result = sqlx::query(
|
let result = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
DELETE FROM auth.user_recent_files
|
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_id)
|
||||||
.bind(item_type)
|
.bind(item_type)
|
||||||
.execute(&*self.db_pool)
|
.execute(&*self.db_pool)
|
||||||
@@ -137,16 +131,14 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
|
|||||||
Ok(result.rows_affected() > 0)
|
Ok(result.rows_affected() > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn clear_all(&self, user_id: &str) -> Result<()> {
|
async fn clear_all(&self, user_id: Uuid) -> Result<()> {
|
||||||
let user_uuid = Uuid::parse_str(user_id)?;
|
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
DELETE FROM auth.user_recent_files
|
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)
|
.execute(&*self.db_pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
@@ -161,21 +153,19 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn prune(&self, user_id: &str, max_items: i32) -> Result<()> {
|
async fn prune(&self, user_id: Uuid, max_items: i32) -> Result<()> {
|
||||||
let user_uuid = Uuid::parse_str(user_id)?;
|
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
DELETE FROM auth.user_recent_files
|
DELETE FROM auth.user_recent_files
|
||||||
WHERE id IN (
|
WHERE id IN (
|
||||||
SELECT id FROM auth.user_recent_files
|
SELECT id FROM auth.user_recent_files
|
||||||
WHERE user_id = $1::TEXT
|
WHERE user_id = $1
|
||||||
ORDER BY accessed_at DESC
|
ORDER BY accessed_at DESC
|
||||||
OFFSET $2
|
OFFSET $2
|
||||||
)
|
)
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(user_uuid)
|
.bind(user_id)
|
||||||
.bind(max_items)
|
.bind(max_items)
|
||||||
.execute(&*self.db_pool)
|
.execute(&*self.db_pool)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use chrono::Utc;
|
|||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
use sqlx::{PgPool, Row};
|
use sqlx::{PgPool, Row};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::ports::auth_ports::SessionStoragePort;
|
use crate::application::ports::auth_ports::SessionStoragePort;
|
||||||
use crate::common::errors::DomainError;
|
use crate::common::errors::DomainError;
|
||||||
@@ -104,7 +105,7 @@ impl SessionRepository for SessionPgRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Gets a session by ID
|
/// 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(
|
let row = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -165,7 +166,7 @@ impl SessionRepository for SessionPgRepository {
|
|||||||
/// Gets all sessions for a user
|
/// Gets all sessions for a user
|
||||||
async fn get_sessions_by_user_id(
|
async fn get_sessions_by_user_id(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> SessionRepositoryResult<Vec<Session>> {
|
) -> SessionRepositoryResult<Vec<Session>> {
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -202,8 +203,8 @@ impl SessionRepository for SessionPgRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Revokes a specific session using a transaction
|
/// Revokes a specific session using a transaction
|
||||||
async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()> {
|
async fn revoke_session(&self, session_id: Uuid) -> SessionRepositoryResult<()> {
|
||||||
let id = session_id.to_string(); // Clone for use in closure
|
let id = session_id; // Copy for use in closure
|
||||||
|
|
||||||
with_transaction(&self.pool, "revoke_session", |tx| {
|
with_transaction(&self.pool, "revoke_session", |tx| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
@@ -216,14 +217,14 @@ impl SessionRepository for SessionPgRepository {
|
|||||||
RETURNING user_id
|
RETURNING user_id
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(&id)
|
.bind(id)
|
||||||
.fetch_optional(&mut **tx)
|
.fetch_optional(&mut **tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_sqlx_error)?;
|
.map_err(Self::map_sqlx_error)?;
|
||||||
|
|
||||||
// If we found the session, we can log a security event
|
// If we found the session, we can log a security event
|
||||||
if let Some(row) = result {
|
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)
|
// Log security event (in a security table)
|
||||||
// This is optional but shows how additional operations
|
// 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
|
/// Revokes all sessions for a user using a transaction
|
||||||
async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult<u64> {
|
async fn revoke_all_user_sessions(&self, user_id: Uuid) -> SessionRepositoryResult<u64> {
|
||||||
let user_id_clone = user_id.to_string(); // Clone for use in closure
|
let user_id_copy = user_id; // Copy for use in closure
|
||||||
|
|
||||||
with_transaction(&self.pool, "revoke_all_user_sessions", |tx| {
|
with_transaction(&self.pool, "revoke_all_user_sessions", |tx| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
@@ -251,7 +252,7 @@ impl SessionRepository for SessionPgRepository {
|
|||||||
WHERE user_id = $1 AND revoked = false
|
WHERE user_id = $1 AND revoked = false
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(&user_id_clone)
|
.bind(user_id_copy)
|
||||||
.execute(&mut **tx)
|
.execute(&mut **tx)
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_sqlx_error)?;
|
.map_err(Self::map_sqlx_error)?;
|
||||||
@@ -260,7 +261,7 @@ impl SessionRepository for SessionPgRepository {
|
|||||||
|
|
||||||
// Log security event
|
// Log security event
|
||||||
if affected > 0 {
|
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)
|
Ok(affected)
|
||||||
@@ -305,13 +306,13 @@ impl SessionStoragePort for SessionPgRepository {
|
|||||||
.map_err(DomainError::from)
|
.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)
|
SessionRepository::revoke_session(self, session_id)
|
||||||
.await
|
.await
|
||||||
.map_err(DomainError::from)
|
.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)
|
SessionRepository::revoke_all_user_sessions(self, user_id)
|
||||||
.await
|
.await
|
||||||
.map_err(DomainError::from)
|
.map_err(DomainError::from)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::common::errors::{DomainError, ErrorKind};
|
use crate::common::errors::{DomainError, ErrorKind};
|
||||||
use crate::domain::repositories::settings_repository::SettingsRepository;
|
use crate::domain::repositories::settings_repository::SettingsRepository;
|
||||||
@@ -60,7 +61,7 @@ impl SettingsRepository for SettingsPgRepository {
|
|||||||
value: &str,
|
value: &str,
|
||||||
category: &str,
|
category: &str,
|
||||||
is_secret: bool,
|
is_secret: bool,
|
||||||
updated_by: Option<&str>,
|
updated_by: Option<Uuid>,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO auth.admin_settings (key, value, category, is_secret, updated_by, updated_at)
|
"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`;
|
/// Only the first caller that inserts the row gets `rows_affected == 1`;
|
||||||
/// concurrent callers see 0 rows affected and receive `false`.
|
/// 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(
|
let result = sqlx::query(
|
||||||
"INSERT INTO auth.admin_settings (key, value, category, is_secret, updated_by, updated_at)
|
"INSERT INTO auth.admin_settings (key, value, category, is_secret, updated_by, updated_at)
|
||||||
VALUES ('system_initialized', 'true', 'system', false, $1, NOW())
|
VALUES ('system_initialized', 'true', 'system', false, $1, NOW())
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use sqlx::{PgPool, Row};
|
use sqlx::{PgPool, Row};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
application::ports::share_ports::ShareStoragePort,
|
application::ports::share_ports::ShareStoragePort,
|
||||||
@@ -37,7 +38,7 @@ impl SharePgRepository {
|
|||||||
|
|
||||||
/// Maps a [`sqlx::postgres::PgRow`] to the domain [`Share`] entity.
|
/// Maps a [`sqlx::postgres::PgRow`] to the domain [`Share`] entity.
|
||||||
fn row_to_entity(row: &sqlx::postgres::PgRow) -> Result<Share, DomainError> {
|
fn row_to_entity(row: &sqlx::postgres::PgRow) -> Result<Share, DomainError> {
|
||||||
let id: String = row
|
let id: Uuid = row
|
||||||
.try_get("id")
|
.try_get("id")
|
||||||
.map_err(|e| DomainError::internal_error("Share", format!("Failed to read id: {e}")))?;
|
.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| {
|
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| {
|
let created_at: i64 = row.try_get("created_at").map_err(|e| {
|
||||||
DomainError::internal_error("Share", format!("Failed to read created_at: {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}"))
|
DomainError::internal_error("Share", format!("Failed to read created_by: {e}"))
|
||||||
})?;
|
})?;
|
||||||
let access_count: i64 = row.try_get("access_count").unwrap_or(0);
|
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,
|
expires_at, permissions_read, permissions_write, permissions_reshare,
|
||||||
created_at, created_by, access_count)
|
created_at, created_by, access_count)
|
||||||
VALUES
|
VALUES
|
||||||
($1::UUID, $2, $3, $4, $5, $6,
|
($1, $2, $3, $4, $5, $6,
|
||||||
$7, $8, $9, $10,
|
$7, $8, $9, $10,
|
||||||
$11, $12, $13)
|
$11, $12, $13)
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
@@ -105,7 +106,7 @@ impl ShareStoragePort for SharePgRepository {
|
|||||||
permissions_reshare = EXCLUDED.permissions_reshare,
|
permissions_reshare = EXCLUDED.permissions_reshare,
|
||||||
access_count = EXCLUDED.access_count
|
access_count = EXCLUDED.access_count
|
||||||
RETURNING
|
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,
|
expires_at, permissions_read, permissions_write, permissions_reshare,
|
||||||
created_at, created_by, access_count
|
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> {
|
async fn find_share_by_token(&self, token: &str) -> Result<Share, DomainError> {
|
||||||
let row = sqlx::query(
|
let row = sqlx::query(
|
||||||
r#"
|
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,
|
expires_at, permissions_read, permissions_write, permissions_reshare,
|
||||||
created_at, created_by, access_count
|
created_at, created_by, access_count
|
||||||
FROM storage.shares
|
FROM storage.shares
|
||||||
@@ -162,16 +163,16 @@ impl ShareStoragePort for SharePgRepository {
|
|||||||
|
|
||||||
async fn find_share_by_id_for_user(
|
async fn find_share_by_id_for_user(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: Uuid,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Share, DomainError> {
|
) -> Result<Share, DomainError> {
|
||||||
let row = sqlx::query(
|
let row = sqlx::query(
|
||||||
r#"
|
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,
|
expires_at, permissions_read, permissions_write, permissions_reshare,
|
||||||
created_at, created_by, access_count
|
created_at, created_by, access_count
|
||||||
FROM storage.shares
|
FROM storage.shares
|
||||||
WHERE id = $1::UUID AND created_by = $2
|
WHERE id = $1 AND created_by = $2
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(id)
|
.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 =
|
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(id)
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.execute(&*self.db_pool)
|
.execute(&*self.db_pool)
|
||||||
@@ -220,11 +221,11 @@ impl ShareStoragePort for SharePgRepository {
|
|||||||
&self,
|
&self,
|
||||||
item_id: &str,
|
item_id: &str,
|
||||||
item_type: &ShareItemType,
|
item_type: &ShareItemType,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<Share>, DomainError> {
|
) -> Result<Vec<Share>, DomainError> {
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
r#"
|
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,
|
expires_at, permissions_read, permissions_write, permissions_reshare,
|
||||||
created_at, created_by, access_count
|
created_at, created_by, access_count
|
||||||
FROM storage.shares
|
FROM storage.shares
|
||||||
@@ -256,9 +257,9 @@ impl ShareStoragePort for SharePgRepository {
|
|||||||
permissions_write = $6,
|
permissions_write = $6,
|
||||||
permissions_reshare = $7,
|
permissions_reshare = $7,
|
||||||
access_count = $8
|
access_count = $8
|
||||||
WHERE id = $1::UUID
|
WHERE id = $1
|
||||||
RETURNING
|
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,
|
expires_at, permissions_read, permissions_write, permissions_reshare,
|
||||||
created_at, created_by, access_count
|
created_at, created_by, access_count
|
||||||
"#,
|
"#,
|
||||||
@@ -289,14 +290,14 @@ impl ShareStoragePort for SharePgRepository {
|
|||||||
|
|
||||||
async fn find_shares_by_user(
|
async fn find_shares_by_user(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
) -> Result<(Vec<Share>, usize), DomainError> {
|
) -> Result<(Vec<Share>, usize), DomainError> {
|
||||||
// Single query with window function — count + rows in one roundtrip
|
// Single query with window function — count + rows in one roundtrip
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
r#"
|
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,
|
expires_at, permissions_read, permissions_write, permissions_reshare,
|
||||||
created_at, created_by, access_count,
|
created_at, created_by, access_count,
|
||||||
COUNT(*) OVER() AS total_count
|
COUNT(*) OVER() AS total_count
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ impl TrashDbRepository {
|
|||||||
id: Uuid,
|
id: Uuid,
|
||||||
name: String,
|
name: String,
|
||||||
item_type: String,
|
item_type: String,
|
||||||
user_id: String,
|
user_id: Uuid,
|
||||||
trashed_at: Option<DateTime<Utc>>,
|
trashed_at: Option<DateTime<Utc>>,
|
||||||
) -> TrashedItem {
|
) -> TrashedItem {
|
||||||
let trashed_at = trashed_at.unwrap_or_else(Utc::now);
|
let trashed_at = trashed_at.unwrap_or_else(Utc::now);
|
||||||
@@ -61,14 +61,12 @@ impl TrashDbRepository {
|
|||||||
_ => TrashedItemType::File,
|
_ => 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
|
// In the soft-delete model, the trash entry ID is the same as the
|
||||||
// original item ID since there is no separate trash table.
|
// original item ID since there is no separate trash table.
|
||||||
TrashedItem::from_raw(
|
TrashedItem::from_raw(
|
||||||
id, // trash entry id (same as original)
|
id, // trash entry id (same as original)
|
||||||
id, // original item id
|
id, // original item id
|
||||||
user_uuid, // owner
|
user_id, // owner
|
||||||
item_type_enum,
|
item_type_enum,
|
||||||
name.clone(),
|
name.clone(),
|
||||||
String::new(), // original_path — not stored separately in soft-delete model
|
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>> {
|
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#"
|
r#"
|
||||||
SELECT id, name, item_type, user_id, trashed_at
|
SELECT id, name, item_type, user_id, trashed_at
|
||||||
FROM storage.trash_items
|
FROM storage.trash_items
|
||||||
@@ -95,7 +93,7 @@ impl TrashRepository for TrashDbRepository {
|
|||||||
ORDER BY trashed_at DESC
|
ORDER BY trashed_at DESC
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(user_id.to_string())
|
.bind(user_id)
|
||||||
.fetch_all(self.pool.as_ref())
|
.fetch_all(self.pool.as_ref())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::internal_error("TrashDb", format!("list: {e}")))?;
|
.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>> {
|
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#"
|
r#"
|
||||||
SELECT id, name, item_type, user_id, trashed_at
|
SELECT id, name, item_type, user_id, trashed_at
|
||||||
FROM storage.trash_items
|
FROM storage.trash_items
|
||||||
@@ -117,7 +115,7 @@ impl TrashRepository for TrashDbRepository {
|
|||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.bind(user_id.to_string())
|
.bind(user_id)
|
||||||
.fetch_optional(self.pool.as_ref())
|
.fetch_optional(self.pool.as_ref())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::internal_error("TrashDb", format!("get: {e}")))?;
|
.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<()> {
|
async fn clear_trash(&self, user_id: &Uuid) -> Result<()> {
|
||||||
// Delete all trashed files for this user
|
// Delete all trashed files for this user
|
||||||
sqlx::query("DELETE FROM storage.files WHERE user_id = $1 AND is_trashed = TRUE")
|
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())
|
.execute(self.pool.as_ref())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::internal_error("TrashDb", format!("clear files: {e}")))?;
|
.map_err(|e| DomainError::internal_error("TrashDb", format!("clear files: {e}")))?;
|
||||||
|
|
||||||
// Delete all trashed folders for this user
|
// Delete all trashed folders for this user
|
||||||
sqlx::query("DELETE FROM storage.folders WHERE user_id = $1 AND is_trashed = TRUE")
|
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())
|
.execute(self.pool.as_ref())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::internal_error("TrashDb", format!("clear folders: {e}")))?;
|
.map_err(|e| DomainError::internal_error("TrashDb", format!("clear folders: {e}")))?;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
use sqlx::{PgPool, Row};
|
use sqlx::{PgPool, Row};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::ports::auth_ports::UserStoragePort;
|
use crate::application::ports::auth_ports::UserStoragePort;
|
||||||
use crate::common::errors::DomainError;
|
use crate::common::errors::DomainError;
|
||||||
@@ -101,7 +102,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Gets a user by ID
|
/// 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(
|
let row = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -278,7 +279,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
/// Updates only the storage usage of a user
|
/// Updates only the storage usage of a user
|
||||||
async fn update_storage_usage(
|
async fn update_storage_usage(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
usage_bytes: i64,
|
usage_bytes: i64,
|
||||||
) -> UserRepositoryResult<()> {
|
) -> UserRepositoryResult<()> {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
@@ -300,7 +301,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the last login date
|
/// 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(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
UPDATE auth.users
|
UPDATE auth.users
|
||||||
@@ -423,7 +424,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
/// Activates or deactivates a user
|
/// Activates or deactivates a user
|
||||||
async fn set_user_active_status(
|
async fn set_user_active_status(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
active: bool,
|
active: bool,
|
||||||
) -> UserRepositoryResult<()> {
|
) -> UserRepositoryResult<()> {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
@@ -447,7 +448,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
/// Changes a user's password
|
/// Changes a user's password
|
||||||
async fn change_password(
|
async fn change_password(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
password_hash: &str,
|
password_hash: &str,
|
||||||
) -> UserRepositoryResult<()> {
|
) -> UserRepositoryResult<()> {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
@@ -469,7 +470,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Changes a user's role
|
/// 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
|
// Convert the role to string for the binding
|
||||||
let role_str = role.to_string();
|
let role_str = role.to_string();
|
||||||
|
|
||||||
@@ -542,7 +543,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes a user
|
/// Deletes a user
|
||||||
async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()> {
|
async fn delete_user(&self, user_id: Uuid) -> UserRepositoryResult<()> {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
DELETE FROM auth.users
|
DELETE FROM auth.users
|
||||||
@@ -606,7 +607,7 @@ impl UserRepository for UserPgRepository {
|
|||||||
/// Updates a user's storage quota
|
/// Updates a user's storage quota
|
||||||
async fn update_storage_quota(
|
async fn update_storage_quota(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
quota_bytes: i64,
|
quota_bytes: i64,
|
||||||
) -> UserRepositoryResult<()> {
|
) -> UserRepositoryResult<()> {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
@@ -675,7 +676,7 @@ impl UserStoragePort for UserPgRepository {
|
|||||||
.map_err(DomainError::from)
|
.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)
|
UserRepository::get_user_by_id(self, id)
|
||||||
.await
|
.await
|
||||||
.map_err(DomainError::from)
|
.map_err(DomainError::from)
|
||||||
@@ -701,7 +702,7 @@ impl UserStoragePort for UserPgRepository {
|
|||||||
|
|
||||||
async fn update_storage_usage(
|
async fn update_storage_usage(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
usage_bytes: i64,
|
usage_bytes: i64,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
UserRepository::update_storage_usage(self, user_id, usage_bytes)
|
UserRepository::update_storage_usage(self, user_id, usage_bytes)
|
||||||
@@ -727,13 +728,13 @@ impl UserStoragePort for UserPgRepository {
|
|||||||
.map_err(DomainError::from)
|
.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)
|
UserRepository::delete_user(self, user_id)
|
||||||
.await
|
.await
|
||||||
.map_err(DomainError::from)
|
.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)
|
UserRepository::change_password(self, user_id, password_hash)
|
||||||
.await
|
.await
|
||||||
.map_err(DomainError::from)
|
.map_err(DomainError::from)
|
||||||
@@ -749,13 +750,13 @@ impl UserStoragePort for UserPgRepository {
|
|||||||
.map_err(DomainError::from)
|
.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)
|
UserRepository::set_user_active_status(self, user_id, active)
|
||||||
.await
|
.await
|
||||||
.map_err(DomainError::from)
|
.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 {
|
let user_role = match role {
|
||||||
"admin" => UserRole::Admin,
|
"admin" => UserRole::Admin,
|
||||||
_ => UserRole::User,
|
_ => UserRole::User,
|
||||||
@@ -767,7 +768,7 @@ impl UserStoragePort for UserPgRepository {
|
|||||||
|
|
||||||
async fn update_storage_quota(
|
async fn update_storage_quota(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
quota_bytes: i64,
|
quota_bytes: i64,
|
||||||
) -> Result<(), DomainError> {
|
) -> Result<(), DomainError> {
|
||||||
UserRepository::update_storage_quota(self, user_id, quota_bytes)
|
UserRepository::update_storage_quota(self, user_id, quota_bytes)
|
||||||
|
|||||||
@@ -801,7 +801,7 @@ impl ChunkedUploadService {
|
|||||||
impl ChunkedUploadPort for ChunkedUploadService {
|
impl ChunkedUploadPort for ChunkedUploadService {
|
||||||
async fn create_session(
|
async fn create_session(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
filename: String,
|
filename: String,
|
||||||
folder_id: Option<String>,
|
folder_id: Option<String>,
|
||||||
content_type: String,
|
content_type: String,
|
||||||
@@ -809,7 +809,7 @@ impl ChunkedUploadPort for ChunkedUploadService {
|
|||||||
chunk_size: Option<usize>,
|
chunk_size: Option<usize>,
|
||||||
) -> Result<CreateUploadResponseDto, DomainError> {
|
) -> Result<CreateUploadResponseDto, DomainError> {
|
||||||
self.create_session_inner(
|
self.create_session_inner(
|
||||||
user_id.to_owned(),
|
user_id.to_string(),
|
||||||
filename,
|
filename,
|
||||||
folder_id,
|
folder_id,
|
||||||
content_type,
|
content_type,
|
||||||
@@ -823,12 +823,12 @@ impl ChunkedUploadPort for ChunkedUploadService {
|
|||||||
async fn upload_chunk(
|
async fn upload_chunk(
|
||||||
&self,
|
&self,
|
||||||
upload_id: &str,
|
upload_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
chunk_index: usize,
|
chunk_index: usize,
|
||||||
data: bytes::Bytes,
|
data: bytes::Bytes,
|
||||||
checksum: Option<String>,
|
checksum: Option<String>,
|
||||||
) -> Result<ChunkUploadResponseDto, DomainError> {
|
) -> 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
|
.await
|
||||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))
|
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))
|
||||||
}
|
}
|
||||||
@@ -836,9 +836,9 @@ impl ChunkedUploadPort for ChunkedUploadService {
|
|||||||
async fn get_status(
|
async fn get_status(
|
||||||
&self,
|
&self,
|
||||||
upload_id: &str,
|
upload_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<UploadStatusResponseDto, DomainError> {
|
) -> Result<UploadStatusResponseDto, DomainError> {
|
||||||
self.get_status_inner(upload_id, user_id)
|
self.get_status_inner(upload_id, &user_id.to_string())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::new(ErrorKind::NotFound, "ChunkedUpload", e))
|
.map_err(|e| DomainError::new(ErrorKind::NotFound, "ChunkedUpload", e))
|
||||||
}
|
}
|
||||||
@@ -846,21 +846,21 @@ impl ChunkedUploadPort for ChunkedUploadService {
|
|||||||
async fn complete_upload(
|
async fn complete_upload(
|
||||||
&self,
|
&self,
|
||||||
upload_id: &str,
|
upload_id: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<(PathBuf, String, Option<String>, String, u64, String), DomainError> {
|
) -> 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
|
.await
|
||||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))
|
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
||||||
self.finalize_upload_inner(upload_id, user_id)
|
self.finalize_upload_inner(upload_id, &user_id.to_string())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))
|
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
||||||
self.cancel_upload_inner(upload_id, user_id)
|
self.cancel_upload_inner(upload_id, &user_id.to_string())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))
|
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::dtos::display_helpers::{
|
use crate::application::dtos::display_helpers::{
|
||||||
category_for, format_file_size, icon_class_for, icon_special_class_for,
|
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(
|
pub async fn resolve_path_for_user(
|
||||||
&self,
|
&self,
|
||||||
path: &str,
|
path: &str,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<ResolvedResource, DomainError> {
|
) -> Result<ResolvedResource, DomainError> {
|
||||||
let path = path.trim_start_matches('/').trim_end_matches('/');
|
let path = path.trim_start_matches('/').trim_end_matches('/');
|
||||||
if path.is_empty() {
|
if path.is_empty() {
|
||||||
@@ -180,7 +181,7 @@ impl PathResolverService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the resource at `path` belongs to `user_id`.
|
/// 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('/');
|
let path = path.trim_start_matches('/').trim_end_matches('/');
|
||||||
if path.is_empty() {
|
if path.is_empty() {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use crate::application::ports::auth_ports::TokenServicePort;
|
|||||||
use crate::common::di::AppState;
|
use crate::common::di::AppState;
|
||||||
use crate::interfaces::errors::AppError;
|
use crate::interfaces::errors::AppError;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Admin API routes — all require admin role.
|
/// Admin API routes — all require admin role.
|
||||||
pub fn admin_routes() -> Router<Arc<AppState>> {
|
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).
|
/// 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
|
let auth = state
|
||||||
.auth_service
|
.auth_service
|
||||||
.as_ref()
|
.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
|
/// GET /api/admin/settings/oidc — get OIDC settings for the admin panel
|
||||||
@@ -108,7 +109,7 @@ async fn save_oidc_settings(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| AppError::internal_error("Admin settings service not available"))?;
|
.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
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to save settings: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Failed to save settings: {}", e)))?;
|
||||||
|
|
||||||
@@ -292,6 +293,8 @@ async fn get_user(
|
|||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
admin_guard(&state, &headers).await?;
|
admin_guard(&state, &headers).await?;
|
||||||
|
|
||||||
|
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
|
||||||
|
|
||||||
let auth = state
|
let auth = state
|
||||||
.auth_service
|
.auth_service
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -299,7 +302,7 @@ async fn get_user(
|
|||||||
|
|
||||||
let user = auth
|
let user = auth
|
||||||
.auth_application_service
|
.auth_application_service
|
||||||
.get_user_admin(&id)
|
.get_user_admin(id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::not_found(format!("User not found: {}", e)))?;
|
.map_err(|e| AppError::not_found(format!("User not found: {}", e)))?;
|
||||||
|
|
||||||
@@ -314,6 +317,8 @@ async fn delete_user(
|
|||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
||||||
|
|
||||||
|
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
|
||||||
|
|
||||||
// Prevent self-deletion
|
// Prevent self-deletion
|
||||||
if admin_id == id {
|
if admin_id == id {
|
||||||
return Err(AppError::new(
|
return Err(AppError::new(
|
||||||
@@ -329,7 +334,7 @@ async fn delete_user(
|
|||||||
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
||||||
|
|
||||||
auth.auth_application_service
|
auth.auth_application_service
|
||||||
.delete_user_admin(&id)
|
.delete_user_admin(id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to delete user: {}", e)))?;
|
.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> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
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
|
// Prevent changing own role
|
||||||
if admin_id == id {
|
if admin_id == id {
|
||||||
return Err(AppError::new(
|
return Err(AppError::new(
|
||||||
@@ -365,7 +372,7 @@ async fn update_user_role(
|
|||||||
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
||||||
|
|
||||||
auth.auth_application_service
|
auth.auth_application_service
|
||||||
.change_user_role(&id, &dto.role)
|
.change_user_role(id, &dto.role)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to change role: {}", e)))?;
|
.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> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
let (admin_id, _) = admin_guard(&state, &headers).await?;
|
||||||
|
|
||||||
|
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
|
||||||
|
|
||||||
// Prevent deactivating yourself
|
// Prevent deactivating yourself
|
||||||
if admin_id == id && !dto.active {
|
if admin_id == id && !dto.active {
|
||||||
return Err(AppError::new(
|
return Err(AppError::new(
|
||||||
@@ -401,7 +410,7 @@ async fn update_user_active(
|
|||||||
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
||||||
|
|
||||||
auth.auth_application_service
|
auth.auth_application_service
|
||||||
.set_user_active(&id, dto.active)
|
.set_user_active(id, dto.active)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to update user status: {}", e)))?;
|
.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> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
admin_guard(&state, &headers).await?;
|
admin_guard(&state, &headers).await?;
|
||||||
|
|
||||||
|
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
|
||||||
|
|
||||||
let auth = state
|
let auth = state
|
||||||
.auth_service
|
.auth_service
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
||||||
|
|
||||||
auth.auth_application_service
|
auth.auth_application_service
|
||||||
.update_user_quota(&id, dto.quota_bytes)
|
.update_user_quota(id, dto.quota_bytes)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to update quota: {}", e)))?;
|
.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> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
admin_guard(&state, &headers).await?;
|
admin_guard(&state, &headers).await?;
|
||||||
|
|
||||||
|
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
|
||||||
|
|
||||||
let auth = state
|
let auth = state
|
||||||
.auth_service
|
.auth_service
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
|
||||||
|
|
||||||
auth.auth_application_service
|
auth.auth_application_service
|
||||||
.admin_reset_password(&id, &dto.new_password)
|
.admin_reset_password(id, &dto.new_password)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
AppError::new(
|
AppError::new(
|
||||||
@@ -558,7 +571,7 @@ async fn set_registration_setting(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| AppError::internal_error("Admin settings service not available"))?;
|
.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
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to save setting: {}", e)))?;
|
.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::routing::{delete, get, post};
|
||||||
use axum::{Json, Router};
|
use axum::{Json, Router};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Protected routes — require JWT auth middleware.
|
/// Protected routes — require JWT auth middleware.
|
||||||
pub fn app_password_routes() -> Router<Arc<AppState>> {
|
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"))?;
|
.ok_or_else(|| AppError::internal_error("App password service not configured"))?;
|
||||||
|
|
||||||
let response = service
|
let response = service
|
||||||
.create(&user.id, request)
|
.create(user.id, request)
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ async fn list_app_passwords(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| AppError::internal_error("App password service not configured"))?;
|
.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))
|
Ok(Json(response))
|
||||||
}
|
}
|
||||||
@@ -72,8 +73,10 @@ async fn revoke_app_password(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| AppError::internal_error("App password service not configured"))?;
|
.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
|
let response = service
|
||||||
.revoke(&user.id, &id)
|
.revoke(user.id, id)
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use axum::{
|
|||||||
routing::{get, post, put},
|
routing::{get, post, put},
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::dtos::user_dto::{
|
use crate::application::dtos::user_dto::{
|
||||||
ChangePasswordDto, LoginDto, OidcCallbackQueryDto, OidcExchangeDto, OidcProviderInfoDto,
|
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() {
|
if let Some(storage_usage_service) = state.storage_usage_service.as_ref() {
|
||||||
// Calculate storage synchronously (we await the result)
|
// Calculate storage synchronously (we await the result)
|
||||||
match storage_usage_service
|
match storage_usage_service
|
||||||
.update_user_storage_usage(&user_id)
|
.update_user_storage_usage(user_id)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(usage) => {
|
Ok(usage) => {
|
||||||
@@ -293,7 +294,7 @@ async fn get_current_user(
|
|||||||
// Now get the user data WITH the updated storage
|
// Now get the user data WITH the updated storage
|
||||||
let user = auth_service
|
let user = auth_service
|
||||||
.auth_application_service
|
.auth_application_service
|
||||||
.get_user_by_id(&user_id)
|
.get_user_by_id(user_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok((StatusCode::OK, Json(user)))
|
Ok((StatusCode::OK, Json(user)))
|
||||||
@@ -311,7 +312,7 @@ async fn change_password(
|
|||||||
|
|
||||||
auth_service
|
auth_service
|
||||||
.auth_application_service
|
.auth_application_service
|
||||||
.change_password(&user_id, dto)
|
.change_password(user_id, dto)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(StatusCode::OK)
|
Ok(StatusCode::OK)
|
||||||
@@ -341,7 +342,7 @@ async fn logout(
|
|||||||
|
|
||||||
auth_service
|
auth_service
|
||||||
.auth_application_service
|
.auth_application_service
|
||||||
.logout(&user_id, &refresh_token)
|
.logout(user_id, &refresh_token)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Clear HttpOnly + CSRF cookies so the browser forgets the session
|
// 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.
|
// 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.
|
// doesn't exist yet. It will be updated to the real id below.
|
||||||
let claimed = admin_svc
|
let claimed = admin_svc
|
||||||
.try_claim_initialization("pending")
|
.try_claim_initialization(Uuid::nil())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
tracing::error!("Failed to claim system initialization: {}", 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
|
// 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,
|
// Not fatal — the claim already prevents concurrent re-initialization,
|
||||||
// and the "pending" marker is still "true" so the system stays locked.
|
// and the "pending" marker is still "true" so the system stays locked.
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
@@ -603,7 +605,7 @@ async fn oidc_callback(
|
|||||||
|
|
||||||
let (_id, app_password) = nextcloud
|
let (_id, app_password) = nextcloud
|
||||||
.app_passwords
|
.app_passwords
|
||||||
.create_nc(&user_id, "Nextcloud (OIDC)")
|
.create_nc(user_id, "Nextcloud (OIDC)")
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
tracing::error!(error = %e, user = %username, "OIDC+NC: failed to create app password");
|
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
|
// Execute batch operation
|
||||||
let result = state
|
let result = state
|
||||||
.batch_service
|
.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
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
tracing::error!("Batch move_files failed: {}", e);
|
tracing::error!("Batch move_files failed: {}", e);
|
||||||
@@ -216,7 +216,7 @@ pub async fn copy_files_batch(
|
|||||||
// Execute batch operation
|
// Execute batch operation
|
||||||
let result = state
|
let result = state
|
||||||
.batch_service
|
.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
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
tracing::error!("Batch copy_files failed: {}", e);
|
tracing::error!("Batch copy_files failed: {}", e);
|
||||||
@@ -272,7 +272,7 @@ pub async fn delete_files_batch(
|
|||||||
// Execute batch operation
|
// Execute batch operation
|
||||||
let result = state
|
let result = state
|
||||||
.batch_service
|
.batch_service
|
||||||
.delete_files(request.file_ids, &auth_user.id)
|
.delete_files(request.file_ids, auth_user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
tracing::error!("Batch delete_files failed: {}", e);
|
tracing::error!("Batch delete_files failed: {}", e);
|
||||||
@@ -336,7 +336,7 @@ pub async fn delete_folders_batch(
|
|||||||
// Execute batch operation
|
// Execute batch operation
|
||||||
let result = state
|
let result = state
|
||||||
.batch_service
|
.batch_service
|
||||||
.delete_folders(request.folder_ids, request.recursive, &auth_user.id)
|
.delete_folders(request.folder_ids, request.recursive, auth_user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
tracing::error!("Batch delete_folders failed: {}", e);
|
tracing::error!("Batch delete_folders failed: {}", e);
|
||||||
@@ -407,7 +407,7 @@ pub async fn create_folders_batch(
|
|||||||
// Execute batch operation
|
// Execute batch operation
|
||||||
let result = state
|
let result = state
|
||||||
.batch_service
|
.batch_service
|
||||||
.create_folders(folders, &auth_user.id)
|
.create_folders(folders, auth_user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
tracing::error!("Batch create_folders failed: {}", e);
|
tracing::error!("Batch create_folders failed: {}", e);
|
||||||
@@ -463,7 +463,7 @@ pub async fn get_files_batch(
|
|||||||
// Execute batch operation
|
// Execute batch operation
|
||||||
let result = state
|
let result = state
|
||||||
.batch_service
|
.batch_service
|
||||||
.get_multiple_files(request.file_ids, &auth_user.id)
|
.get_multiple_files(request.file_ids, auth_user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
tracing::error!("Batch get_files failed: {}", e);
|
tracing::error!("Batch get_files failed: {}", e);
|
||||||
@@ -519,7 +519,7 @@ pub async fn get_folders_batch(
|
|||||||
// Execute batch operation
|
// Execute batch operation
|
||||||
let result = state
|
let result = state
|
||||||
.batch_service
|
.batch_service
|
||||||
.get_multiple_folders(request.folder_ids, &auth_user.id)
|
.get_multiple_folders(request.folder_ids, auth_user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
tracing::error!("Batch get_folders failed: {}", e);
|
tracing::error!("Batch get_folders failed: {}", e);
|
||||||
@@ -603,7 +603,7 @@ pub async fn trash_batch(
|
|||||||
if !request.file_ids.is_empty() {
|
if !request.file_ids.is_empty() {
|
||||||
match state
|
match state
|
||||||
.batch_service
|
.batch_service
|
||||||
.trash_files(request.file_ids, &auth_user.id)
|
.trash_files(request.file_ids, auth_user.id)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
@@ -630,7 +630,7 @@ pub async fn trash_batch(
|
|||||||
if !request.folder_ids.is_empty() {
|
if !request.folder_ids.is_empty() {
|
||||||
match state
|
match state
|
||||||
.batch_service
|
.batch_service
|
||||||
.trash_folders(request.folder_ids, &auth_user.id)
|
.trash_folders(request.folder_ids, auth_user.id)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
@@ -707,7 +707,7 @@ pub async fn move_folders_batch(
|
|||||||
|
|
||||||
let result = state
|
let result = state
|
||||||
.batch_service
|
.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
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
tracing::error!("Batch move_folders failed: {}", e);
|
tracing::error!("Batch move_folders failed: {}", e);
|
||||||
@@ -760,7 +760,7 @@ pub async fn download_batch(
|
|||||||
|
|
||||||
let temp_file = state
|
let temp_file = state
|
||||||
.batch_service
|
.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
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
tracing::error!("Batch download ZIP failed: {}", e);
|
tracing::error!("Batch download ZIP failed: {}", e);
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ async fn handle_propfind(
|
|||||||
vec![]
|
vec![]
|
||||||
} else {
|
} else {
|
||||||
calendar_service
|
calendar_service
|
||||||
.list_my_calendars(&user.id)
|
.list_my_calendars(user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to list calendars: {}", e)))?
|
.map_err(|e| AppError::internal_error(format!("Failed to list calendars: {}", e)))?
|
||||||
};
|
};
|
||||||
@@ -264,13 +264,13 @@ async fn handle_propfind(
|
|||||||
|
|
||||||
if parts.len() == 1 {
|
if parts.len() == 1 {
|
||||||
// Single path segment: try as calendar ID first, fall back to user home
|
// 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 {
|
if let Ok(calendar) = calendar_result {
|
||||||
// Valid calendar ID — return calendar collection
|
// Valid calendar ID — return calendar collection
|
||||||
let events = if depth != "0" {
|
let events = if depth != "0" {
|
||||||
calendar_service
|
calendar_service
|
||||||
.list_events(first_segment, None, None, &user.id)
|
.list_events(first_segment, None, None, user.id)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
@@ -300,7 +300,7 @@ async fn handle_propfind(
|
|||||||
// List all calendars for this user
|
// List all calendars for this user
|
||||||
let calendars =
|
let calendars =
|
||||||
calendar_service
|
calendar_service
|
||||||
.list_my_calendars(&user.id)
|
.list_my_calendars(user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
AppError::internal_error(format!("Failed to list calendars: {}", e))
|
AppError::internal_error(format!("Failed to list calendars: {}", e))
|
||||||
@@ -328,7 +328,7 @@ async fn handle_propfind(
|
|||||||
let rest = parts[1];
|
let rest = parts[1];
|
||||||
|
|
||||||
// Check if first_segment is a valid calendar ID
|
// 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() {
|
let (calendar_id, event_path) = if calendar_result.is_ok() {
|
||||||
// first_segment is a calendar ID, rest is event path
|
// first_segment is a calendar ID, rest is event path
|
||||||
@@ -341,13 +341,13 @@ async fn handle_propfind(
|
|||||||
// /caldav/{username}/{calendar_id}
|
// /caldav/{username}/{calendar_id}
|
||||||
// Try to get this as a calendar collection
|
// Try to get this as a calendar collection
|
||||||
let cal = calendar_service
|
let cal = calendar_service
|
||||||
.get_calendar(sub_parts[0], &user.id)
|
.get_calendar(sub_parts[0], user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::not_found(format!("Calendar not found: {}", e)))?;
|
.map_err(|e| AppError::not_found(format!("Calendar not found: {}", e)))?;
|
||||||
|
|
||||||
let events = if depth != "0" {
|
let events = if depth != "0" {
|
||||||
calendar_service
|
calendar_service
|
||||||
.list_events(sub_parts[0], None, None, &user.id)
|
.list_events(sub_parts[0], None, None, user.id)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
@@ -384,7 +384,7 @@ async fn handle_propfind(
|
|||||||
let ical_uid = event_path.trim_end_matches(".ics");
|
let ical_uid = event_path.trim_end_matches(".ics");
|
||||||
|
|
||||||
let events = calendar_service
|
let events = calendar_service
|
||||||
.list_events(calendar_id, None, None, &user.id)
|
.list_events(calendar_id, None, None, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?;
|
||||||
|
|
||||||
@@ -444,14 +444,14 @@ async fn handle_report(
|
|||||||
CalDavReportType::CalendarQuery { time_range, .. } => {
|
CalDavReportType::CalendarQuery { time_range, .. } => {
|
||||||
if let Some((start, end)) = time_range {
|
if let Some((start, end)) = time_range {
|
||||||
calendar_service
|
calendar_service
|
||||||
.get_events_in_range(calendar_id, *start, *end, &user.id)
|
.get_events_in_range(calendar_id, *start, *end, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
AppError::internal_error(format!("Failed to query events: {}", e))
|
AppError::internal_error(format!("Failed to query events: {}", e))
|
||||||
})?
|
})?
|
||||||
} else {
|
} else {
|
||||||
calendar_service
|
calendar_service
|
||||||
.list_events(calendar_id, None, None, &user.id)
|
.list_events(calendar_id, None, None, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
AppError::internal_error(format!("Failed to list events: {}", e))
|
AppError::internal_error(format!("Failed to list events: {}", e))
|
||||||
@@ -460,7 +460,7 @@ async fn handle_report(
|
|||||||
}
|
}
|
||||||
CalDavReportType::CalendarMultiget { hrefs, .. } => {
|
CalDavReportType::CalendarMultiget { hrefs, .. } => {
|
||||||
let all_events = calendar_service
|
let all_events = calendar_service
|
||||||
.list_events(calendar_id, None, None, &user.id)
|
.list_events(calendar_id, None, None, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?;
|
||||||
|
|
||||||
@@ -470,7 +470,7 @@ async fn handle_report(
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
CalDavReportType::SyncCollection { .. } => calendar_service
|
CalDavReportType::SyncCollection { .. } => calendar_service
|
||||||
.list_events(calendar_id, None, None, &user.id)
|
.list_events(calendar_id, None, None, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?,
|
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?,
|
||||||
};
|
};
|
||||||
@@ -526,7 +526,7 @@ async fn handle_mkcalendar(
|
|||||||
};
|
};
|
||||||
|
|
||||||
calendar_service
|
calendar_service
|
||||||
.create_calendar(create_dto, &user.id)
|
.create_calendar(create_dto, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to create calendar: {}", e)))?;
|
.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 existing = if let Some(ref uid) = ical_uid {
|
||||||
let events = calendar_service
|
let events = calendar_service
|
||||||
.list_events(calendar_id, None, None, &user.id)
|
.list_events(calendar_id, None, None, user.id)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
events.into_iter().find(|e| e.ical_uid == *uid)
|
events.into_iter().find(|e| e.ical_uid == *uid)
|
||||||
@@ -577,7 +577,7 @@ async fn handle_put(
|
|||||||
if let Some(existing_event) = existing {
|
if let Some(existing_event) = existing {
|
||||||
// Update existing event — re-create from iCal for full fidelity
|
// Update existing event — re-create from iCal for full fidelity
|
||||||
calendar_service
|
calendar_service
|
||||||
.delete_event(&existing_event.id, &user.id)
|
.delete_event(&existing_event.id, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to update event: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Failed to update event: {}", e)))?;
|
||||||
|
|
||||||
@@ -586,7 +586,7 @@ async fn handle_put(
|
|||||||
ical_data,
|
ical_data,
|
||||||
};
|
};
|
||||||
let event = calendar_service
|
let event = calendar_service
|
||||||
.create_event_from_ical(create_dto, &user.id)
|
.create_event_from_ical(create_dto, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to recreate event: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Failed to recreate event: {}", e)))?;
|
||||||
|
|
||||||
@@ -602,7 +602,7 @@ async fn handle_put(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let event = calendar_service
|
let event = calendar_service
|
||||||
.create_event_from_ical(create_dto, &user.id)
|
.create_event_from_ical(create_dto, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to create event: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Failed to create event: {}", e)))?;
|
||||||
|
|
||||||
@@ -641,12 +641,12 @@ async fn handle_get(
|
|||||||
if parts.len() < 2 {
|
if parts.len() < 2 {
|
||||||
// GET on calendar collection
|
// GET on calendar collection
|
||||||
let events = calendar_service
|
let events = calendar_service
|
||||||
.list_events(calendar_id, None, None, &user.id)
|
.list_events(calendar_id, None, None, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?;
|
||||||
|
|
||||||
let calendar = calendar_service
|
let calendar = calendar_service
|
||||||
.get_calendar(calendar_id, &user.id)
|
.get_calendar(calendar_id, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::not_found(format!("Calendar not found: {}", e)))?;
|
.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 ical_uid = event_file.trim_end_matches(".ics");
|
||||||
|
|
||||||
let events = calendar_service
|
let events = calendar_service
|
||||||
.list_events(calendar_id, None, None, &user.id)
|
.list_events(calendar_id, None, None, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?;
|
||||||
|
|
||||||
@@ -760,7 +760,7 @@ async fn handle_delete(
|
|||||||
|
|
||||||
if parts.len() < 2 {
|
if parts.len() < 2 {
|
||||||
calendar_service
|
calendar_service
|
||||||
.delete_calendar(calendar_id, &user.id)
|
.delete_calendar(calendar_id, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to delete calendar: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Failed to delete calendar: {}", e)))?;
|
||||||
} else {
|
} else {
|
||||||
@@ -768,7 +768,7 @@ async fn handle_delete(
|
|||||||
let ical_uid = event_file.trim_end_matches(".ics");
|
let ical_uid = event_file.trim_end_matches(".ics");
|
||||||
|
|
||||||
let events = calendar_service
|
let events = calendar_service
|
||||||
.list_events(calendar_id, None, None, &user.id)
|
.list_events(calendar_id, None, None, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?;
|
.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)))?;
|
.ok_or_else(|| AppError::not_found(format!("Event not found: {}", ical_uid)))?;
|
||||||
|
|
||||||
calendar_service
|
calendar_service
|
||||||
.delete_event(&event.id, &user.id)
|
.delete_event(&event.id, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to delete event: {}", e)))?;
|
.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() {
|
if update.name.is_some() || update.description.is_some() || update.color.is_some() {
|
||||||
calendar_service
|
calendar_service
|
||||||
.update_calendar(calendar_id, update, &user.id)
|
.update_calendar(calendar_id, update, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to update calendar: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Failed to update calendar: {}", e)))?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ async fn handle_propfind(
|
|||||||
if path.is_empty() {
|
if path.is_empty() {
|
||||||
// Root CardDAV path — list user's address books
|
// Root CardDAV path — list user's address books
|
||||||
let address_books = addressbook_service
|
let address_books = addressbook_service
|
||||||
.list_user_address_books(&user.id)
|
.list_user_address_books(user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
AppError::internal_error(format!("Failed to list address books: {}", e))
|
AppError::internal_error(format!("Failed to list address books: {}", e))
|
||||||
@@ -231,13 +231,13 @@ async fn handle_propfind(
|
|||||||
if parts.len() == 1 {
|
if parts.len() == 1 {
|
||||||
// Address book collection
|
// Address book collection
|
||||||
let address_book = addressbook_service
|
let address_book = addressbook_service
|
||||||
.get_address_book(address_book_id, &user.id)
|
.get_address_book(address_book_id, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::not_found(format!("Address book not found: {}", e)))?;
|
.map_err(|e| AppError::not_found(format!("Address book not found: {}", e)))?;
|
||||||
|
|
||||||
let contacts = if depth != "0" {
|
let contacts = if depth != "0" {
|
||||||
contact_svc
|
contact_svc
|
||||||
.list_contacts(address_book_id, &user.id)
|
.list_contacts(address_book_id, user.id)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
@@ -269,7 +269,7 @@ async fn handle_propfind(
|
|||||||
|
|
||||||
// Look up by UID across all contacts in this address book
|
// Look up by UID across all contacts in this address book
|
||||||
let contacts = contact_svc
|
let contacts = contact_svc
|
||||||
.list_contacts(address_book_id, &user.id)
|
.list_contacts(address_book_id, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?;
|
||||||
|
|
||||||
@@ -331,12 +331,12 @@ async fn handle_report(
|
|||||||
|
|
||||||
let contacts = match &report {
|
let contacts = match &report {
|
||||||
CardDavReportType::AddressbookQuery { .. } => contact_svc
|
CardDavReportType::AddressbookQuery { .. } => contact_svc
|
||||||
.list_contacts(address_book_id, &user.id)
|
.list_contacts(address_book_id, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?,
|
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?,
|
||||||
CardDavReportType::AddressbookMultiget { hrefs, .. } => {
|
CardDavReportType::AddressbookMultiget { hrefs, .. } => {
|
||||||
let all_contacts = contact_svc
|
let all_contacts = contact_svc
|
||||||
.list_contacts(address_book_id, &user.id)
|
.list_contacts(address_book_id, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?;
|
||||||
|
|
||||||
@@ -346,7 +346,7 @@ async fn handle_report(
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
CardDavReportType::SyncCollection { .. } => contact_svc
|
CardDavReportType::SyncCollection { .. } => contact_svc
|
||||||
.list_contacts(address_book_id, &user.id)
|
.list_contacts(address_book_id, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?,
|
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?,
|
||||||
};
|
};
|
||||||
@@ -403,7 +403,7 @@ async fn handle_mkcol(
|
|||||||
|
|
||||||
let create_dto = CreateAddressBookDto {
|
let create_dto = CreateAddressBookDto {
|
||||||
name,
|
name,
|
||||||
owner_id: user.id.clone(),
|
owner_id: user.id.to_string(),
|
||||||
description,
|
description,
|
||||||
color,
|
color,
|
||||||
is_public: Some(false),
|
is_public: Some(false),
|
||||||
@@ -452,7 +452,7 @@ async fn handle_put(
|
|||||||
// Check if contact already exists
|
// Check if contact already exists
|
||||||
let existing = if let Some(ref uid) = vcard_uid {
|
let existing = if let Some(ref uid) = vcard_uid {
|
||||||
let contacts = contact_svc
|
let contacts = contact_svc
|
||||||
.list_contacts(address_book_id, &user.id)
|
.list_contacts(address_book_id, user.id)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
contacts.into_iter().find(|c| c.uid == *uid)
|
contacts.into_iter().find(|c| c.uid == *uid)
|
||||||
@@ -463,14 +463,14 @@ async fn handle_put(
|
|||||||
if let Some(existing_contact) = existing {
|
if let Some(existing_contact) = existing {
|
||||||
// Update: delete + recreate from vCard
|
// Update: delete + recreate from vCard
|
||||||
contact_svc
|
contact_svc
|
||||||
.delete_contact(&existing_contact.id, &user.id)
|
.delete_contact(&existing_contact.id, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to update contact: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Failed to update contact: {}", e)))?;
|
||||||
|
|
||||||
let create_dto = CreateContactVCardDto {
|
let create_dto = CreateContactVCardDto {
|
||||||
address_book_id: address_book_id.to_string(),
|
address_book_id: address_book_id.to_string(),
|
||||||
vcard: vcard_data,
|
vcard: vcard_data,
|
||||||
user_id: user.id.clone(),
|
user_id: user.id.to_string(),
|
||||||
};
|
};
|
||||||
let contact = contact_svc
|
let contact = contact_svc
|
||||||
.create_contact_from_vcard(create_dto)
|
.create_contact_from_vcard(create_dto)
|
||||||
@@ -486,7 +486,7 @@ async fn handle_put(
|
|||||||
let create_dto = CreateContactVCardDto {
|
let create_dto = CreateContactVCardDto {
|
||||||
address_book_id: address_book_id.to_string(),
|
address_book_id: address_book_id.to_string(),
|
||||||
vcard: vcard_data,
|
vcard: vcard_data,
|
||||||
user_id: user.id.clone(),
|
user_id: user.id.to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let contact = contact_svc
|
let contact = contact_svc
|
||||||
@@ -529,7 +529,7 @@ async fn handle_get(
|
|||||||
if parts.len() < 2 {
|
if parts.len() < 2 {
|
||||||
// GET on address book collection — return all contacts as vcf
|
// GET on address book collection — return all contacts as vcf
|
||||||
let contacts = contact_svc
|
let contacts = contact_svc
|
||||||
.list_contacts(address_book_id, &user.id)
|
.list_contacts(address_book_id, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?;
|
.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 contact_uid = contact_file.trim_end_matches(".vcf");
|
||||||
|
|
||||||
let contacts = contact_svc
|
let contacts = contact_svc
|
||||||
.list_contacts(address_book_id, &user.id)
|
.list_contacts(address_book_id, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?;
|
||||||
|
|
||||||
@@ -590,7 +590,7 @@ async fn handle_delete(
|
|||||||
if parts.len() < 2 {
|
if parts.len() < 2 {
|
||||||
// Delete address book
|
// Delete address book
|
||||||
addressbook_service
|
addressbook_service
|
||||||
.delete_address_book(address_book_id, &user.id)
|
.delete_address_book(address_book_id, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
AppError::internal_error(format!("Failed to delete address book: {}", 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 contact_uid = contact_file.trim_end_matches(".vcf");
|
||||||
|
|
||||||
let contacts = contact_svc
|
let contacts = contact_svc
|
||||||
.list_contacts(address_book_id, &user.id)
|
.list_contacts(address_book_id, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?;
|
.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)))?;
|
.ok_or_else(|| AppError::not_found(format!("Contact not found: {}", contact_uid)))?;
|
||||||
|
|
||||||
contact_svc
|
contact_svc
|
||||||
.delete_contact(&contact.id, &user.id)
|
.delete_contact(&contact.id, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to delete contact: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Failed to delete contact: {}", e)))?;
|
||||||
}
|
}
|
||||||
@@ -653,7 +653,7 @@ async fn handle_proppatch(
|
|||||||
description: None,
|
description: None,
|
||||||
color: None,
|
color: None,
|
||||||
is_public: None,
|
is_public: None,
|
||||||
user_id: user.id.clone(),
|
user_id: user.id.to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
for prop in &props_to_set {
|
for prop in &props_to_set {
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ impl ChunkedUploadHandler {
|
|||||||
// ── Quota enforcement ────────────────────────────────────
|
// ── Quota enforcement ────────────────────────────────────
|
||||||
if let Some(storage_svc) = state.storage_usage_service.as_ref()
|
if let Some(storage_svc) = state.storage_usage_service.as_ref()
|
||||||
&& let Err(err) = storage_svc
|
&& let Err(err) = storage_svc
|
||||||
.check_storage_quota(&auth_user.id, request.total_size)
|
.check_storage_quota(auth_user.id, request.total_size)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
@@ -146,7 +146,7 @@ impl ChunkedUploadHandler {
|
|||||||
|
|
||||||
match chunked_service
|
match chunked_service
|
||||||
.create_session(
|
.create_session(
|
||||||
&auth_user.id,
|
auth_user.id,
|
||||||
request.filename,
|
request.filename,
|
||||||
request.folder_id,
|
request.folder_id,
|
||||||
content_type,
|
content_type,
|
||||||
@@ -192,7 +192,7 @@ impl ChunkedUploadHandler {
|
|||||||
match chunked_service
|
match chunked_service
|
||||||
.upload_chunk(
|
.upload_chunk(
|
||||||
&upload_id,
|
&upload_id,
|
||||||
&auth_user.id,
|
auth_user.id,
|
||||||
params.chunk_index,
|
params.chunk_index,
|
||||||
body,
|
body,
|
||||||
checksum,
|
checksum,
|
||||||
@@ -233,7 +233,7 @@ impl ChunkedUploadHandler {
|
|||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let chunked_service = &state.core.chunked_upload_service;
|
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()
|
Ok(status) => Response::builder()
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
.header(header::CONTENT_TYPE, "application/json")
|
.header(header::CONTENT_TYPE, "application/json")
|
||||||
@@ -268,7 +268,7 @@ impl ChunkedUploadHandler {
|
|||||||
// Assemble chunks (hash-on-write: SHA-256 computed during assembly)
|
// Assemble chunks (hash-on-write: SHA-256 computed during assembly)
|
||||||
let (assembled_path, filename, folder_id, content_type, total_size, hash) =
|
let (assembled_path, filename, folder_id, content_type, total_size, hash) =
|
||||||
match chunked_service
|
match chunked_service
|
||||||
.complete_upload(&upload_id, &auth_user.id)
|
.complete_upload(&upload_id, auth_user.id)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(result) => result,
|
Ok(result) => result,
|
||||||
@@ -299,7 +299,7 @@ impl ChunkedUploadHandler {
|
|||||||
Ok(file) => {
|
Ok(file) => {
|
||||||
// Cleanup session
|
// Cleanup session
|
||||||
let _ = chunked_service
|
let _ = chunked_service
|
||||||
.finalize_upload(&upload_id, &auth_user.id)
|
.finalize_upload(&upload_id, auth_user.id)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
@@ -338,7 +338,7 @@ impl ChunkedUploadHandler {
|
|||||||
let chunked_service = &state.core.chunked_upload_service;
|
let chunked_service = &state.core.chunked_upload_service;
|
||||||
|
|
||||||
match chunked_service
|
match chunked_service
|
||||||
.cancel_upload(&upload_id, &auth_user.id)
|
.cancel_upload(&upload_id, auth_user.id)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => StatusCode::NO_CONTENT.into_response(),
|
Ok(_) => StatusCode::NO_CONTENT.into_response(),
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ impl DedupHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only reveal whether THIS user has the blob — no global oracle
|
// 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 {
|
if user_has_it {
|
||||||
// Fetch size from metadata (safe — user owns a reference)
|
// 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
|
// 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()
|
return Response::builder()
|
||||||
.status(StatusCode::NOT_FOUND)
|
.status(StatusCode::NOT_FOUND)
|
||||||
.header(header::CONTENT_TYPE, "application/json")
|
.header(header::CONTENT_TYPE, "application/json")
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use axum::{
|
|||||||
routing::{delete, get, post},
|
routing::{delete, get, post},
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::dtos::device_auth_dto::*;
|
use crate::application::dtos::device_auth_dto::*;
|
||||||
use crate::application::services::device_auth_service::DeviceAuthService;
|
use crate::application::services::device_auth_service::DeviceAuthService;
|
||||||
@@ -145,7 +146,7 @@ async fn device_verify_action(
|
|||||||
match body.action.to_lowercase().as_str() {
|
match body.action.to_lowercase().as_str() {
|
||||||
"approve" | "allow" | "accept" => {
|
"approve" | "allow" | "accept" => {
|
||||||
device_service
|
device_service
|
||||||
.approve(&body.user_code, &auth_user.id)
|
.approve(&body.user_code, auth_user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
tracing::error!("Device approve failed: {}", e);
|
tracing::error!("Device approve failed: {}", e);
|
||||||
@@ -181,7 +182,7 @@ async fn list_devices(
|
|||||||
let device_service = get_device_service(&state)?;
|
let device_service = get_device_service(&state)?;
|
||||||
|
|
||||||
let devices = device_service
|
let devices = device_service
|
||||||
.list_user_devices(&auth_user.id)
|
.list_user_devices(auth_user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
tracing::error!("List devices failed: {}", e);
|
tracing::error!("List devices failed: {}", e);
|
||||||
@@ -202,8 +203,10 @@ async fn revoke_device(
|
|||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
let device_service = get_device_service(&state)?;
|
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
|
device_service
|
||||||
.revoke_device(&device_id, &auth_user.id)
|
.revoke_device(device_id, auth_user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
tracing::error!("Revoke device failed: {}", e);
|
tracing::error!("Revoke device failed: {}", e);
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ pub async fn get_favorites(
|
|||||||
State(favorites_service): State<Arc<FavoritesService>>,
|
State(favorites_service): State<Arc<FavoritesService>>,
|
||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let user_id = &auth_user.id;
|
let user_id = auth_user.id;
|
||||||
|
|
||||||
match favorites_service.get_favorites(user_id).await {
|
match favorites_service.get_favorites(user_id).await {
|
||||||
Ok(favorites) => {
|
Ok(favorites) => {
|
||||||
@@ -56,7 +56,7 @@ pub async fn add_favorite(
|
|||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Path((item_type, item_id)): Path<(String, String)>,
|
Path((item_type, item_id)): Path<(String, String)>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let user_id = &auth_user.id;
|
let user_id = auth_user.id;
|
||||||
|
|
||||||
// Validate item_type
|
// Validate item_type
|
||||||
if item_type != "file" && item_type != "folder" {
|
if item_type != "file" && item_type != "folder" {
|
||||||
@@ -99,7 +99,7 @@ pub async fn remove_favorite(
|
|||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Path((item_type, item_id)): Path<(String, String)>,
|
Path((item_type, item_id)): Path<(String, String)>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let user_id = &auth_user.id;
|
let user_id = auth_user.id;
|
||||||
|
|
||||||
match favorites_service
|
match favorites_service
|
||||||
.remove_from_favorites(user_id, &item_id, &item_type)
|
.remove_from_favorites(user_id, &item_id, &item_type)
|
||||||
@@ -143,7 +143,7 @@ pub async fn batch_add_favorites(
|
|||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Json(body): Json<BatchFavoritesRequest>,
|
Json(body): Json<BatchFavoritesRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let user_id = &auth_user.id;
|
let user_id = auth_user.id;
|
||||||
|
|
||||||
if body.items.is_empty() {
|
if body.items.is_empty() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ impl FileHandler {
|
|||||||
if let Some(ref fid) = folder_id {
|
if let Some(ref fid) = folder_id {
|
||||||
use crate::application::ports::inbound::FolderUseCase;
|
use crate::application::ports::inbound::FolderUseCase;
|
||||||
let folder_service = &state.applications.folder_service;
|
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!(
|
tracing::warn!(
|
||||||
"⛔ UPLOAD REJECTED (IDOR): user='{}' attempted upload to folder '{}' owned by another user",
|
"⛔ UPLOAD REJECTED (IDOR): user='{}' attempted upload to folder '{}' owned by another user",
|
||||||
auth_user.username,
|
auth_user.username,
|
||||||
@@ -130,7 +130,7 @@ impl FileHandler {
|
|||||||
.and_then(|s| s.parse::<u64>().ok())
|
.and_then(|s| s.parse::<u64>().ok())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
if let Err(err) = storage_svc
|
if let Err(err) = storage_svc
|
||||||
.check_storage_quota(&auth_user.id, estimated_size)
|
.check_storage_quota(auth_user.id, estimated_size)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
@@ -235,7 +235,7 @@ impl FileHandler {
|
|||||||
// ── Quota enforcement ────────────────────────────────
|
// ── Quota enforcement ────────────────────────────────
|
||||||
if let Some(storage_svc) = state.storage_usage_service.as_ref()
|
if let Some(storage_svc) = state.storage_usage_service.as_ref()
|
||||||
&& let Err(err) = storage_svc
|
&& let Err(err) = storage_svc
|
||||||
.check_storage_quota(&auth_user.id, total_size)
|
.check_storage_quota(auth_user.id, total_size)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
let _ = tokio::fs::remove_file(&temp_path).await;
|
let _ = tokio::fs::remove_file(&temp_path).await;
|
||||||
@@ -321,7 +321,7 @@ impl FileHandler {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let file = match file_retrieval_service
|
let file = match file_retrieval_service
|
||||||
.get_file_owned(&id, &auth_user.id)
|
.get_file_owned(&id, auth_user.id)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
@@ -397,7 +397,7 @@ impl FileHandler {
|
|||||||
let retrieval = &state.applications.file_retrieval_service;
|
let retrieval = &state.applications.file_retrieval_service;
|
||||||
|
|
||||||
// ── Get file metadata (ownership-scoped) ────────────────────────
|
// ── 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,
|
Ok(f) => f,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return AppError::from(err).into_response();
|
return AppError::from(err).into_response();
|
||||||
@@ -455,7 +455,7 @@ impl FileHandler {
|
|||||||
Self::content_disposition(&file_dto.name, &file_dto.mime_type, ¶ms);
|
Self::content_disposition(&file_dto.name, &file_dto.mime_type, ¶ms);
|
||||||
|
|
||||||
match retrieval
|
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
|
.await
|
||||||
{
|
{
|
||||||
Ok(stream) => {
|
Ok(stream) => {
|
||||||
@@ -569,7 +569,7 @@ impl FileHandler {
|
|||||||
tracing::info!("API: Listing files with folder_id: {:?}", folder_id);
|
tracing::info!("API: Listing files with folder_id: {:?}", folder_id);
|
||||||
|
|
||||||
let retrieval = &state.applications.file_retrieval_service;
|
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) => {
|
Ok(files) => {
|
||||||
// Compute lightweight ETag from max modified_at + count
|
// Compute lightweight ETag from max modified_at + count
|
||||||
let max_mod = files.iter().map(|f| f.modified_at).max().unwrap_or(0);
|
let max_mod = files.iter().map(|f| f.modified_at).max().unwrap_or(0);
|
||||||
@@ -655,7 +655,7 @@ impl FileHandler {
|
|||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Verify ownership
|
// Verify ownership
|
||||||
let file_read = &state.repositories.file_read_repository;
|
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();
|
let msg = e.to_string();
|
||||||
return (
|
return (
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
@@ -703,7 +703,7 @@ impl FileHandler {
|
|||||||
|
|
||||||
// Auth required: trash-first with dedup cleanup + ownership verification
|
// Auth required: trash-first with dedup cleanup + ownership verification
|
||||||
let result = mgmt
|
let result = mgmt
|
||||||
.delete_with_cleanup(&id, &auth_user.id)
|
.delete_with_cleanup(&id, auth_user.id)
|
||||||
.await
|
.await
|
||||||
.map(|was_trashed| {
|
.map(|was_trashed| {
|
||||||
if was_trashed {
|
if was_trashed {
|
||||||
@@ -745,7 +745,7 @@ impl FileHandler {
|
|||||||
|
|
||||||
tracing::info!("Renaming file {} to \"{}\"", id, new_name);
|
tracing::info!("Renaming file {} to \"{}\"", id, new_name);
|
||||||
let mgmt = &state.applications.file_management_service;
|
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(),
|
Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(),
|
||||||
Err(err) => AppError::from(err).into_response()
|
Err(err) => AppError::from(err).into_response()
|
||||||
}
|
}
|
||||||
@@ -763,7 +763,7 @@ impl FileHandler {
|
|||||||
let mgmt = &state.applications.file_management_service;
|
let mgmt = &state.applications.file_management_service;
|
||||||
|
|
||||||
match mgmt
|
match mgmt
|
||||||
.move_file_owned(&id, &auth_user.id, payload.folder_id)
|
.move_file_owned(&id, auth_user.id, payload.folder_id)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(file) => (StatusCode::OK, Json(file)).into_response(),
|
Ok(file) => (StatusCode::OK, Json(file)).into_response(),
|
||||||
@@ -784,7 +784,7 @@ impl FileHandler {
|
|||||||
.map(|s| s.to_string());
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
let mgmt = &state.applications.file_management_service;
|
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(),
|
Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(),
|
||||||
Err(err) => AppError::from(err).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",
|
"create_folder: parent_id is None for user '{}', resolving home folder",
|
||||||
auth_user.username
|
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) => {
|
Ok(folders) => {
|
||||||
if let Some(home) = folders.first() {
|
if let Some(home) = folders.first() {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
@@ -71,7 +71,7 @@ impl FolderHandler {
|
|||||||
if let Some(ref parent_id) = dto.parent_id {
|
if let Some(ref parent_id) = dto.parent_id {
|
||||||
use crate::application::ports::inbound::FolderUseCase;
|
use crate::application::ports::inbound::FolderUseCase;
|
||||||
if service
|
if service
|
||||||
.get_folder_owned(parent_id, &auth_user.id)
|
.get_folder_owned(parent_id, auth_user.id)
|
||||||
.await
|
.await
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
@@ -102,7 +102,7 @@ impl FolderHandler {
|
|||||||
Ok(folder) => {
|
Ok(folder) => {
|
||||||
// Access check: folder must belong to the requesting user
|
// Access check: folder must belong to the requesting user
|
||||||
if let Some(ref owner) = folder.owner_id
|
if let Some(ref owner) = folder.owner_id
|
||||||
&& owner != &auth_user.id
|
&& owner != &auth_user.id.to_string()
|
||||||
{
|
{
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"get_folder: user '{}' attempted to access folder '{}' owned by '{}'",
|
"get_folder: user '{}' attempted to access folder '{}' owned by '{}'",
|
||||||
@@ -155,7 +155,7 @@ impl FolderHandler {
|
|||||||
pagination: Query<PaginationRequestDto>,
|
pagination: Query<PaginationRequestDto>,
|
||||||
) -> axum::response::Response {
|
) -> axum::response::Response {
|
||||||
match service
|
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
|
.await
|
||||||
{
|
{
|
||||||
Ok(paginated_result) => (StatusCode::OK, Json(paginated_result)).into_response(),
|
Ok(paginated_result) => (StatusCode::OK, Json(paginated_result)).into_response(),
|
||||||
@@ -172,7 +172,7 @@ impl FolderHandler {
|
|||||||
auth_user: &AuthUser,
|
auth_user: &AuthUser,
|
||||||
) -> axum::response::Response {
|
) -> axum::response::Response {
|
||||||
match service
|
match service
|
||||||
.list_folders_for_owner(parent_id, &auth_user.id)
|
.list_folders_for_owner(parent_id, auth_user.id)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(folders) => (StatusCode::OK, Json(folders)).into_response(),
|
Ok(folders) => (StatusCode::OK, Json(folders)).into_response(),
|
||||||
@@ -215,8 +215,8 @@ impl FolderHandler {
|
|||||||
|
|
||||||
// Run both queries concurrently — no sequential wait.
|
// Run both queries concurrently — no sequential wait.
|
||||||
let (folders_result, files_result) = tokio::join!(
|
let (folders_result, files_result) = tokio::join!(
|
||||||
folder_service.list_folders_for_owner(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)
|
file_service.list_files_owned(Some(&id), auth_user.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
match (folders_result, files_result) {
|
match (folders_result, files_result) {
|
||||||
@@ -253,7 +253,7 @@ impl FolderHandler {
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Json(dto): Json<RenameFolderDto>,
|
Json(dto): Json<RenameFolderDto>,
|
||||||
) -> impl IntoResponse {
|
) -> 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(),
|
Ok(folder) => (StatusCode::OK, Json(folder)).into_response(),
|
||||||
Err(err) => AppError::from(err).into_response(),
|
Err(err) => AppError::from(err).into_response(),
|
||||||
}
|
}
|
||||||
@@ -266,7 +266,7 @@ impl FolderHandler {
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Json(dto): Json<MoveFolderDto>,
|
Json(dto): Json<MoveFolderDto>,
|
||||||
) -> impl IntoResponse {
|
) -> 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(),
|
Ok(folder) => (StatusCode::OK, Json(folder)).into_response(),
|
||||||
Err(err) => AppError::from(err).into_response(),
|
Err(err) => AppError::from(err).into_response(),
|
||||||
}
|
}
|
||||||
@@ -278,7 +278,7 @@ impl FolderHandler {
|
|||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> 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(),
|
Ok(_) => StatusCode::NO_CONTENT.into_response(),
|
||||||
Err(err) => AppError::from(err).into_response(),
|
Err(err) => AppError::from(err).into_response(),
|
||||||
}
|
}
|
||||||
@@ -290,7 +290,7 @@ impl FolderHandler {
|
|||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let user_id = &auth_user.id;
|
let user_id = auth_user.id;
|
||||||
// Check if trash service is available
|
// Check if trash service is available
|
||||||
if let Some(trash_service) = &state.trash_service {
|
if let Some(trash_service) = &state.trash_service {
|
||||||
tracing::info!("Moving folder to trash: {}", id);
|
tracing::info!("Moving folder to trash: {}", id);
|
||||||
@@ -337,7 +337,7 @@ impl FolderHandler {
|
|||||||
match folder_service.get_folder(&id).await {
|
match folder_service.get_folder(&id).await {
|
||||||
Ok(folder) => {
|
Ok(folder) => {
|
||||||
// Access check: folder must belong to the requesting user
|
// 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!(
|
tracing::warn!(
|
||||||
"download_folder_zip: user '{}' attempted to download folder '{}' owned by '{:?}'",
|
"download_folder_zip: user '{}' attempted to download folder '{}' owned by '{:?}'",
|
||||||
auth_user.id,
|
auth_user.id,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ pub async fn list_photos(
|
|||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Query(params): Query<PhotosQueryParams>,
|
Query(params): Query<PhotosQueryParams>,
|
||||||
) -> impl IntoResponse {
|
) -> 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 limit = params.limit.unwrap_or(200).clamp(1, 500);
|
||||||
|
|
||||||
let file_read = &state.repositories.file_read_repository;
|
let file_read = &state.repositories.file_read_repository;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ pub async fn get_recent_items(
|
|||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Query(params): Query<GetRecentParams>,
|
Query(params): Query<GetRecentParams>,
|
||||||
) -> impl IntoResponse {
|
) -> 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 {
|
match recent_service.get_recent_items(user_id, params.limit).await {
|
||||||
Ok(items) => {
|
Ok(items) => {
|
||||||
@@ -51,7 +51,7 @@ pub async fn record_item_access(
|
|||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Path((item_type, item_id)): Path<(String, String)>,
|
Path((item_type, item_id)): Path<(String, String)>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let user_id = &auth_user.id;
|
let user_id = auth_user.id;
|
||||||
|
|
||||||
// Validate item type
|
// Validate item type
|
||||||
if item_type != "file" && item_type != "folder" {
|
if item_type != "file" && item_type != "folder" {
|
||||||
@@ -97,7 +97,7 @@ pub async fn remove_from_recent(
|
|||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Path((item_type, item_id)): Path<(String, String)>,
|
Path((item_type, item_id)): Path<(String, String)>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let user_id = &auth_user.id;
|
let user_id = auth_user.id;
|
||||||
|
|
||||||
match recent_service
|
match recent_service
|
||||||
.remove_from_recent(user_id, &item_id, &item_type)
|
.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>>,
|
State(recent_service): State<Arc<RecentService>>,
|
||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let user_id = &auth_user.id;
|
let user_id = auth_user.id;
|
||||||
|
|
||||||
match recent_service.clear_recent_items(user_id).await {
|
match recent_service.clear_recent_items(user_id).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ impl SearchHandler {
|
|||||||
sort_by: params.sort_by.unwrap_or_else(|| "relevance".to_string()),
|
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) => {
|
Ok(results) => {
|
||||||
info!(
|
info!(
|
||||||
"Search completed in {}ms — {} files, {} folders",
|
"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) => {
|
Ok(results) => {
|
||||||
info!(
|
info!(
|
||||||
"Advanced search completed in {}ms — {} files, {} folders",
|
"Advanced search completed in {}ms — {} files, {} folders",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Json,
|
Json,
|
||||||
@@ -40,7 +41,7 @@ pub async fn create_shared_link(
|
|||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Json(dto): Json<CreateShareDto>,
|
Json(dto): Json<CreateShareDto>,
|
||||||
) -> impl IntoResponse {
|
) -> 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(),
|
Ok(share) => (StatusCode::CREATED, Json(share)).into_response(),
|
||||||
Err(err) => AppError::from(err).into_response(),
|
Err(err) => AppError::from(err).into_response(),
|
||||||
}
|
}
|
||||||
@@ -52,7 +53,11 @@ pub async fn get_shared_link(
|
|||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> 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(),
|
Ok(share) => (StatusCode::OK, Json(share)).into_response(),
|
||||||
Err(err) => AppError::from(err).into_response(),
|
Err(err) => AppError::from(err).into_response(),
|
||||||
}
|
}
|
||||||
@@ -65,7 +70,7 @@ pub async fn get_user_shares(
|
|||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Query(query): Query<GetSharesQuery>,
|
Query(query): Query<GetSharesQuery>,
|
||||||
) -> impl IntoResponse {
|
) -> 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 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) {
|
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>,
|
Path(id): Path<String>,
|
||||||
Json(dto): Json<UpdateShareDto>,
|
Json(dto): Json<UpdateShareDto>,
|
||||||
) -> impl IntoResponse {
|
) -> 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
|
match share_use_case
|
||||||
.update_shared_link(&id, &auth_user.id, dto)
|
.update_shared_link(id, auth_user.id, dto)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(share) => (StatusCode::OK, Json(share)).into_response(),
|
Ok(share) => (StatusCode::OK, Json(share)).into_response(),
|
||||||
@@ -123,7 +132,11 @@ pub async fn delete_shared_link(
|
|||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> 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(),
|
Ok(_) => StatusCode::NO_CONTENT.into_response(),
|
||||||
Err(err) => AppError::from(err).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.
|
// SECURITY: Always use the authenticated user's ID from the JWT token.
|
||||||
// Never allow user ID override via query parameters to prevent
|
// Never allow user ID override via query parameters to prevent
|
||||||
// privilege escalation attacks.
|
// 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);
|
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 {
|
match result {
|
||||||
Ok(items) => {
|
Ok(items) => {
|
||||||
@@ -60,7 +60,7 @@ pub async fn move_file_to_trash(
|
|||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Path(item_id): Path<String>,
|
Path(item_id): Path<String>,
|
||||||
) -> (StatusCode, Json<serde_json::Value>) {
|
) -> (StatusCode, Json<serde_json::Value>) {
|
||||||
let user_id = &auth_user.id;
|
let user_id = auth_user.id;
|
||||||
debug!(
|
debug!(
|
||||||
"Request to move file to trash: id={}, user={}",
|
"Request to move file to trash: id={}, user={}",
|
||||||
item_id, user_id
|
item_id, user_id
|
||||||
@@ -111,7 +111,7 @@ pub async fn move_folder_to_trash(
|
|||||||
auth_user: AuthUser,
|
auth_user: AuthUser,
|
||||||
Path(item_id): Path<String>,
|
Path(item_id): Path<String>,
|
||||||
) -> (StatusCode, Json<serde_json::Value>) {
|
) -> (StatusCode, Json<serde_json::Value>) {
|
||||||
let user_id = &auth_user.id;
|
let user_id = auth_user.id;
|
||||||
debug!(
|
debug!(
|
||||||
"Request to move folder to trash: id={}, user={}",
|
"Request to move folder to trash: id={}, user={}",
|
||||||
item_id, user_id
|
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 {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
@@ -239,7 +239,7 @@ pub async fn delete_permanently(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let result = trash_service
|
let result = trash_service
|
||||||
.delete_permanently(&trash_id, &auth_user.id)
|
.delete_permanently(&trash_id, auth_user.id)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match result {
|
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 {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ async fn handle_webdav_methods(
|
|||||||
/// If `path` doesn't already start with the user's home folder name, prepend
|
/// 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.
|
/// 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.
|
/// 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 folder_service = &state.applications.folder_service;
|
||||||
let home_folders = folder_service
|
let home_folders = folder_service
|
||||||
.list_folders_for_owner(None, user_id)
|
.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.
|
// prefix when the path doesn't already include it.
|
||||||
// Extract user_id before any async call to keep the future Send.
|
// Extract user_id before any async call to keep the future Send.
|
||||||
let path = if !path.is_empty() && method.as_str() != "OPTIONS" {
|
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 {
|
if let Some(uid) = user_id {
|
||||||
resolve_webdav_path(&state, &uid, &path)
|
resolve_webdav_path(&state, uid, &path)
|
||||||
.await
|
.await
|
||||||
.unwrap_or(path)
|
.unwrap_or(path)
|
||||||
} else {
|
} else {
|
||||||
@@ -371,14 +371,14 @@ async fn handle_propfind(
|
|||||||
propfind_request,
|
propfind_request,
|
||||||
folder_service,
|
folder_service,
|
||||||
file_retrieval_service,
|
file_retrieval_service,
|
||||||
&user.id,
|
user.id,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single-query path resolution: folder OR file in one DB round-trip
|
// Single-query path resolution: folder OR file in one DB round-trip
|
||||||
if let Some(resolver) = &state.path_resolver {
|
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)) => {
|
Ok(ResolvedResource::Folder(folder)) => {
|
||||||
let folder_id = folder.id.clone();
|
let folder_id = folder.id.clone();
|
||||||
return build_streaming_propfind_response(
|
return build_streaming_propfind_response(
|
||||||
@@ -389,7 +389,7 @@ async fn handle_propfind(
|
|||||||
propfind_request,
|
propfind_request,
|
||||||
folder_service,
|
folder_service,
|
||||||
file_retrieval_service,
|
file_retrieval_service,
|
||||||
&user.id,
|
user.id,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -420,7 +420,7 @@ async fn handle_propfind(
|
|||||||
} else {
|
} else {
|
||||||
// Fallback: legacy double-query path when PathResolver is unavailable
|
// Fallback: legacy double-query path when PathResolver is unavailable
|
||||||
if let Ok(folder) = folder_service.get_folder_by_path(&path).await {
|
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();
|
let folder_id = folder.id.clone();
|
||||||
return build_streaming_propfind_response(
|
return build_streaming_propfind_response(
|
||||||
folder,
|
folder,
|
||||||
@@ -430,12 +430,12 @@ async fn handle_propfind(
|
|||||||
propfind_request,
|
propfind_request,
|
||||||
folder_service,
|
folder_service,
|
||||||
file_retrieval_service,
|
file_retrieval_service,
|
||||||
&user.id,
|
user.id,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
if let Ok(file) = file_retrieval_service.get_file_by_path(&path).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 buf = Vec::with_capacity(1024);
|
||||||
{
|
{
|
||||||
let mut xml_writer = Writer::new(&mut buf);
|
let mut xml_writer = Writer::new(&mut buf);
|
||||||
@@ -477,12 +477,11 @@ async fn build_streaming_propfind_response(
|
|||||||
propfind_request: PropFindRequest,
|
propfind_request: PropFindRequest,
|
||||||
folder_service: std::sync::Arc<FolderService>,
|
folder_service: std::sync::Arc<FolderService>,
|
||||||
file_retrieval_service: std::sync::Arc<FileRetrievalService>,
|
file_retrieval_service: std::sync::Arc<FileRetrievalService>,
|
||||||
user_id: &str,
|
user_id: Uuid,
|
||||||
) -> Result<Response<Body>, AppError> {
|
) -> Result<Response<Body>, AppError> {
|
||||||
let depth = depth.to_string();
|
let depth = depth.to_string();
|
||||||
let base_href = base_href.to_string();
|
let base_href = base_href.to_string();
|
||||||
let propfind_request = Arc::new(propfind_request);
|
let propfind_request = Arc::new(propfind_request);
|
||||||
let user_id = user_id.to_string();
|
|
||||||
|
|
||||||
let stream = async_stream::try_stream! {
|
let stream = async_stream::try_stream! {
|
||||||
// ── XML header + <D:multistatus> + folder entry ──────────
|
// ── XML header + <D:multistatus> + folder entry ──────────
|
||||||
@@ -512,7 +511,7 @@ async fn build_streaming_propfind_response(
|
|||||||
page_size: pagination.page_size,
|
page_size: pagination.page_size,
|
||||||
};
|
};
|
||||||
let result = folder_service
|
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
|
.await
|
||||||
.map_err(|e| std::io::Error::other(e.to_string()))?;
|
.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;
|
let mut offset: i64 = 0;
|
||||||
loop {
|
loop {
|
||||||
let batch: Vec<FileDto> = file_retrieval_service
|
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
|
.await
|
||||||
.map_err(|e| std::io::Error::other(e.to_string()))?;
|
.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
|
// Resolve file — user-scoped when PathResolver is available
|
||||||
let file = if let Some(resolver) = &state.path_resolver {
|
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::File(f)) => f,
|
||||||
Ok(ResolvedResource::Folder(_)) => {
|
Ok(ResolvedResource::Folder(_)) => {
|
||||||
return Err(AppError::bad_request("Cannot GET a directory"));
|
return Err(AppError::bad_request("Cannot GET a directory"));
|
||||||
@@ -690,7 +689,7 @@ async fn handle_get(
|
|||||||
.get_file_by_path(&path)
|
.get_file_by_path(&path)
|
||||||
.await
|
.await
|
||||||
.map_err(|_e| AppError::not_found(format!("File not found: {}", path)))?;
|
.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
|
f
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -740,7 +739,7 @@ async fn handle_head(
|
|||||||
|
|
||||||
// Single-query path resolution (user-scoped)
|
// Single-query path resolution (user-scoped)
|
||||||
if let Some(resolver) = &state.path_resolver {
|
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)) => {
|
Ok(ResolvedResource::Folder(folder)) => {
|
||||||
return Ok(Response::builder()
|
return Ok(Response::builder()
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
@@ -771,7 +770,7 @@ async fn handle_head(
|
|||||||
|
|
||||||
// Fallback: legacy double-query path (with ownership check)
|
// Fallback: legacy double-query path (with ownership check)
|
||||||
if let Ok(folder) = folder_service.get_folder_by_path(&path).await {
|
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()
|
return Ok(Response::builder()
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
.header(header::CONTENT_TYPE, "httpd/unix-directory")
|
.header(header::CONTENT_TYPE, "httpd/unix-directory")
|
||||||
@@ -786,7 +785,7 @@ async fn handle_head(
|
|||||||
.get_file_by_path(&path)
|
.get_file_by_path(&path)
|
||||||
.await
|
.await
|
||||||
.map_err(|_e| AppError::not_found(format!("Resource not found: {}", path)))?;
|
.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()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
@@ -840,7 +839,7 @@ async fn handle_put(
|
|||||||
// parent folder (create). Without this check a user could
|
// parent folder (create). Without this check a user could
|
||||||
// overwrite another user's file via a crafted PUT path.
|
// overwrite another user's file via a crafted PUT path.
|
||||||
if let Some(resolver) = &state.path_resolver {
|
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::File(_)) => { /* existing file owned by user — OK */ }
|
||||||
Ok(ResolvedResource::Folder(_)) => {
|
Ok(ResolvedResource::Folder(_)) => {
|
||||||
return Err(AppError::bad_request("Cannot PUT to a directory"));
|
return Err(AppError::bad_request("Cannot PUT to a directory"));
|
||||||
@@ -854,7 +853,7 @@ async fn handle_put(
|
|||||||
};
|
};
|
||||||
if !parent_path.is_empty() {
|
if !parent_path.is_empty() {
|
||||||
resolver
|
resolver
|
||||||
.resolve_path_for_user(parent_path, &user.id)
|
.resolve_path_for_user(parent_path, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
AppError::not_found(format!("Parent folder not found: {}", parent_path))
|
AppError::not_found(format!("Parent folder not found: {}", parent_path))
|
||||||
@@ -1052,10 +1051,10 @@ async fn handle_delete(
|
|||||||
|
|
||||||
// Single-query path resolution (user-scoped)
|
// Single-query path resolution (user-scoped)
|
||||||
if let Some(resolver) = &state.path_resolver {
|
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)) => {
|
Ok(ResolvedResource::Folder(folder)) => {
|
||||||
folder_service
|
folder_service
|
||||||
.delete_folder(&folder.id, &user.id)
|
.delete_folder(&folder.id, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
AppError::internal_error(format!("Failed to delete folder: {}", 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;
|
let folder_result = folder_service.get_folder_by_path(&path).await;
|
||||||
|
|
||||||
if let Ok(folder) = folder_result {
|
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
|
folder_service
|
||||||
.delete_folder(&folder.id, &user.id)
|
.delete_folder(&folder.id, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to delete folder: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Failed to delete folder: {}", e)))?;
|
||||||
} else {
|
} else {
|
||||||
@@ -1086,7 +1085,7 @@ async fn handle_delete(
|
|||||||
.get_file_by_path(&path)
|
.get_file_by_path(&path)
|
||||||
.await
|
.await
|
||||||
.map_err(|_e| AppError::not_found(format!("Resource not found: {}", path)))?;
|
.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
|
file_management_service
|
||||||
.delete_file(&file.id)
|
.delete_file(&file.id)
|
||||||
@@ -1157,7 +1156,7 @@ async fn handle_move(
|
|||||||
if !overwrite {
|
if !overwrite {
|
||||||
let dest_exists = if let Some(resolver) = &state.path_resolver {
|
let dest_exists = if let Some(resolver) = &state.path_resolver {
|
||||||
resolver
|
resolver
|
||||||
.exists_for_user(&destination_path, &user.id)
|
.exists_for_user(&destination_path, user.id)
|
||||||
.await
|
.await
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
} else {
|
} else {
|
||||||
@@ -1179,7 +1178,7 @@ async fn handle_move(
|
|||||||
|
|
||||||
// Resolve source: single-query when PathResolver is available (user-scoped)
|
// Resolve source: single-query when PathResolver is available (user-scoped)
|
||||||
if let Some(resolver) = &state.path_resolver {
|
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)) => {
|
Ok(ResolvedResource::Folder(folder)) => {
|
||||||
let dest_folder_name = destination_path
|
let dest_folder_name = destination_path
|
||||||
.split('/')
|
.split('/')
|
||||||
@@ -1200,7 +1199,7 @@ async fn handle_move(
|
|||||||
// SECURITY: verify destination parent belongs to caller (V-08)
|
// SECURITY: verify destination parent belongs to caller (V-08)
|
||||||
assert_owner(
|
assert_owner(
|
||||||
parent.owner_id.as_deref(),
|
parent.owner_id.as_deref(),
|
||||||
&user.id,
|
&user.id.to_string(),
|
||||||
dest_parent_path,
|
dest_parent_path,
|
||||||
)?;
|
)?;
|
||||||
Some(parent.id)
|
Some(parent.id)
|
||||||
@@ -1211,7 +1210,7 @@ async fn handle_move(
|
|||||||
};
|
};
|
||||||
|
|
||||||
folder_service
|
folder_service
|
||||||
.move_folder(&folder.id, move_dto, &user.id)
|
.move_folder(&folder.id, move_dto, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
AppError::internal_error(format!("Failed to move folder: {}", e))
|
AppError::internal_error(format!("Failed to move folder: {}", e))
|
||||||
@@ -1222,7 +1221,7 @@ async fn handle_move(
|
|||||||
name: dest_folder_name.to_string(),
|
name: dest_folder_name.to_string(),
|
||||||
};
|
};
|
||||||
folder_service
|
folder_service
|
||||||
.rename_folder(&folder.id, rename_dto, &user.id)
|
.rename_folder(&folder.id, rename_dto, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
AppError::internal_error(format!("Failed to rename folder: {}", e))
|
AppError::internal_error(format!("Failed to rename folder: {}", e))
|
||||||
@@ -1251,7 +1250,7 @@ async fn handle_move(
|
|||||||
&& let Ok(parent) =
|
&& let Ok(parent) =
|
||||||
folder_service.get_folder_by_path(dest_parent_path).await
|
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
|
file_management_service
|
||||||
.move_file(&file.id, Some(dest_parent_path.to_string()))
|
.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;
|
let folder_result = folder_service.get_folder_by_path(&source_path).await;
|
||||||
|
|
||||||
if let Ok(folder) = folder_result {
|
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
|
let dest_folder_name = destination_path
|
||||||
.split('/')
|
.split('/')
|
||||||
.next_back()
|
.next_back()
|
||||||
@@ -1299,7 +1298,7 @@ async fn handle_move(
|
|||||||
match folder_service.get_folder_by_path(dest_parent_path).await {
|
match folder_service.get_folder_by_path(dest_parent_path).await {
|
||||||
Ok(parent) => {
|
Ok(parent) => {
|
||||||
// SECURITY: verify destination parent belongs to caller (V-08)
|
// 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)
|
Some(parent.id)
|
||||||
}
|
}
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
@@ -1308,7 +1307,7 @@ async fn handle_move(
|
|||||||
};
|
};
|
||||||
|
|
||||||
folder_service
|
folder_service
|
||||||
.move_folder(&folder.id, move_dto, &user.id)
|
.move_folder(&folder.id, move_dto, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to move folder: {}", e)))?;
|
.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(),
|
name: dest_folder_name.to_string(),
|
||||||
};
|
};
|
||||||
folder_service
|
folder_service
|
||||||
.rename_folder(&folder.id, rename_dto, &user.id)
|
.rename_folder(&folder.id, rename_dto, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
AppError::internal_error(format!("Failed to rename folder: {}", e))
|
AppError::internal_error(format!("Failed to rename folder: {}", e))
|
||||||
@@ -1330,7 +1329,7 @@ async fn handle_move(
|
|||||||
.map_err(|_e| {
|
.map_err(|_e| {
|
||||||
AppError::not_found(format!("Resource not found: {}", source_path))
|
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
|
let dest_filename = destination_path
|
||||||
.split('/')
|
.split('/')
|
||||||
@@ -1352,7 +1351,7 @@ async fn handle_move(
|
|||||||
if !dest_parent_path.is_empty()
|
if !dest_parent_path.is_empty()
|
||||||
&& let Ok(parent) = folder_service.get_folder_by_path(dest_parent_path).await
|
&& 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
|
file_management_service
|
||||||
.move_file(&file.id, Some(dest_parent_path.to_string()))
|
.move_file(&file.id, Some(dest_parent_path.to_string()))
|
||||||
@@ -1438,7 +1437,7 @@ async fn handle_copy(
|
|||||||
if !overwrite {
|
if !overwrite {
|
||||||
let dest_exists = if let Some(resolver) = &state.path_resolver {
|
let dest_exists = if let Some(resolver) = &state.path_resolver {
|
||||||
resolver
|
resolver
|
||||||
.exists_for_user(&destination_path, &user.id)
|
.exists_for_user(&destination_path, user.id)
|
||||||
.await
|
.await
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
} else {
|
} else {
|
||||||
@@ -1460,7 +1459,7 @@ async fn handle_copy(
|
|||||||
|
|
||||||
// Resolve source: single-query when PathResolver is available (user-scoped)
|
// Resolve source: single-query when PathResolver is available (user-scoped)
|
||||||
if let Some(resolver) = &state.path_resolver {
|
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)) => {
|
Ok(ResolvedResource::Folder(folder)) => {
|
||||||
let recursive = depth != "0";
|
let recursive = depth != "0";
|
||||||
|
|
||||||
@@ -1480,7 +1479,7 @@ async fn handle_copy(
|
|||||||
match folder_service.get_folder_by_path(dest_parent_path).await {
|
match folder_service.get_folder_by_path(dest_parent_path).await {
|
||||||
Ok(parent) => {
|
Ok(parent) => {
|
||||||
// SECURITY: verify destination parent belongs to caller (V-08)
|
// 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)
|
Some(parent.id)
|
||||||
}
|
}
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
@@ -1528,7 +1527,7 @@ async fn handle_copy(
|
|||||||
match folder_service.get_folder_by_path(dest_parent_path).await {
|
match folder_service.get_folder_by_path(dest_parent_path).await {
|
||||||
Ok(parent) => {
|
Ok(parent) => {
|
||||||
// SECURITY: verify destination parent belongs to caller (V-08)
|
// 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)
|
Some(parent.id)
|
||||||
}
|
}
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
@@ -1553,7 +1552,7 @@ async fn handle_copy(
|
|||||||
let folder_result = folder_service.get_folder_by_path(&source_path).await;
|
let folder_result = folder_service.get_folder_by_path(&source_path).await;
|
||||||
|
|
||||||
if let Ok(folder) = folder_result {
|
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 recursive = depth != "0";
|
||||||
|
|
||||||
let dest_folder_name = destination_path
|
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 {
|
match folder_service.get_folder_by_path(dest_parent_path).await {
|
||||||
Ok(parent) => {
|
Ok(parent) => {
|
||||||
// SECURITY: verify destination parent belongs to caller (V-08)
|
// 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)
|
Some(parent.id)
|
||||||
}
|
}
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
@@ -1613,7 +1612,7 @@ async fn handle_copy(
|
|||||||
.map_err(|_e| {
|
.map_err(|_e| {
|
||||||
AppError::not_found(format!("Resource not found: {}", source_path))
|
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('/') {
|
let dest_parent_path = if let Some(idx) = destination_path.rfind('/') {
|
||||||
&destination_path[..idx]
|
&destination_path[..idx]
|
||||||
@@ -1627,7 +1626,7 @@ async fn handle_copy(
|
|||||||
match folder_service.get_folder_by_path(dest_parent_path).await {
|
match folder_service.get_folder_by_path(dest_parent_path).await {
|
||||||
Ok(parent) => {
|
Ok(parent) => {
|
||||||
// SECURITY: verify destination parent belongs to caller (V-08)
|
// 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)
|
Some(parent.id)
|
||||||
}
|
}
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
@@ -1737,7 +1736,7 @@ async fn handle_lock(
|
|||||||
let token = format!("opaquelocktoken:{}", Uuid::new_v4());
|
let token = format!("opaquelocktoken:{}", Uuid::new_v4());
|
||||||
let lock_info = LockInfo {
|
let lock_info = LockInfo {
|
||||||
token,
|
token,
|
||||||
owner: owner.or(Some(user.id.clone())),
|
owner: owner.or(Some(user.id.to_string())),
|
||||||
depth: depth.to_string(),
|
depth: depth.to_string(),
|
||||||
timeout,
|
timeout,
|
||||||
scope,
|
scope,
|
||||||
|
|||||||
@@ -396,7 +396,7 @@ pub struct EditorUrlResponse {
|
|||||||
async fn authorize_wopi_access<S: FileRetrievalUseCase>(
|
async fn authorize_wopi_access<S: FileRetrievalUseCase>(
|
||||||
file_retrieval: &S,
|
file_retrieval: &S,
|
||||||
file_id: &str,
|
file_id: &str,
|
||||||
caller_id: &str,
|
caller_id: uuid::Uuid,
|
||||||
requested_action: &str,
|
requested_action: &str,
|
||||||
) -> Result<(crate::application::dtos::file_dto::FileDto, bool), StatusCode> {
|
) -> Result<(crate::application::dtos::file_dto::FileDto, bool), StatusCode> {
|
||||||
let file = file_retrieval
|
let file = file_retrieval
|
||||||
@@ -425,7 +425,7 @@ pub async fn get_editor_url(
|
|||||||
let (file, can_write) = match authorize_wopi_access(
|
let (file, can_write) = match authorize_wopi_access(
|
||||||
state.app_state.applications.file_retrieval_service.as_ref(),
|
state.app_state.applications.file_retrieval_service.as_ref(),
|
||||||
¶ms.file_id,
|
¶ms.file_id,
|
||||||
&user_id,
|
user_id,
|
||||||
¶ms.action,
|
¶ms.action,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -464,7 +464,7 @@ pub async fn get_editor_url(
|
|||||||
let (access_token, access_token_ttl) =
|
let (access_token, access_token_ttl) =
|
||||||
match state
|
match state
|
||||||
.token_service
|
.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,
|
Ok(t) => t,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -503,10 +503,14 @@ async fn host_page(
|
|||||||
|
|
||||||
// Re-verify ownership even though the token was valid — defence in depth.
|
// Re-verify ownership even though the token was valid — defence in depth.
|
||||||
let requested_action = if claims.can_write { "edit" } else { "view" };
|
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(
|
let file = match authorize_wopi_access(
|
||||||
state.app_state.applications.file_retrieval_service.as_ref(),
|
state.app_state.applications.file_retrieval_service.as_ref(),
|
||||||
&file_id,
|
&file_id,
|
||||||
&claims.sub,
|
caller_uuid,
|
||||||
requested_action,
|
requested_action,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use axum::{
|
|||||||
};
|
};
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::common::di::AppState;
|
use crate::common::di::AppState;
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ pub struct CookieAuthenticated;
|
|||||||
// Structure for use in Axum extractors
|
// Structure for use in Axum extractors
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct AuthUser {
|
pub struct AuthUser {
|
||||||
pub id: String,
|
pub id: Uuid,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub role: String,
|
pub role: String,
|
||||||
}
|
}
|
||||||
@@ -35,7 +36,7 @@ pub struct AuthUser {
|
|||||||
/// async fn my_handler(CurrentUserId(user_id): CurrentUserId) -> impl IntoResponse { ... }
|
/// async fn my_handler(CurrentUserId(user_id): CurrentUserId) -> impl IntoResponse { ... }
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct CurrentUserId(pub String);
|
pub struct CurrentUserId(pub Uuid);
|
||||||
|
|
||||||
// Implement FromRequestParts for AuthUser — allows using `auth_user: AuthUser` in handlers
|
// Implement FromRequestParts for AuthUser — allows using `auth_user: AuthUser` in handlers
|
||||||
impl<S> FromRequestParts<S> for AuthUser
|
impl<S> FromRequestParts<S> for AuthUser
|
||||||
@@ -49,7 +50,7 @@ where
|
|||||||
.extensions
|
.extensions
|
||||||
.get::<Arc<CurrentUser>>()
|
.get::<Arc<CurrentUser>>()
|
||||||
.map(|cu| AuthUser {
|
.map(|cu| AuthUser {
|
||||||
id: cu.id.clone(),
|
id: cu.id,
|
||||||
username: cu.username.clone(),
|
username: cu.username.clone(),
|
||||||
role: cu.role.clone(),
|
role: cu.role.clone(),
|
||||||
})
|
})
|
||||||
@@ -86,7 +87,7 @@ where
|
|||||||
parts
|
parts
|
||||||
.extensions
|
.extensions
|
||||||
.get::<Arc<CurrentUser>>()
|
.get::<Arc<CurrentUser>>()
|
||||||
.map(|cu| CurrentUserId(cu.id.clone()))
|
.map(|cu| CurrentUserId(cu.id))
|
||||||
.ok_or(AuthError::UserNotFound)
|
.ok_or(AuthError::UserNotFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,7 +95,7 @@ where
|
|||||||
/// Optional user ID extractor – never fails.
|
/// Optional user ID extractor – never fails.
|
||||||
/// Yields `Some(id)` when auth middleware ran, `None` otherwise.
|
/// Yields `Some(id)` when auth middleware ran, `None` otherwise.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct OptionalUserId(pub Option<String>);
|
pub struct OptionalUserId(pub Option<Uuid>);
|
||||||
|
|
||||||
impl<S> FromRequestParts<S> for OptionalUserId
|
impl<S> FromRequestParts<S> for OptionalUserId
|
||||||
where
|
where
|
||||||
@@ -107,7 +108,7 @@ where
|
|||||||
parts
|
parts
|
||||||
.extensions
|
.extensions
|
||||||
.get::<Arc<CurrentUser>>()
|
.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> {
|
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||||
Ok(OptionalAuthUser(parts.extensions.get::<Arc<CurrentUser>>().map(
|
Ok(OptionalAuthUser(parts.extensions.get::<Arc<CurrentUser>>().map(
|
||||||
|cu| AuthUser {
|
|cu| AuthUser {
|
||||||
id: cu.id.clone(),
|
id: cu.id,
|
||||||
username: cu.username.clone(),
|
username: cu.username.clone(),
|
||||||
role: cu.role.clone(),
|
role: cu.role.clone(),
|
||||||
},
|
},
|
||||||
@@ -216,8 +217,11 @@ pub async fn auth_middleware(
|
|||||||
"Token validated successfully for user: {}",
|
"Token validated successfully for user: {}",
|
||||||
claims.username
|
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 {
|
let current_user = Arc::new(CurrentUser {
|
||||||
id: claims.sub,
|
id: user_id,
|
||||||
username: claims.username,
|
username: claims.username,
|
||||||
email: claims.email,
|
email: claims.email,
|
||||||
role: claims.role,
|
role: claims.role,
|
||||||
@@ -303,8 +307,11 @@ pub async fn auth_middleware(
|
|||||||
match token_service.validate_token(&token_str) {
|
match token_service.validate_token(&token_str) {
|
||||||
Ok(claims) => {
|
Ok(claims) => {
|
||||||
tracing::debug!("Cookie token validated for user: {}", claims.username);
|
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 {
|
let current_user = Arc::new(CurrentUser {
|
||||||
id: claims.sub,
|
id: user_id,
|
||||||
username: claims.username,
|
username: claims.username,
|
||||||
email: claims.email,
|
email: claims.email,
|
||||||
role: claims.role,
|
role: claims.role,
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ pub async fn handle_login_submit(
|
|||||||
|
|
||||||
let app_password = match nextcloud
|
let app_password = match nextcloud
|
||||||
.app_passwords
|
.app_passwords
|
||||||
.create_nc(¤t_user.id, "Nextcloud")
|
.create_nc(current_user.id, "Nextcloud")
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok((_id, password)) => password,
|
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 {
|
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() {
|
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),
|
Ok((used, total)) => (used, total),
|
||||||
Err(_) => (0, 0),
|
Err(_) => (0, 0),
|
||||||
},
|
},
|
||||||
@@ -151,7 +151,7 @@ async fn user_provisioning_response(
|
|||||||
|
|
||||||
// Fetch quota from storage usage service
|
// Fetch quota from storage usage service
|
||||||
let quota: (i64, i64) = match state.storage_usage_service.as_ref() {
|
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),
|
Ok((used, total)) => (used, total),
|
||||||
Err(_) => (0, 0),
|
Err(_) => (0, 0),
|
||||||
},
|
},
|
||||||
@@ -212,7 +212,7 @@ pub async fn handle_revoke_apppassword(
|
|||||||
|
|
||||||
if let Err(e) = nextcloud
|
if let Err(e) = nextcloud
|
||||||
.app_passwords
|
.app_passwords
|
||||||
.revoke_by_password(&user.id, &app_password)
|
.revoke_by_password(user.id, &app_password)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
tracing::warn!("Failed to revoke app password for {}: {}", user.id, e);
|
tracing::warn!("Failed to revoke app password for {}: {}", user.id, e);
|
||||||
@@ -367,7 +367,7 @@ pub async fn handle_search(
|
|||||||
..SearchCriteriaDto::default()
|
..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,
|
Ok(r) => r,
|
||||||
Err(_) => return empty_search_response().into_response(),
|
Err(_) => return empty_search_response().into_response(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -90,7 +90,8 @@ pub async fn handle_preview(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Verify the authenticated user owns this file
|
// 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()
|
return Response::builder()
|
||||||
.status(StatusCode::NOT_FOUND)
|
.status(StatusCode::NOT_FOUND)
|
||||||
.body(Body::from("File not found"))
|
.body(Body::from("File not found"))
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ async fn handle_filter_files(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let favorites = fav_svc
|
let favorites = fav_svc
|
||||||
.get_favorites(&user.id)
|
.get_favorites(user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to get favorites: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Failed to get favorites: {}", e)))?;
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ async fn handle_search(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let results = search_svc
|
let results = search_svc
|
||||||
.search(criteria, &user.id)
|
.search(criteria, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Search failed: {}", e)))?;
|
.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"))?;
|
.ok_or_else(|| AppError::internal_error("Trash service not available"))?;
|
||||||
|
|
||||||
let items = trash_svc
|
let items = trash_svc
|
||||||
.get_trash_items(&user.id)
|
.get_trash_items(user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to list trash: {}", e)))?;
|
.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"))?;
|
.ok_or_else(|| AppError::internal_error("Trash service not available"))?;
|
||||||
|
|
||||||
trash_svc
|
trash_svc
|
||||||
.restore_item(&id, &user.id)
|
.restore_item(&id, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to restore item: {}", e)))?;
|
.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"))?;
|
.ok_or_else(|| AppError::internal_error("Trash service not available"))?;
|
||||||
|
|
||||||
trash_svc
|
trash_svc
|
||||||
.empty_trash(&user.id)
|
.empty_trash(user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to empty trash: {}", e)))?;
|
.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"))?;
|
.ok_or_else(|| AppError::internal_error("Trash service not available"))?;
|
||||||
|
|
||||||
trash_svc
|
trash_svc
|
||||||
.delete_permanently(&id, &user.id)
|
.delete_permanently(&id, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
AppError::internal_error(format!("Failed to permanently delete item: {}", e))
|
AppError::internal_error(format!("Failed to permanently delete item: {}", e))
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ async fn handle_propfind(
|
|||||||
items.push((&sf.id, "folder"));
|
items.push((&sf.id, "folder"));
|
||||||
}
|
}
|
||||||
fav_svc
|
fav_svc
|
||||||
.batch_check_favorites(&user.id, &items)
|
.batch_check_favorites(user.id, &items)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
@@ -234,7 +234,7 @@ async fn handle_propfind(
|
|||||||
let favorite_ids = if let Some(fav_svc) = state.favorites_service.as_ref() {
|
let favorite_ids = if let Some(fav_svc) = state.favorites_service.as_ref() {
|
||||||
let items: Vec<(&str, &str)> = vec![(&file.id, "file")];
|
let items: Vec<(&str, &str)> = vec![(&file.id, "file")];
|
||||||
fav_svc
|
fav_svc
|
||||||
.batch_check_favorites(&user.id, &items)
|
.batch_check_favorites(user.id, &items)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
@@ -414,14 +414,14 @@ async fn handle_proppatch(
|
|||||||
if let Some(fav_svc) = state.favorites_service.as_ref() {
|
if let Some(fav_svc) = state.favorites_service.as_ref() {
|
||||||
if value == 1 {
|
if value == 1 {
|
||||||
fav_svc
|
fav_svc
|
||||||
.add_to_favorites(&user.id, &item_id, item_type)
|
.add_to_favorites(user.id, &item_id, item_type)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
AppError::internal_error(format!("Failed to add favorite: {}", e))
|
AppError::internal_error(format!("Failed to add favorite: {}", e))
|
||||||
})?;
|
})?;
|
||||||
} else {
|
} else {
|
||||||
fav_svc
|
fav_svc
|
||||||
.remove_from_favorites(&user.id, &item_id, item_type)
|
.remove_from_favorites(user.id, &item_id, item_type)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
AppError::internal_error(format!("Failed to remove favorite: {}", 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 Some(trash_svc) = state.trash_service.as_ref() {
|
||||||
if let Ok(folder) = folder_service.get_folder_by_path(&internal_path).await {
|
if let Ok(folder) = folder_service.get_folder_by_path(&internal_path).await {
|
||||||
trash_svc
|
trash_svc
|
||||||
.move_to_trash(&folder.id, "folder", &user.id)
|
.move_to_trash(&folder.id, "folder", user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to trash folder: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Failed to trash folder: {}", e)))?;
|
||||||
return Ok(Response::builder()
|
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 {
|
if let Ok(file) = file_service.get_file_by_path(&internal_path).await {
|
||||||
trash_svc
|
trash_svc
|
||||||
.move_to_trash(&file.id, "file", &user.id)
|
.move_to_trash(&file.id, "file", user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to trash file: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Failed to trash file: {}", e)))?;
|
||||||
return Ok(Response::builder()
|
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 {
|
if let Ok(folder) = folder_service.get_folder_by_path(&internal_path).await {
|
||||||
folder_service
|
folder_service
|
||||||
.delete_folder(&folder.id, &user.id)
|
.delete_folder(&folder.id, user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Failed to delete folder: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Failed to delete folder: {}", e)))?;
|
||||||
|
|
||||||
@@ -835,7 +835,7 @@ async fn handle_move(
|
|||||||
RenameFolderDto {
|
RenameFolderDto {
|
||||||
name: dest_name.to_string(),
|
name: dest_name.to_string(),
|
||||||
},
|
},
|
||||||
&user.id,
|
user.id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Rename failed: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Rename failed: {}", e)))?;
|
||||||
@@ -853,7 +853,7 @@ async fn handle_move(
|
|||||||
MoveFolderDto {
|
MoveFolderDto {
|
||||||
parent_id: Some(dest_parent.id.clone()),
|
parent_id: Some(dest_parent.id.clone()),
|
||||||
},
|
},
|
||||||
&user.id,
|
user.id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Move failed: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Move failed: {}", e)))?;
|
||||||
@@ -867,7 +867,7 @@ async fn handle_move(
|
|||||||
RenameFolderDto {
|
RenameFolderDto {
|
||||||
name: dest_name.to_string(),
|
name: dest_name.to_string(),
|
||||||
},
|
},
|
||||||
&user.id,
|
user.id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::internal_error(format!("Rename failed: {}", e)))?;
|
.map_err(|e| AppError::internal_error(format!("Rename failed: {}", e)))?;
|
||||||
|
|||||||
Reference in New Issue
Block a user