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

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

85 files changed, 1090 insertions(+), 1739 deletions(-)
This commit is contained in:
Diocrafts
2026-03-07 14:59:32 +01:00
parent 9f08460027
commit 06ed0455ce
85 changed files with 1090 additions and 1739 deletions
+19 -18
View File
@@ -3,6 +3,7 @@ use crate::domain::entities::app_password::AppPassword;
use crate::domain::entities::device_code::DeviceCode;
use crate::domain::entities::session::Session;
use crate::domain::entities::user::User;
use uuid::Uuid;
// ============================================================================
// Cryptography Ports - Extracted from Domain to maintain Clean Architecture
@@ -72,7 +73,7 @@ pub trait UserStoragePort: Send + Sync + 'static {
async fn create_user(&self, user: User) -> Result<User, DomainError>;
/// Gets a user by ID
async fn get_user_by_id(&self, id: &str) -> Result<User, DomainError>;
async fn get_user_by_id(&self, id: Uuid) -> Result<User, DomainError>;
/// Gets a user by username
async fn get_user_by_username(&self, username: &str) -> Result<User, DomainError>;
@@ -86,7 +87,7 @@ pub trait UserStoragePort: Send + Sync + 'static {
/// Updates only the storage usage of a user
async fn update_storage_usage(
&self,
user_id: &str,
user_id: Uuid,
usage_bytes: i64,
) -> Result<(), DomainError>;
@@ -100,10 +101,10 @@ pub trait UserStoragePort: Send + Sync + 'static {
async fn list_users_by_role(&self, role: &str) -> Result<Vec<User>, DomainError>;
/// Deletes a user by their ID
async fn delete_user(&self, user_id: &str) -> Result<(), DomainError>;
async fn delete_user(&self, user_id: Uuid) -> Result<(), DomainError>;
/// Changes a user's password
async fn change_password(&self, user_id: &str, password_hash: &str) -> Result<(), DomainError>;
async fn change_password(&self, user_id: Uuid, password_hash: &str) -> Result<(), DomainError>;
/// Finds a user by OIDC provider + subject pair
async fn get_user_by_oidc_subject(
@@ -113,15 +114,15 @@ pub trait UserStoragePort: Send + Sync + 'static {
) -> Result<User, DomainError>;
/// Activates or deactivates a user
async fn set_user_active_status(&self, user_id: &str, active: bool) -> Result<(), DomainError>;
async fn set_user_active_status(&self, user_id: Uuid, active: bool) -> Result<(), DomainError>;
/// Changes a user's role
async fn change_role(&self, user_id: &str, role: &str) -> Result<(), DomainError>;
async fn change_role(&self, user_id: Uuid, role: &str) -> Result<(), DomainError>;
/// Updates a user's storage quota
async fn update_storage_quota(
&self,
user_id: &str,
user_id: Uuid,
quota_bytes: i64,
) -> Result<(), DomainError>;
@@ -197,10 +198,10 @@ pub trait SessionStoragePort: Send + Sync + 'static {
) -> Result<Session, DomainError>;
/// Revokes a specific session
async fn revoke_session(&self, session_id: &str) -> Result<(), DomainError>;
async fn revoke_session(&self, session_id: Uuid) -> Result<(), DomainError>;
/// Revokes all sessions of a user
async fn revoke_all_user_sessions(&self, user_id: &str) -> Result<u64, DomainError>;
async fn revoke_all_user_sessions(&self, user_id: Uuid) -> Result<u64, DomainError>;
}
// ============================================================================
@@ -224,10 +225,10 @@ pub trait DeviceCodeStoragePort: Send + Sync + 'static {
async fn delete_expired(&self) -> Result<u64, DomainError>;
/// List authorized device codes for a user (for UI management)
async fn list_by_user(&self, user_id: &str) -> Result<Vec<DeviceCode>, DomainError>;
async fn list_by_user(&self, user_id: Uuid) -> Result<Vec<DeviceCode>, DomainError>;
/// Delete a specific device code by ID (revocation)
async fn delete_by_id(&self, id: &str) -> Result<(), DomainError>;
async fn delete_by_id(&self, id: Uuid) -> Result<(), DomainError>;
}
// ============================================================================
@@ -240,31 +241,31 @@ pub trait AppPasswordStoragePort: Send + Sync + 'static {
async fn create(&self, app_password: AppPassword) -> Result<AppPassword, DomainError>;
/// Get all active (non-expired) app passwords for a user.
async fn list_by_user(&self, user_id: &str) -> Result<Vec<AppPassword>, DomainError>;
async fn list_by_user(&self, user_id: Uuid) -> Result<Vec<AppPassword>, DomainError>;
/// Get a specific app password by ID.
async fn get_by_id(&self, id: &str) -> Result<AppPassword, DomainError>;
async fn get_by_id(&self, id: Uuid) -> Result<AppPassword, DomainError>;
/// Get all active app passwords for a user ID (for Basic auth verification).
/// This includes the password hash for verification.
async fn get_active_by_user_id(&self, user_id: &str) -> Result<Vec<AppPassword>, DomainError>;
async fn get_active_by_user_id(&self, user_id: Uuid) -> Result<Vec<AppPassword>, DomainError>;
/// Update the `last_used_at` timestamp after a successful authentication.
async fn touch_last_used(&self, id: &str) -> Result<(), DomainError>;
async fn touch_last_used(&self, id: Uuid) -> Result<(), DomainError>;
/// Get active app passwords for a user filtered by token prefix (first 8 chars).
/// More efficient than `get_active_by_user_id` when the password prefix is known.
async fn get_active_by_user_prefix(
&self,
user_id: &str,
user_id: Uuid,
prefix: &str,
) -> Result<Vec<AppPassword>, DomainError>;
/// Deactivate (soft-delete) an app password, scoped to the owning user.
async fn revoke(&self, id: &str, user_id: &str) -> Result<(), DomainError>;
async fn revoke(&self, id: Uuid, user_id: Uuid) -> Result<(), DomainError>;
/// Delete an app password owned by a specific user. Returns true if found and deleted.
async fn delete_by_user_and_id(&self, id: &str, user_id: &str) -> Result<bool, DomainError>;
async fn delete_by_user_and_id(&self, id: Uuid, user_id: Uuid) -> Result<bool, DomainError>;
/// Hard-delete expired/revoked app passwords (cleanup).
async fn delete_expired(&self) -> Result<u64, DomainError>;
+25 -24
View File
@@ -4,6 +4,7 @@ use crate::application::dtos::calendar_dto::{
};
use crate::common::errors::DomainError;
use chrono::{DateTime, Utc};
use uuid::Uuid;
/// Port for external calendar storage mechanisms
pub trait CalendarStoragePort: Send + Sync + 'static {
@@ -11,7 +12,7 @@ pub trait CalendarStoragePort: Send + Sync + 'static {
async fn create_calendar(
&self,
calendar: CreateCalendarDto,
owner_id: &str,
owner_id: Uuid,
) -> Result<CalendarDto, DomainError>;
async fn update_calendar(
&self,
@@ -22,11 +23,11 @@ pub trait CalendarStoragePort: Send + Sync + 'static {
async fn get_calendar(&self, calendar_id: &str) -> Result<CalendarDto, DomainError>;
async fn list_calendars_by_owner(
&self,
owner_id: &str,
owner_id: Uuid,
) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_calendars_shared_with_user(
&self,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_public_calendars(
&self,
@@ -36,20 +37,20 @@ pub trait CalendarStoragePort: Send + Sync + 'static {
async fn check_calendar_access(
&self,
calendar_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<bool, DomainError>;
// Calendar sharing
async fn share_calendar(
&self,
calendar_id: &str,
user_id: &str,
user_id: Uuid,
access_level: &str,
) -> Result<(), DomainError>;
async fn remove_calendar_sharing(
&self,
calendar_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError>;
async fn get_calendar_shares(
&self,
@@ -114,22 +115,22 @@ pub trait CalendarUseCase: Send + Sync + 'static {
async fn create_calendar(
&self,
calendar: CreateCalendarDto,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarDto, DomainError>;
async fn update_calendar(
&self,
calendar_id: &str,
update: UpdateCalendarDto,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarDto, DomainError>;
async fn delete_calendar(&self, calendar_id: &str, user_id: &str) -> Result<(), DomainError>;
async fn delete_calendar(&self, calendar_id: &str, user_id: Uuid) -> Result<(), DomainError>;
async fn get_calendar(
&self,
calendar_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarDto, DomainError>;
async fn list_my_calendars(&self, user_id: &str) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_shared_calendars(&self, user_id: &str) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_my_calendars(&self, user_id: Uuid) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_shared_calendars(&self, user_id: Uuid) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_public_calendars(
&self,
limit: Option<i64>,
@@ -140,57 +141,57 @@ pub trait CalendarUseCase: Send + Sync + 'static {
async fn share_calendar(
&self,
calendar_id: &str,
target_user_id: &str,
target_user_id: Uuid,
access_level: &str,
caller_user_id: &str,
caller_user_id: Uuid,
) -> Result<(), DomainError>;
async fn remove_calendar_sharing(
&self,
calendar_id: &str,
target_user_id: &str,
caller_user_id: &str,
target_user_id: Uuid,
caller_user_id: Uuid,
) -> Result<(), DomainError>;
async fn get_calendar_shares(
&self,
calendar_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<(String, String)>, DomainError>;
// Event operations
async fn create_event(
&self,
event: CreateEventDto,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarEventDto, DomainError>;
async fn create_event_from_ical(
&self,
event: CreateEventICalDto,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarEventDto, DomainError>;
async fn update_event(
&self,
event_id: &str,
update: UpdateEventDto,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarEventDto, DomainError>;
async fn delete_event(&self, event_id: &str, user_id: &str) -> Result<(), DomainError>;
async fn delete_event(&self, event_id: &str, user_id: Uuid) -> Result<(), DomainError>;
async fn get_event(
&self,
event_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarEventDto, DomainError>;
async fn list_events(
&self,
calendar_id: &str,
limit: Option<i64>,
offset: Option<i64>,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<CalendarEventDto>, DomainError>;
async fn get_events_in_range(
&self,
calendar_id: &str,
start: DateTime<Utc>,
end: DateTime<Utc>,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<CalendarEventDto>, DomainError>;
}
+20 -19
View File
@@ -7,6 +7,7 @@ use crate::application::dtos::contact_dto::{
GroupMembershipDto, UpdateContactDto, UpdateContactGroupDto,
};
use crate::common::errors::DomainError;
use uuid::Uuid;
pub type CardDavRepositoryError = DomainError;
@@ -24,16 +25,16 @@ pub trait AddressBookUseCase: Send + Sync + 'static {
async fn delete_address_book(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError>;
async fn get_address_book(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<AddressBookDto, DomainError>;
async fn list_user_address_books(
&self,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<AddressBookDto>, DomainError>;
async fn list_public_address_books(&self) -> Result<Vec<AddressBookDto>, DomainError>;
@@ -41,17 +42,17 @@ pub trait AddressBookUseCase: Send + Sync + 'static {
async fn share_address_book(
&self,
dto: ShareAddressBookDto,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError>;
async fn unshare_address_book(
&self,
dto: UnshareAddressBookDto,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError>;
async fn get_address_book_shares(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<(String, bool)>, DomainError>;
}
@@ -67,19 +68,19 @@ pub trait ContactUseCase: Send + Sync + 'static {
contact_id: &str,
update: UpdateContactDto,
) -> Result<ContactDto, DomainError>;
async fn delete_contact(&self, contact_id: &str, user_id: &str) -> Result<(), DomainError>;
async fn get_contact(&self, contact_id: &str, user_id: &str)
async fn delete_contact(&self, contact_id: &str, user_id: Uuid) -> Result<(), DomainError>;
async fn get_contact(&self, contact_id: &str, user_id: Uuid)
-> Result<ContactDto, DomainError>;
async fn list_contacts(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactDto>, DomainError>;
async fn search_contacts(
&self,
address_book_id: &str,
query: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactDto>, DomainError>;
// Contact Group operations
@@ -92,49 +93,49 @@ pub trait ContactUseCase: Send + Sync + 'static {
group_id: &str,
update: UpdateContactGroupDto,
) -> Result<ContactGroupDto, DomainError>;
async fn delete_group(&self, group_id: &str, user_id: &str) -> Result<(), DomainError>;
async fn delete_group(&self, group_id: &str, user_id: Uuid) -> Result<(), DomainError>;
async fn get_group(
&self,
group_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<ContactGroupDto, DomainError>;
async fn list_groups(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactGroupDto>, DomainError>;
// Group membership
async fn add_contact_to_group(
&self,
dto: GroupMembershipDto,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError>;
async fn remove_contact_from_group(
&self,
dto: GroupMembershipDto,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError>;
async fn list_contacts_in_group(
&self,
group_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactDto>, DomainError>;
async fn list_groups_for_contact(
&self,
contact_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactGroupDto>, DomainError>;
// vCard operations
async fn get_contact_vcard(
&self,
contact_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<String, DomainError>;
async fn get_contacts_as_vcards(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<(String, String)>, DomainError>;
}
@@ -8,6 +8,7 @@ use crate::common::errors::DomainError;
use bytes::Bytes;
use serde::Serialize;
use std::path::PathBuf;
use uuid::Uuid;
/// Default chunk size (5 MB) — optimised for parallel transfers.
pub const DEFAULT_CHUNK_SIZE: usize = 5 * 1024 * 1024;
@@ -59,7 +60,7 @@ pub trait ChunkedUploadPort: Send + Sync + 'static {
/// total number of chunks, and expiration timestamp.
async fn create_session(
&self,
user_id: &str,
user_id: Uuid,
filename: String,
folder_id: Option<String>,
content_type: String,
@@ -73,7 +74,7 @@ pub trait ChunkedUploadPort: Send + Sync + 'static {
async fn upload_chunk(
&self,
upload_id: &str,
user_id: &str,
user_id: Uuid,
chunk_index: usize,
data: Bytes,
checksum: Option<String>,
@@ -83,7 +84,7 @@ pub trait ChunkedUploadPort: Send + Sync + 'static {
async fn get_status(
&self,
upload_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<UploadStatusResponseDto, DomainError>;
/// Assemble all chunks into the final file.
@@ -94,14 +95,14 @@ pub trait ChunkedUploadPort: Send + Sync + 'static {
async fn complete_upload(
&self,
upload_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<(PathBuf, String, Option<String>, String, u64, String), DomainError>;
/// Finalize upload: clean up the session and temporary files.
async fn finalize_upload(&self, upload_id: &str, user_id: &str) -> Result<(), DomainError>;
async fn finalize_upload(&self, upload_id: &str, user_id: Uuid) -> Result<(), DomainError>;
/// Cancel an upload and clean up all temporary data.
async fn cancel_upload(&self, upload_id: &str, user_id: &str) -> Result<(), DomainError>;
async fn cancel_upload(&self, upload_id: &str, user_id: Uuid) -> Result<(), DomainError>;
/// Check if a file size qualifies for chunked upload.
fn should_use_chunked(&self, size: u64) -> bool;
+14 -12
View File
@@ -1,32 +1,34 @@
use std::collections::HashSet;
use uuid::Uuid;
use crate::application::dtos::favorites_dto::{BatchFavoritesResult, FavoriteItemDto};
use crate::common::errors::Result;
/// Defines operations for managing user favorites
pub trait FavoritesUseCase: Send + Sync {
/// Get all favorites for a user
async fn get_favorites(&self, user_id: &str) -> Result<Vec<FavoriteItemDto>>;
async fn get_favorites(&self, user_id: Uuid) -> Result<Vec<FavoriteItemDto>>;
/// Add an item to user's favorites
async fn add_to_favorites(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>;
async fn add_to_favorites(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<()>;
/// Remove an item from user's favorites
async fn remove_from_favorites(
&self,
user_id: &str,
user_id: Uuid,
item_id: &str,
item_type: &str,
) -> Result<bool>;
/// Check if an item is in user's favorites
async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
async fn is_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<bool>;
/// Add multiple items to favorites in a single transaction.
/// Returns enriched favourites list so the client can replace its cache.
async fn batch_add_to_favorites(
&self,
user_id: &str,
user_id: Uuid,
items: &[(String, String)],
) -> Result<BatchFavoritesResult>;
@@ -34,7 +36,7 @@ pub trait FavoritesUseCase: Send + Sync {
/// Returns the set of item_ids that are favorites.
async fn batch_check_favorites(
&self,
user_id: &str,
user_id: Uuid,
item_ids: &[(&str, &str)], // (item_id, item_type) pairs
) -> Result<HashSet<String>>;
}
@@ -50,26 +52,26 @@ pub trait FavoritesUseCase: Send + Sync {
/// lives in `infrastructure::repositories::pg`.
pub trait FavoritesRepositoryPort: Send + Sync + 'static {
/// Gets all favorites for a user.
async fn get_favorites(&self, user_id: &str) -> Result<Vec<FavoriteItemDto>>;
async fn get_favorites(&self, user_id: Uuid) -> Result<Vec<FavoriteItemDto>>;
/// Adds an item to favorites. Returns `Ok(())` if it already existed (idempotent).
async fn add_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>;
async fn add_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<()>;
/// Removes an item from favorites. Returns `true` if it existed.
async fn remove_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
async fn remove_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<bool>;
/// Checks if an item is in favorites.
async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
async fn is_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<bool>;
/// Insert multiple items in a single transaction.
/// Returns the number of rows actually inserted (ignoring duplicates).
async fn add_favorites_batch(&self, user_id: &str, items: &[(String, String)]) -> Result<u64>;
async fn add_favorites_batch(&self, user_id: Uuid, items: &[(String, String)]) -> Result<u64>;
/// Check which of the given item IDs are favorites for this user.
/// Returns the set of item_ids that are favorites.
async fn batch_check_favorites(
&self,
user_id: &str,
user_id: Uuid,
item_ids: &[(&str, &str)], // (item_id, item_type) pairs
) -> Result<HashSet<String>>;
}
+14 -12
View File
@@ -3,6 +3,7 @@ use futures::Stream;
use std::path::Path;
use std::pin::Pin;
use std::sync::Arc;
use uuid::Uuid;
use crate::application::dtos::file_dto::FileDto;
use crate::application::ports::storage_ports::CopyFolderTreeResult;
@@ -116,7 +117,7 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
///
/// Returns `NotFound` if the file does not exist **or** belongs to
/// another user. All user-facing handlers should use this method.
async fn get_file_owned(&self, id: &str, caller_id: &str) -> Result<FileDto, DomainError>;
async fn get_file_owned(&self, id: &str, caller_id: Uuid) -> Result<FileDto, DomainError>;
/// Gets a file by its path (for WebDAV)
async fn get_file_by_path(&self, path: &str) -> Result<FileDto, DomainError>;
@@ -131,7 +132,7 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
async fn list_files_owned(
&self,
folder_id: Option<&str>,
owner_id: &str,
owner_id: Uuid,
) -> Result<Vec<FileDto>, DomainError>;
/// Gets file content as a stream (for large files)
@@ -144,7 +145,7 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
async fn get_file_stream_owned(
&self,
id: &str,
caller_id: &str,
caller_id: Uuid,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
/// Optimized multi-tier download.
@@ -166,7 +167,7 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
async fn get_file_optimized_owned(
&self,
id: &str,
caller_id: &str,
caller_id: Uuid,
accept_webp: bool,
prefer_original: bool,
) -> Result<(FileDto, OptimizedFileContent), DomainError>;
@@ -198,7 +199,7 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
async fn get_file_range_stream_owned(
&self,
id: &str,
caller_id: &str,
caller_id: Uuid,
start: u64,
end: Option<u64>,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
@@ -238,14 +239,15 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
async fn list_files_batch_for_owner(
&self,
folder_id: Option<&str>,
owner_id: &str,
owner_id: Uuid,
offset: i64,
limit: i64,
) -> Result<Vec<FileDto>, DomainError> {
let all = self.list_files_batch(folder_id, offset, limit).await?;
let owner_str = owner_id.to_string();
Ok(all
.into_iter()
.filter(|f| f.owner_id.as_deref().is_some_and(|o| o == owner_id))
.filter(|f| f.owner_id.as_deref().is_some_and(|o| o == owner_str))
.collect())
}
}
@@ -267,7 +269,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
async fn move_file_owned(
&self,
file_id: &str,
caller_id: &str,
caller_id: Uuid,
folder_id: Option<String>,
) -> Result<FileDto, DomainError>;
@@ -282,7 +284,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
async fn copy_file_owned(
&self,
file_id: &str,
caller_id: &str,
caller_id: Uuid,
target_folder_id: Option<String>,
) -> Result<FileDto, DomainError>;
@@ -293,7 +295,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
async fn rename_file_owned(
&self,
file_id: &str,
caller_id: &str,
caller_id: Uuid,
new_name: &str,
) -> Result<FileDto, DomainError>;
@@ -301,7 +303,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
/// Deletes a file, enforcing that `caller_id` is the owner.
async fn delete_file_owned(&self, id: &str, caller_id: &str) -> Result<(), DomainError>;
async fn delete_file_owned(&self, id: &str, caller_id: Uuid) -> Result<(), DomainError>;
/// Smart delete: trash-first with dedup reference cleanup.
///
@@ -310,7 +312,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
/// 3. Decrements the dedup reference count for the content hash.
///
/// Returns `Ok(true)` when trashed, `Ok(false)` when permanently deleted.
async fn delete_with_cleanup(&self, id: &str, user_id: &str) -> Result<bool, DomainError>;
async fn delete_with_cleanup(&self, id: &str, user_id: Uuid) -> Result<bool, DomainError>;
/// Copies an entire folder subtree atomically (WebDAV COPY Depth: infinity).
///
+10 -8
View File
@@ -1,5 +1,7 @@
use std::sync::Arc;
use uuid::Uuid;
use crate::application::dtos::folder_dto::{
CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto,
};
@@ -20,7 +22,7 @@ pub trait FolderUseCase: Send + Sync + 'static {
///
/// Returns `NotFound` if the folder does not exist **or** belongs to
/// another user. All user-facing handlers should use this method.
async fn get_folder_owned(&self, id: &str, caller_id: &str) -> Result<FolderDto, DomainError>;
async fn get_folder_owned(&self, id: &str, caller_id: Uuid) -> Result<FolderDto, DomainError>;
/// Gets a folder by its path
async fn get_folder_by_path(&self, path: &str) -> Result<FolderDto, DomainError>;
@@ -33,7 +35,7 @@ pub trait FolderUseCase: Send + Sync + 'static {
async fn list_folders_for_owner(
&self,
parent_id: Option<&str>,
owner_id: &str,
owner_id: Uuid,
) -> Result<Vec<FolderDto>, DomainError>;
/// Lists folders with pagination
@@ -47,7 +49,7 @@ pub trait FolderUseCase: Send + Sync + 'static {
async fn list_folders_for_owner_paginated(
&self,
parent_id: Option<&str>,
owner_id: &str,
owner_id: Uuid,
pagination: &crate::application::dtos::pagination::PaginationRequestDto,
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>;
@@ -56,7 +58,7 @@ pub trait FolderUseCase: Send + Sync + 'static {
&self,
id: &str,
dto: RenameFolderDto,
caller_id: &str,
caller_id: Uuid,
) -> Result<FolderDto, DomainError>;
/// Moves a folder to another parent (ownership verified against caller_id)
@@ -64,16 +66,16 @@ pub trait FolderUseCase: Send + Sync + 'static {
&self,
id: &str,
dto: MoveFolderDto,
caller_id: &str,
caller_id: Uuid,
) -> Result<FolderDto, DomainError>;
/// Deletes a folder (ownership verified against caller_id)
async fn delete_folder(&self, id: &str, caller_id: &str) -> Result<(), DomainError>;
async fn delete_folder(&self, id: &str, caller_id: Uuid) -> Result<(), DomainError>;
/// Creates a root-level home folder for a user during registration.
async fn create_home_folder(
&self,
user_id: &str,
user_id: Uuid,
name: String,
) -> Result<FolderDto, DomainError>;
@@ -103,7 +105,7 @@ pub trait SearchUseCase: Send + Sync + 'static {
async fn search(
&self,
criteria: SearchCriteriaDto,
user_id: &str,
user_id: Uuid,
) -> Result<Arc<SearchResultsDto>, DomainError>;
/// Returns quick suggestions for autocomplete (lightweight, fast).
+11 -9
View File
@@ -1,3 +1,5 @@
use uuid::Uuid;
use crate::application::dtos::recent_dto::RecentItemDto;
use crate::common::errors::Result;
@@ -6,24 +8,24 @@ pub trait RecentItemsUseCase: Send + Sync {
/// Get all recent items for a user
async fn get_recent_items(
&self,
user_id: &str,
user_id: Uuid,
limit: Option<i32>,
) -> Result<Vec<RecentItemDto>>;
/// Record access to an item
async fn record_item_access(&self, user_id: &str, item_id: &str, item_type: &str)
async fn record_item_access(&self, user_id: Uuid, item_id: &str, item_type: &str)
-> Result<()>;
/// Remove an item from recents
async fn remove_from_recent(
&self,
user_id: &str,
user_id: Uuid,
item_id: &str,
item_type: &str,
) -> Result<bool>;
/// Clear the entire recent items list
async fn clear_recent_items(&self, user_id: &str) -> Result<()>;
async fn clear_recent_items(&self, user_id: Uuid) -> Result<()>;
}
// ─────────────────────────────────────────────────────
@@ -36,17 +38,17 @@ pub trait RecentItemsUseCase: Send + Sync {
/// `RecentService` does not depend directly on `PgPool`.
pub trait RecentItemsRepositoryPort: Send + Sync + 'static {
/// Gets the latest recent items for a user (ordered by date desc).
async fn get_recent_items(&self, user_id: &str, limit: i32) -> Result<Vec<RecentItemDto>>;
async fn get_recent_items(&self, user_id: Uuid, limit: i32) -> Result<Vec<RecentItemDto>>;
/// Records/updates access to an item (upsert by user+item+type).
async fn upsert_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>;
async fn upsert_access(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<()>;
/// Removes an item from recents. Returns `true` if it existed.
async fn remove_item(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
async fn remove_item(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<bool>;
/// Removes all recent items for a user.
async fn clear_all(&self, user_id: &str) -> Result<()>;
async fn clear_all(&self, user_id: Uuid) -> Result<()>;
/// Removes items exceeding `max_items` (the oldest ones).
async fn prune(&self, user_id: &str, max_items: i32) -> Result<()>;
async fn prune(&self, user_id: Uuid, max_items: i32) -> Result<()>;
}
+14 -12
View File
@@ -1,3 +1,5 @@
use uuid::Uuid;
use crate::{
application::dtos::{
pagination::PaginatedResponseDto,
@@ -11,12 +13,12 @@ pub trait ShareUseCase: Send + Sync + 'static {
/// Create a new shared link for a file or folder
async fn create_shared_link(
&self,
user_id: &str,
user_id: Uuid,
dto: CreateShareDto,
) -> Result<ShareDto, DomainError>;
/// Get a shared link by its ID (ownership-verified)
async fn get_shared_link(&self, id: &str, requester_id: &str) -> Result<ShareDto, DomainError>;
async fn get_shared_link(&self, id: Uuid, requester_id: Uuid) -> Result<ShareDto, DomainError>;
/// Get a shared link by its token (for access by non-users)
async fn get_shared_link_by_token(&self, token: &str) -> Result<ShareDto, DomainError>;
@@ -26,24 +28,24 @@ pub trait ShareUseCase: Send + Sync + 'static {
&self,
item_id: &str,
item_type: &ShareItemType,
requester_id: &str,
requester_id: Uuid,
) -> Result<Vec<ShareDto>, DomainError>;
/// Update a shared link (ownership-verified)
async fn update_shared_link(
&self,
id: &str,
requester_id: &str,
id: Uuid,
requester_id: Uuid,
dto: UpdateShareDto,
) -> Result<ShareDto, DomainError>;
/// Delete a shared link (ownership-verified)
async fn delete_shared_link(&self, id: &str, requester_id: &str) -> Result<(), DomainError>;
async fn delete_shared_link(&self, id: Uuid, requester_id: Uuid) -> Result<(), DomainError>;
/// Get all shared links created by a specific user
async fn get_user_shared_links(
&self,
user_id: &str,
user_id: Uuid,
page: usize,
per_page: usize,
) -> Result<PaginatedResponseDto<ShareDto>, DomainError>;
@@ -77,19 +79,19 @@ pub trait ShareStoragePort: Send + Sync + 'static {
/// (prevents share-ID enumeration).
async fn find_share_by_id_for_user(
&self,
id: &str,
user_id: &str,
id: Uuid,
user_id: Uuid,
) -> Result<crate::domain::entities::share::Share, DomainError>;
/// Delete a share only if it belongs to the given user.
async fn delete_share_for_user(&self, id: &str, user_id: &str) -> Result<(), DomainError>;
async fn delete_share_for_user(&self, id: Uuid, user_id: Uuid) -> Result<(), DomainError>;
/// Find shares for a specific item that belong to the given user.
async fn find_shares_by_item_for_user(
&self,
item_id: &str,
item_type: &ShareItemType,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<crate::domain::entities::share::Share>, DomainError>;
async fn update_share(
@@ -99,7 +101,7 @@ pub trait ShareStoragePort: Send + Sync + 'static {
async fn find_shares_by_user(
&self,
user_id: &str,
user_id: Uuid,
offset: usize,
limit: usize,
) -> Result<(Vec<crate::domain::entities::share::Share>, usize), DomainError>;
+15 -12
View File
@@ -3,6 +3,7 @@ use futures::Stream;
use serde_json::Value;
use std::path::PathBuf;
use std::pin::Pin;
use uuid::Uuid;
use crate::application::dtos::search_dto::SearchCriteriaDto;
use crate::common::errors::DomainError;
@@ -33,13 +34,13 @@ pub trait FileReadPort: Send + Sync + 'static {
/// Returns `NotFound` if the file does not exist **or** belongs to a
/// different user. This is the primary IDOR-safe accessor — handlers
/// serving end-user requests should always prefer this over `get_file`.
async fn get_file_for_owner(&self, id: &str, owner_id: &str) -> Result<File, DomainError>;
async fn get_file_for_owner(&self, id: &str, owner_id: Uuid) -> Result<File, DomainError>;
/// Verifies that the file identified by `id` belongs to `owner_id`.
///
/// Returns `Ok(())` on success or `NotFound` when the file does not
/// exist or belongs to another user.
async fn verify_file_owner(&self, id: &str, owner_id: &str) -> Result<(), DomainError> {
async fn verify_file_owner(&self, id: &str, owner_id: Uuid) -> Result<(), DomainError> {
self.get_file_for_owner(id, owner_id).await.map(|_| ())
}
@@ -53,12 +54,13 @@ pub trait FileReadPort: Send + Sync + 'static {
async fn list_files_for_owner(
&self,
folder_id: Option<&str>,
owner_id: &str,
owner_id: Uuid,
) -> Result<Vec<File>, DomainError> {
let all = self.list_files(folder_id).await?;
let owner_str = owner_id.to_string();
Ok(all
.into_iter()
.filter(|f| f.owner_id().is_some_and(|o| o == owner_id))
.filter(|f| f.owner_id().is_some_and(|o| o == owner_str))
.collect())
}
@@ -134,15 +136,16 @@ pub trait FileReadPort: Send + Sync + 'static {
async fn list_files_batch_for_owner(
&self,
folder_id: Option<&str>,
owner_id: &str,
owner_id: Uuid,
offset: i64,
limit: i64,
) -> Result<Vec<File>, DomainError> {
// Default: filter in-memory (repos should override with SQL)
let all = self.list_files_batch(folder_id, offset, limit).await?;
let owner_str = owner_id.to_string();
Ok(all
.into_iter()
.filter(|f| f.owner_id().is_some_and(|o| o == owner_id))
.filter(|f| f.owner_id().is_some_and(|o| o == owner_str))
.collect())
}
@@ -175,7 +178,7 @@ pub trait FileReadPort: Send + Sync + 'static {
&self,
folder_id: Option<&str>,
criteria: &SearchCriteriaDto,
user_id: &str,
user_id: Uuid,
) -> Result<(Vec<File>, usize), DomainError>;
/// Search files recursively in a folder subtree using ltree.
@@ -190,7 +193,7 @@ pub trait FileReadPort: Send + Sync + 'static {
&self,
root_folder_id: Option<&str>,
criteria: &SearchCriteriaDto,
user_id: &str,
user_id: Uuid,
) -> Result<(Vec<File>, usize), DomainError> {
// Default: delegate to paginated search (non-recursive fallback)
self.search_files_paginated(root_folder_id, criteria, user_id)
@@ -204,7 +207,7 @@ pub trait FileReadPort: Send + Sync + 'static {
&self,
folder_id: Option<&str>,
criteria: &SearchCriteriaDto,
user_id: &str,
user_id: Uuid,
) -> Result<usize, DomainError>;
/// Return up to `limit` files whose name contains `query` (case-insensitive).
@@ -359,7 +362,7 @@ pub trait FileWritePort: Send + Sync + 'static {
/// Secondary port for storage usage management
pub trait StorageUsagePort: Send + Sync + 'static {
/// Updates storage usage statistics for a user
async fn update_user_storage_usage(&self, user_id: &str) -> Result<i64, DomainError>;
async fn update_user_storage_usage(&self, user_id: Uuid) -> Result<i64, DomainError>;
/// Updates storage usage statistics for a user, looked up by username
async fn update_user_storage_usage_by_username(
@@ -375,12 +378,12 @@ pub trait StorageUsagePort: Send + Sync + 'static {
/// descriptive message otherwise.
async fn check_storage_quota(
&self,
user_id: &str,
user_id: Uuid,
additional_bytes: u64,
) -> Result<(), DomainError>;
/// Returns (used_bytes, quota_bytes) for a user.
async fn get_user_storage_info(&self, user_id: &str) -> Result<(i64, i64), DomainError>;
async fn get_user_storage_info(&self, user_id: Uuid) -> Result<(i64, i64), DomainError>;
}
/// Generic storage service interface for calendar and contact services
+7 -5
View File
@@ -1,20 +1,22 @@
use uuid::Uuid;
use crate::application::dtos::trash_dto::TrashedItemDto;
use crate::common::errors::Result;
/// Port for trash-related use cases
pub trait TrashUseCase: Send + Sync {
/// List items in the user's trash
async fn get_trash_items(&self, user_id: &str) -> Result<Vec<TrashedItemDto>>;
async fn get_trash_items(&self, user_id: Uuid) -> Result<Vec<TrashedItemDto>>;
/// Move a file or folder to trash
async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: &str) -> Result<()>;
async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: Uuid) -> Result<()>;
/// Restore an item from trash to its original location
async fn restore_item(&self, trash_id: &str, user_id: &str) -> Result<()>;
async fn restore_item(&self, trash_id: &str, user_id: Uuid) -> Result<()>;
/// Permanently delete an item from trash
async fn delete_permanently(&self, trash_id: &str, user_id: &str) -> Result<()>;
async fn delete_permanently(&self, trash_id: &str, user_id: Uuid) -> Result<()>;
/// Empty the trash for a specific user
async fn empty_trash(&self, user_id: &str) -> Result<()>;
async fn empty_trash(&self, user_id: Uuid) -> Result<()>;
}