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

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

85 files changed, 1090 insertions(+), 1739 deletions(-)
This commit is contained in:
Diocrafts
2026-03-07 14:59:32 +01:00
parent 9f08460027
commit 06ed0455ce
85 changed files with 1090 additions and 1739 deletions
+2 -1
View File
@@ -1,6 +1,7 @@
use crate::domain::entities::user::User;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserDto {
@@ -80,7 +81,7 @@ pub struct RefreshTokenDto {
/// Authenticated current user data (for use in application services)
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CurrentUser {
pub id: String,
pub id: Uuid,
pub username: String,
pub email: String,
pub role: String,
+19 -18
View File
@@ -3,6 +3,7 @@ use crate::domain::entities::app_password::AppPassword;
use crate::domain::entities::device_code::DeviceCode;
use crate::domain::entities::session::Session;
use crate::domain::entities::user::User;
use uuid::Uuid;
// ============================================================================
// Cryptography Ports - Extracted from Domain to maintain Clean Architecture
@@ -72,7 +73,7 @@ pub trait UserStoragePort: Send + Sync + 'static {
async fn create_user(&self, user: User) -> Result<User, DomainError>;
/// Gets a user by ID
async fn get_user_by_id(&self, id: &str) -> Result<User, DomainError>;
async fn get_user_by_id(&self, id: Uuid) -> Result<User, DomainError>;
/// Gets a user by username
async fn get_user_by_username(&self, username: &str) -> Result<User, DomainError>;
@@ -86,7 +87,7 @@ pub trait UserStoragePort: Send + Sync + 'static {
/// Updates only the storage usage of a user
async fn update_storage_usage(
&self,
user_id: &str,
user_id: Uuid,
usage_bytes: i64,
) -> Result<(), DomainError>;
@@ -100,10 +101,10 @@ pub trait UserStoragePort: Send + Sync + 'static {
async fn list_users_by_role(&self, role: &str) -> Result<Vec<User>, DomainError>;
/// Deletes a user by their ID
async fn delete_user(&self, user_id: &str) -> Result<(), DomainError>;
async fn delete_user(&self, user_id: Uuid) -> Result<(), DomainError>;
/// Changes a user's password
async fn change_password(&self, user_id: &str, password_hash: &str) -> Result<(), DomainError>;
async fn change_password(&self, user_id: Uuid, password_hash: &str) -> Result<(), DomainError>;
/// Finds a user by OIDC provider + subject pair
async fn get_user_by_oidc_subject(
@@ -113,15 +114,15 @@ pub trait UserStoragePort: Send + Sync + 'static {
) -> Result<User, DomainError>;
/// Activates or deactivates a user
async fn set_user_active_status(&self, user_id: &str, active: bool) -> Result<(), DomainError>;
async fn set_user_active_status(&self, user_id: Uuid, active: bool) -> Result<(), DomainError>;
/// Changes a user's role
async fn change_role(&self, user_id: &str, role: &str) -> Result<(), DomainError>;
async fn change_role(&self, user_id: Uuid, role: &str) -> Result<(), DomainError>;
/// Updates a user's storage quota
async fn update_storage_quota(
&self,
user_id: &str,
user_id: Uuid,
quota_bytes: i64,
) -> Result<(), DomainError>;
@@ -197,10 +198,10 @@ pub trait SessionStoragePort: Send + Sync + 'static {
) -> Result<Session, DomainError>;
/// Revokes a specific session
async fn revoke_session(&self, session_id: &str) -> Result<(), DomainError>;
async fn revoke_session(&self, session_id: Uuid) -> Result<(), DomainError>;
/// Revokes all sessions of a user
async fn revoke_all_user_sessions(&self, user_id: &str) -> Result<u64, DomainError>;
async fn revoke_all_user_sessions(&self, user_id: Uuid) -> Result<u64, DomainError>;
}
// ============================================================================
@@ -224,10 +225,10 @@ pub trait DeviceCodeStoragePort: Send + Sync + 'static {
async fn delete_expired(&self) -> Result<u64, DomainError>;
/// List authorized device codes for a user (for UI management)
async fn list_by_user(&self, user_id: &str) -> Result<Vec<DeviceCode>, DomainError>;
async fn list_by_user(&self, user_id: Uuid) -> Result<Vec<DeviceCode>, DomainError>;
/// Delete a specific device code by ID (revocation)
async fn delete_by_id(&self, id: &str) -> Result<(), DomainError>;
async fn delete_by_id(&self, id: Uuid) -> Result<(), DomainError>;
}
// ============================================================================
@@ -240,31 +241,31 @@ pub trait AppPasswordStoragePort: Send + Sync + 'static {
async fn create(&self, app_password: AppPassword) -> Result<AppPassword, DomainError>;
/// Get all active (non-expired) app passwords for a user.
async fn list_by_user(&self, user_id: &str) -> Result<Vec<AppPassword>, DomainError>;
async fn list_by_user(&self, user_id: Uuid) -> Result<Vec<AppPassword>, DomainError>;
/// Get a specific app password by ID.
async fn get_by_id(&self, id: &str) -> Result<AppPassword, DomainError>;
async fn get_by_id(&self, id: Uuid) -> Result<AppPassword, DomainError>;
/// Get all active app passwords for a user ID (for Basic auth verification).
/// This includes the password hash for verification.
async fn get_active_by_user_id(&self, user_id: &str) -> Result<Vec<AppPassword>, DomainError>;
async fn get_active_by_user_id(&self, user_id: Uuid) -> Result<Vec<AppPassword>, DomainError>;
/// Update the `last_used_at` timestamp after a successful authentication.
async fn touch_last_used(&self, id: &str) -> Result<(), DomainError>;
async fn touch_last_used(&self, id: Uuid) -> Result<(), DomainError>;
/// Get active app passwords for a user filtered by token prefix (first 8 chars).
/// More efficient than `get_active_by_user_id` when the password prefix is known.
async fn get_active_by_user_prefix(
&self,
user_id: &str,
user_id: Uuid,
prefix: &str,
) -> Result<Vec<AppPassword>, DomainError>;
/// Deactivate (soft-delete) an app password, scoped to the owning user.
async fn revoke(&self, id: &str, user_id: &str) -> Result<(), DomainError>;
async fn revoke(&self, id: Uuid, user_id: Uuid) -> Result<(), DomainError>;
/// Delete an app password owned by a specific user. Returns true if found and deleted.
async fn delete_by_user_and_id(&self, id: &str, user_id: &str) -> Result<bool, DomainError>;
async fn delete_by_user_and_id(&self, id: Uuid, user_id: Uuid) -> Result<bool, DomainError>;
/// Hard-delete expired/revoked app passwords (cleanup).
async fn delete_expired(&self) -> Result<u64, DomainError>;
+25 -24
View File
@@ -4,6 +4,7 @@ use crate::application::dtos::calendar_dto::{
};
use crate::common::errors::DomainError;
use chrono::{DateTime, Utc};
use uuid::Uuid;
/// Port for external calendar storage mechanisms
pub trait CalendarStoragePort: Send + Sync + 'static {
@@ -11,7 +12,7 @@ pub trait CalendarStoragePort: Send + Sync + 'static {
async fn create_calendar(
&self,
calendar: CreateCalendarDto,
owner_id: &str,
owner_id: Uuid,
) -> Result<CalendarDto, DomainError>;
async fn update_calendar(
&self,
@@ -22,11 +23,11 @@ pub trait CalendarStoragePort: Send + Sync + 'static {
async fn get_calendar(&self, calendar_id: &str) -> Result<CalendarDto, DomainError>;
async fn list_calendars_by_owner(
&self,
owner_id: &str,
owner_id: Uuid,
) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_calendars_shared_with_user(
&self,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_public_calendars(
&self,
@@ -36,20 +37,20 @@ pub trait CalendarStoragePort: Send + Sync + 'static {
async fn check_calendar_access(
&self,
calendar_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<bool, DomainError>;
// Calendar sharing
async fn share_calendar(
&self,
calendar_id: &str,
user_id: &str,
user_id: Uuid,
access_level: &str,
) -> Result<(), DomainError>;
async fn remove_calendar_sharing(
&self,
calendar_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError>;
async fn get_calendar_shares(
&self,
@@ -114,22 +115,22 @@ pub trait CalendarUseCase: Send + Sync + 'static {
async fn create_calendar(
&self,
calendar: CreateCalendarDto,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarDto, DomainError>;
async fn update_calendar(
&self,
calendar_id: &str,
update: UpdateCalendarDto,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarDto, DomainError>;
async fn delete_calendar(&self, calendar_id: &str, user_id: &str) -> Result<(), DomainError>;
async fn delete_calendar(&self, calendar_id: &str, user_id: Uuid) -> Result<(), DomainError>;
async fn get_calendar(
&self,
calendar_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarDto, DomainError>;
async fn list_my_calendars(&self, user_id: &str) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_shared_calendars(&self, user_id: &str) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_my_calendars(&self, user_id: Uuid) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_shared_calendars(&self, user_id: Uuid) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_public_calendars(
&self,
limit: Option<i64>,
@@ -140,57 +141,57 @@ pub trait CalendarUseCase: Send + Sync + 'static {
async fn share_calendar(
&self,
calendar_id: &str,
target_user_id: &str,
target_user_id: Uuid,
access_level: &str,
caller_user_id: &str,
caller_user_id: Uuid,
) -> Result<(), DomainError>;
async fn remove_calendar_sharing(
&self,
calendar_id: &str,
target_user_id: &str,
caller_user_id: &str,
target_user_id: Uuid,
caller_user_id: Uuid,
) -> Result<(), DomainError>;
async fn get_calendar_shares(
&self,
calendar_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<(String, String)>, DomainError>;
// Event operations
async fn create_event(
&self,
event: CreateEventDto,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarEventDto, DomainError>;
async fn create_event_from_ical(
&self,
event: CreateEventICalDto,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarEventDto, DomainError>;
async fn update_event(
&self,
event_id: &str,
update: UpdateEventDto,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarEventDto, DomainError>;
async fn delete_event(&self, event_id: &str, user_id: &str) -> Result<(), DomainError>;
async fn delete_event(&self, event_id: &str, user_id: Uuid) -> Result<(), DomainError>;
async fn get_event(
&self,
event_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarEventDto, DomainError>;
async fn list_events(
&self,
calendar_id: &str,
limit: Option<i64>,
offset: Option<i64>,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<CalendarEventDto>, DomainError>;
async fn get_events_in_range(
&self,
calendar_id: &str,
start: DateTime<Utc>,
end: DateTime<Utc>,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<CalendarEventDto>, DomainError>;
}
+20 -19
View File
@@ -7,6 +7,7 @@ use crate::application::dtos::contact_dto::{
GroupMembershipDto, UpdateContactDto, UpdateContactGroupDto,
};
use crate::common::errors::DomainError;
use uuid::Uuid;
pub type CardDavRepositoryError = DomainError;
@@ -24,16 +25,16 @@ pub trait AddressBookUseCase: Send + Sync + 'static {
async fn delete_address_book(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError>;
async fn get_address_book(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<AddressBookDto, DomainError>;
async fn list_user_address_books(
&self,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<AddressBookDto>, DomainError>;
async fn list_public_address_books(&self) -> Result<Vec<AddressBookDto>, DomainError>;
@@ -41,17 +42,17 @@ pub trait AddressBookUseCase: Send + Sync + 'static {
async fn share_address_book(
&self,
dto: ShareAddressBookDto,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError>;
async fn unshare_address_book(
&self,
dto: UnshareAddressBookDto,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError>;
async fn get_address_book_shares(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<(String, bool)>, DomainError>;
}
@@ -67,19 +68,19 @@ pub trait ContactUseCase: Send + Sync + 'static {
contact_id: &str,
update: UpdateContactDto,
) -> Result<ContactDto, DomainError>;
async fn delete_contact(&self, contact_id: &str, user_id: &str) -> Result<(), DomainError>;
async fn get_contact(&self, contact_id: &str, user_id: &str)
async fn delete_contact(&self, contact_id: &str, user_id: Uuid) -> Result<(), DomainError>;
async fn get_contact(&self, contact_id: &str, user_id: Uuid)
-> Result<ContactDto, DomainError>;
async fn list_contacts(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactDto>, DomainError>;
async fn search_contacts(
&self,
address_book_id: &str,
query: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactDto>, DomainError>;
// Contact Group operations
@@ -92,49 +93,49 @@ pub trait ContactUseCase: Send + Sync + 'static {
group_id: &str,
update: UpdateContactGroupDto,
) -> Result<ContactGroupDto, DomainError>;
async fn delete_group(&self, group_id: &str, user_id: &str) -> Result<(), DomainError>;
async fn delete_group(&self, group_id: &str, user_id: Uuid) -> Result<(), DomainError>;
async fn get_group(
&self,
group_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<ContactGroupDto, DomainError>;
async fn list_groups(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactGroupDto>, DomainError>;
// Group membership
async fn add_contact_to_group(
&self,
dto: GroupMembershipDto,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError>;
async fn remove_contact_from_group(
&self,
dto: GroupMembershipDto,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError>;
async fn list_contacts_in_group(
&self,
group_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactDto>, DomainError>;
async fn list_groups_for_contact(
&self,
contact_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactGroupDto>, DomainError>;
// vCard operations
async fn get_contact_vcard(
&self,
contact_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<String, DomainError>;
async fn get_contacts_as_vcards(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<(String, String)>, DomainError>;
}
@@ -8,6 +8,7 @@ use crate::common::errors::DomainError;
use bytes::Bytes;
use serde::Serialize;
use std::path::PathBuf;
use uuid::Uuid;
/// Default chunk size (5 MB) — optimised for parallel transfers.
pub const DEFAULT_CHUNK_SIZE: usize = 5 * 1024 * 1024;
@@ -59,7 +60,7 @@ pub trait ChunkedUploadPort: Send + Sync + 'static {
/// total number of chunks, and expiration timestamp.
async fn create_session(
&self,
user_id: &str,
user_id: Uuid,
filename: String,
folder_id: Option<String>,
content_type: String,
@@ -73,7 +74,7 @@ pub trait ChunkedUploadPort: Send + Sync + 'static {
async fn upload_chunk(
&self,
upload_id: &str,
user_id: &str,
user_id: Uuid,
chunk_index: usize,
data: Bytes,
checksum: Option<String>,
@@ -83,7 +84,7 @@ pub trait ChunkedUploadPort: Send + Sync + 'static {
async fn get_status(
&self,
upload_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<UploadStatusResponseDto, DomainError>;
/// Assemble all chunks into the final file.
@@ -94,14 +95,14 @@ pub trait ChunkedUploadPort: Send + Sync + 'static {
async fn complete_upload(
&self,
upload_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<(PathBuf, String, Option<String>, String, u64, String), DomainError>;
/// Finalize upload: clean up the session and temporary files.
async fn finalize_upload(&self, upload_id: &str, user_id: &str) -> Result<(), DomainError>;
async fn finalize_upload(&self, upload_id: &str, user_id: Uuid) -> Result<(), DomainError>;
/// Cancel an upload and clean up all temporary data.
async fn cancel_upload(&self, upload_id: &str, user_id: &str) -> Result<(), DomainError>;
async fn cancel_upload(&self, upload_id: &str, user_id: Uuid) -> Result<(), DomainError>;
/// Check if a file size qualifies for chunked upload.
fn should_use_chunked(&self, size: u64) -> bool;
+14 -12
View File
@@ -1,32 +1,34 @@
use std::collections::HashSet;
use uuid::Uuid;
use crate::application::dtos::favorites_dto::{BatchFavoritesResult, FavoriteItemDto};
use crate::common::errors::Result;
/// Defines operations for managing user favorites
pub trait FavoritesUseCase: Send + Sync {
/// Get all favorites for a user
async fn get_favorites(&self, user_id: &str) -> Result<Vec<FavoriteItemDto>>;
async fn get_favorites(&self, user_id: Uuid) -> Result<Vec<FavoriteItemDto>>;
/// Add an item to user's favorites
async fn add_to_favorites(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>;
async fn add_to_favorites(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<()>;
/// Remove an item from user's favorites
async fn remove_from_favorites(
&self,
user_id: &str,
user_id: Uuid,
item_id: &str,
item_type: &str,
) -> Result<bool>;
/// Check if an item is in user's favorites
async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
async fn is_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<bool>;
/// Add multiple items to favorites in a single transaction.
/// Returns enriched favourites list so the client can replace its cache.
async fn batch_add_to_favorites(
&self,
user_id: &str,
user_id: Uuid,
items: &[(String, String)],
) -> Result<BatchFavoritesResult>;
@@ -34,7 +36,7 @@ pub trait FavoritesUseCase: Send + Sync {
/// Returns the set of item_ids that are favorites.
async fn batch_check_favorites(
&self,
user_id: &str,
user_id: Uuid,
item_ids: &[(&str, &str)], // (item_id, item_type) pairs
) -> Result<HashSet<String>>;
}
@@ -50,26 +52,26 @@ pub trait FavoritesUseCase: Send + Sync {
/// lives in `infrastructure::repositories::pg`.
pub trait FavoritesRepositoryPort: Send + Sync + 'static {
/// Gets all favorites for a user.
async fn get_favorites(&self, user_id: &str) -> Result<Vec<FavoriteItemDto>>;
async fn get_favorites(&self, user_id: Uuid) -> Result<Vec<FavoriteItemDto>>;
/// Adds an item to favorites. Returns `Ok(())` if it already existed (idempotent).
async fn add_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>;
async fn add_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<()>;
/// Removes an item from favorites. Returns `true` if it existed.
async fn remove_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
async fn remove_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<bool>;
/// Checks if an item is in favorites.
async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
async fn is_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<bool>;
/// Insert multiple items in a single transaction.
/// Returns the number of rows actually inserted (ignoring duplicates).
async fn add_favorites_batch(&self, user_id: &str, items: &[(String, String)]) -> Result<u64>;
async fn add_favorites_batch(&self, user_id: Uuid, items: &[(String, String)]) -> Result<u64>;
/// Check which of the given item IDs are favorites for this user.
/// Returns the set of item_ids that are favorites.
async fn batch_check_favorites(
&self,
user_id: &str,
user_id: Uuid,
item_ids: &[(&str, &str)], // (item_id, item_type) pairs
) -> Result<HashSet<String>>;
}
+14 -12
View File
@@ -3,6 +3,7 @@ use futures::Stream;
use std::path::Path;
use std::pin::Pin;
use std::sync::Arc;
use uuid::Uuid;
use crate::application::dtos::file_dto::FileDto;
use crate::application::ports::storage_ports::CopyFolderTreeResult;
@@ -116,7 +117,7 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
///
/// Returns `NotFound` if the file does not exist **or** belongs to
/// another user. All user-facing handlers should use this method.
async fn get_file_owned(&self, id: &str, caller_id: &str) -> Result<FileDto, DomainError>;
async fn get_file_owned(&self, id: &str, caller_id: Uuid) -> Result<FileDto, DomainError>;
/// Gets a file by its path (for WebDAV)
async fn get_file_by_path(&self, path: &str) -> Result<FileDto, DomainError>;
@@ -131,7 +132,7 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
async fn list_files_owned(
&self,
folder_id: Option<&str>,
owner_id: &str,
owner_id: Uuid,
) -> Result<Vec<FileDto>, DomainError>;
/// Gets file content as a stream (for large files)
@@ -144,7 +145,7 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
async fn get_file_stream_owned(
&self,
id: &str,
caller_id: &str,
caller_id: Uuid,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
/// Optimized multi-tier download.
@@ -166,7 +167,7 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
async fn get_file_optimized_owned(
&self,
id: &str,
caller_id: &str,
caller_id: Uuid,
accept_webp: bool,
prefer_original: bool,
) -> Result<(FileDto, OptimizedFileContent), DomainError>;
@@ -198,7 +199,7 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
async fn get_file_range_stream_owned(
&self,
id: &str,
caller_id: &str,
caller_id: Uuid,
start: u64,
end: Option<u64>,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
@@ -238,14 +239,15 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
async fn list_files_batch_for_owner(
&self,
folder_id: Option<&str>,
owner_id: &str,
owner_id: Uuid,
offset: i64,
limit: i64,
) -> Result<Vec<FileDto>, DomainError> {
let all = self.list_files_batch(folder_id, offset, limit).await?;
let owner_str = owner_id.to_string();
Ok(all
.into_iter()
.filter(|f| f.owner_id.as_deref().is_some_and(|o| o == owner_id))
.filter(|f| f.owner_id.as_deref().is_some_and(|o| o == owner_str))
.collect())
}
}
@@ -267,7 +269,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
async fn move_file_owned(
&self,
file_id: &str,
caller_id: &str,
caller_id: Uuid,
folder_id: Option<String>,
) -> Result<FileDto, DomainError>;
@@ -282,7 +284,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
async fn copy_file_owned(
&self,
file_id: &str,
caller_id: &str,
caller_id: Uuid,
target_folder_id: Option<String>,
) -> Result<FileDto, DomainError>;
@@ -293,7 +295,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
async fn rename_file_owned(
&self,
file_id: &str,
caller_id: &str,
caller_id: Uuid,
new_name: &str,
) -> Result<FileDto, DomainError>;
@@ -301,7 +303,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
/// Deletes a file, enforcing that `caller_id` is the owner.
async fn delete_file_owned(&self, id: &str, caller_id: &str) -> Result<(), DomainError>;
async fn delete_file_owned(&self, id: &str, caller_id: Uuid) -> Result<(), DomainError>;
/// Smart delete: trash-first with dedup reference cleanup.
///
@@ -310,7 +312,7 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
/// 3. Decrements the dedup reference count for the content hash.
///
/// Returns `Ok(true)` when trashed, `Ok(false)` when permanently deleted.
async fn delete_with_cleanup(&self, id: &str, user_id: &str) -> Result<bool, DomainError>;
async fn delete_with_cleanup(&self, id: &str, user_id: Uuid) -> Result<bool, DomainError>;
/// Copies an entire folder subtree atomically (WebDAV COPY Depth: infinity).
///
+10 -8
View File
@@ -1,5 +1,7 @@
use std::sync::Arc;
use uuid::Uuid;
use crate::application::dtos::folder_dto::{
CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto,
};
@@ -20,7 +22,7 @@ pub trait FolderUseCase: Send + Sync + 'static {
///
/// Returns `NotFound` if the folder does not exist **or** belongs to
/// another user. All user-facing handlers should use this method.
async fn get_folder_owned(&self, id: &str, caller_id: &str) -> Result<FolderDto, DomainError>;
async fn get_folder_owned(&self, id: &str, caller_id: Uuid) -> Result<FolderDto, DomainError>;
/// Gets a folder by its path
async fn get_folder_by_path(&self, path: &str) -> Result<FolderDto, DomainError>;
@@ -33,7 +35,7 @@ pub trait FolderUseCase: Send + Sync + 'static {
async fn list_folders_for_owner(
&self,
parent_id: Option<&str>,
owner_id: &str,
owner_id: Uuid,
) -> Result<Vec<FolderDto>, DomainError>;
/// Lists folders with pagination
@@ -47,7 +49,7 @@ pub trait FolderUseCase: Send + Sync + 'static {
async fn list_folders_for_owner_paginated(
&self,
parent_id: Option<&str>,
owner_id: &str,
owner_id: Uuid,
pagination: &crate::application::dtos::pagination::PaginationRequestDto,
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>;
@@ -56,7 +58,7 @@ pub trait FolderUseCase: Send + Sync + 'static {
&self,
id: &str,
dto: RenameFolderDto,
caller_id: &str,
caller_id: Uuid,
) -> Result<FolderDto, DomainError>;
/// Moves a folder to another parent (ownership verified against caller_id)
@@ -64,16 +66,16 @@ pub trait FolderUseCase: Send + Sync + 'static {
&self,
id: &str,
dto: MoveFolderDto,
caller_id: &str,
caller_id: Uuid,
) -> Result<FolderDto, DomainError>;
/// Deletes a folder (ownership verified against caller_id)
async fn delete_folder(&self, id: &str, caller_id: &str) -> Result<(), DomainError>;
async fn delete_folder(&self, id: &str, caller_id: Uuid) -> Result<(), DomainError>;
/// Creates a root-level home folder for a user during registration.
async fn create_home_folder(
&self,
user_id: &str,
user_id: Uuid,
name: String,
) -> Result<FolderDto, DomainError>;
@@ -103,7 +105,7 @@ pub trait SearchUseCase: Send + Sync + 'static {
async fn search(
&self,
criteria: SearchCriteriaDto,
user_id: &str,
user_id: Uuid,
) -> Result<Arc<SearchResultsDto>, DomainError>;
/// Returns quick suggestions for autocomplete (lightweight, fast).
+11 -9
View File
@@ -1,3 +1,5 @@
use uuid::Uuid;
use crate::application::dtos::recent_dto::RecentItemDto;
use crate::common::errors::Result;
@@ -6,24 +8,24 @@ pub trait RecentItemsUseCase: Send + Sync {
/// Get all recent items for a user
async fn get_recent_items(
&self,
user_id: &str,
user_id: Uuid,
limit: Option<i32>,
) -> Result<Vec<RecentItemDto>>;
/// Record access to an item
async fn record_item_access(&self, user_id: &str, item_id: &str, item_type: &str)
async fn record_item_access(&self, user_id: Uuid, item_id: &str, item_type: &str)
-> Result<()>;
/// Remove an item from recents
async fn remove_from_recent(
&self,
user_id: &str,
user_id: Uuid,
item_id: &str,
item_type: &str,
) -> Result<bool>;
/// Clear the entire recent items list
async fn clear_recent_items(&self, user_id: &str) -> Result<()>;
async fn clear_recent_items(&self, user_id: Uuid) -> Result<()>;
}
// ─────────────────────────────────────────────────────
@@ -36,17 +38,17 @@ pub trait RecentItemsUseCase: Send + Sync {
/// `RecentService` does not depend directly on `PgPool`.
pub trait RecentItemsRepositoryPort: Send + Sync + 'static {
/// Gets the latest recent items for a user (ordered by date desc).
async fn get_recent_items(&self, user_id: &str, limit: i32) -> Result<Vec<RecentItemDto>>;
async fn get_recent_items(&self, user_id: Uuid, limit: i32) -> Result<Vec<RecentItemDto>>;
/// Records/updates access to an item (upsert by user+item+type).
async fn upsert_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>;
async fn upsert_access(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<()>;
/// Removes an item from recents. Returns `true` if it existed.
async fn remove_item(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
async fn remove_item(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<bool>;
/// Removes all recent items for a user.
async fn clear_all(&self, user_id: &str) -> Result<()>;
async fn clear_all(&self, user_id: Uuid) -> Result<()>;
/// Removes items exceeding `max_items` (the oldest ones).
async fn prune(&self, user_id: &str, max_items: i32) -> Result<()>;
async fn prune(&self, user_id: Uuid, max_items: i32) -> Result<()>;
}
+14 -12
View File
@@ -1,3 +1,5 @@
use uuid::Uuid;
use crate::{
application::dtos::{
pagination::PaginatedResponseDto,
@@ -11,12 +13,12 @@ pub trait ShareUseCase: Send + Sync + 'static {
/// Create a new shared link for a file or folder
async fn create_shared_link(
&self,
user_id: &str,
user_id: Uuid,
dto: CreateShareDto,
) -> Result<ShareDto, DomainError>;
/// Get a shared link by its ID (ownership-verified)
async fn get_shared_link(&self, id: &str, requester_id: &str) -> Result<ShareDto, DomainError>;
async fn get_shared_link(&self, id: Uuid, requester_id: Uuid) -> Result<ShareDto, DomainError>;
/// Get a shared link by its token (for access by non-users)
async fn get_shared_link_by_token(&self, token: &str) -> Result<ShareDto, DomainError>;
@@ -26,24 +28,24 @@ pub trait ShareUseCase: Send + Sync + 'static {
&self,
item_id: &str,
item_type: &ShareItemType,
requester_id: &str,
requester_id: Uuid,
) -> Result<Vec<ShareDto>, DomainError>;
/// Update a shared link (ownership-verified)
async fn update_shared_link(
&self,
id: &str,
requester_id: &str,
id: Uuid,
requester_id: Uuid,
dto: UpdateShareDto,
) -> Result<ShareDto, DomainError>;
/// Delete a shared link (ownership-verified)
async fn delete_shared_link(&self, id: &str, requester_id: &str) -> Result<(), DomainError>;
async fn delete_shared_link(&self, id: Uuid, requester_id: Uuid) -> Result<(), DomainError>;
/// Get all shared links created by a specific user
async fn get_user_shared_links(
&self,
user_id: &str,
user_id: Uuid,
page: usize,
per_page: usize,
) -> Result<PaginatedResponseDto<ShareDto>, DomainError>;
@@ -77,19 +79,19 @@ pub trait ShareStoragePort: Send + Sync + 'static {
/// (prevents share-ID enumeration).
async fn find_share_by_id_for_user(
&self,
id: &str,
user_id: &str,
id: Uuid,
user_id: Uuid,
) -> Result<crate::domain::entities::share::Share, DomainError>;
/// Delete a share only if it belongs to the given user.
async fn delete_share_for_user(&self, id: &str, user_id: &str) -> Result<(), DomainError>;
async fn delete_share_for_user(&self, id: Uuid, user_id: Uuid) -> Result<(), DomainError>;
/// Find shares for a specific item that belong to the given user.
async fn find_shares_by_item_for_user(
&self,
item_id: &str,
item_type: &ShareItemType,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<crate::domain::entities::share::Share>, DomainError>;
async fn update_share(
@@ -99,7 +101,7 @@ pub trait ShareStoragePort: Send + Sync + 'static {
async fn find_shares_by_user(
&self,
user_id: &str,
user_id: Uuid,
offset: usize,
limit: usize,
) -> Result<(Vec<crate::domain::entities::share::Share>, usize), DomainError>;
+15 -12
View File
@@ -3,6 +3,7 @@ use futures::Stream;
use serde_json::Value;
use std::path::PathBuf;
use std::pin::Pin;
use uuid::Uuid;
use crate::application::dtos::search_dto::SearchCriteriaDto;
use crate::common::errors::DomainError;
@@ -33,13 +34,13 @@ pub trait FileReadPort: Send + Sync + 'static {
/// Returns `NotFound` if the file does not exist **or** belongs to a
/// different user. This is the primary IDOR-safe accessor — handlers
/// serving end-user requests should always prefer this over `get_file`.
async fn get_file_for_owner(&self, id: &str, owner_id: &str) -> Result<File, DomainError>;
async fn get_file_for_owner(&self, id: &str, owner_id: Uuid) -> Result<File, DomainError>;
/// Verifies that the file identified by `id` belongs to `owner_id`.
///
/// Returns `Ok(())` on success or `NotFound` when the file does not
/// exist or belongs to another user.
async fn verify_file_owner(&self, id: &str, owner_id: &str) -> Result<(), DomainError> {
async fn verify_file_owner(&self, id: &str, owner_id: Uuid) -> Result<(), DomainError> {
self.get_file_for_owner(id, owner_id).await.map(|_| ())
}
@@ -53,12 +54,13 @@ pub trait FileReadPort: Send + Sync + 'static {
async fn list_files_for_owner(
&self,
folder_id: Option<&str>,
owner_id: &str,
owner_id: Uuid,
) -> Result<Vec<File>, DomainError> {
let all = self.list_files(folder_id).await?;
let owner_str = owner_id.to_string();
Ok(all
.into_iter()
.filter(|f| f.owner_id().is_some_and(|o| o == owner_id))
.filter(|f| f.owner_id().is_some_and(|o| o == owner_str))
.collect())
}
@@ -134,15 +136,16 @@ pub trait FileReadPort: Send + Sync + 'static {
async fn list_files_batch_for_owner(
&self,
folder_id: Option<&str>,
owner_id: &str,
owner_id: Uuid,
offset: i64,
limit: i64,
) -> Result<Vec<File>, DomainError> {
// Default: filter in-memory (repos should override with SQL)
let all = self.list_files_batch(folder_id, offset, limit).await?;
let owner_str = owner_id.to_string();
Ok(all
.into_iter()
.filter(|f| f.owner_id().is_some_and(|o| o == owner_id))
.filter(|f| f.owner_id().is_some_and(|o| o == owner_str))
.collect())
}
@@ -175,7 +178,7 @@ pub trait FileReadPort: Send + Sync + 'static {
&self,
folder_id: Option<&str>,
criteria: &SearchCriteriaDto,
user_id: &str,
user_id: Uuid,
) -> Result<(Vec<File>, usize), DomainError>;
/// Search files recursively in a folder subtree using ltree.
@@ -190,7 +193,7 @@ pub trait FileReadPort: Send + Sync + 'static {
&self,
root_folder_id: Option<&str>,
criteria: &SearchCriteriaDto,
user_id: &str,
user_id: Uuid,
) -> Result<(Vec<File>, usize), DomainError> {
// Default: delegate to paginated search (non-recursive fallback)
self.search_files_paginated(root_folder_id, criteria, user_id)
@@ -204,7 +207,7 @@ pub trait FileReadPort: Send + Sync + 'static {
&self,
folder_id: Option<&str>,
criteria: &SearchCriteriaDto,
user_id: &str,
user_id: Uuid,
) -> Result<usize, DomainError>;
/// Return up to `limit` files whose name contains `query` (case-insensitive).
@@ -359,7 +362,7 @@ pub trait FileWritePort: Send + Sync + 'static {
/// Secondary port for storage usage management
pub trait StorageUsagePort: Send + Sync + 'static {
/// Updates storage usage statistics for a user
async fn update_user_storage_usage(&self, user_id: &str) -> Result<i64, DomainError>;
async fn update_user_storage_usage(&self, user_id: Uuid) -> Result<i64, DomainError>;
/// Updates storage usage statistics for a user, looked up by username
async fn update_user_storage_usage_by_username(
@@ -375,12 +378,12 @@ pub trait StorageUsagePort: Send + Sync + 'static {
/// descriptive message otherwise.
async fn check_storage_quota(
&self,
user_id: &str,
user_id: Uuid,
additional_bytes: u64,
) -> Result<(), DomainError>;
/// Returns (used_bytes, quota_bytes) for a user.
async fn get_user_storage_info(&self, user_id: &str) -> Result<(i64, i64), DomainError>;
async fn get_user_storage_info(&self, user_id: Uuid) -> Result<(i64, i64), DomainError>;
}
/// Generic storage service interface for calendar and contact services
+7 -5
View File
@@ -1,20 +1,22 @@
use uuid::Uuid;
use crate::application::dtos::trash_dto::TrashedItemDto;
use crate::common::errors::Result;
/// Port for trash-related use cases
pub trait TrashUseCase: Send + Sync {
/// List items in the user's trash
async fn get_trash_items(&self, user_id: &str) -> Result<Vec<TrashedItemDto>>;
async fn get_trash_items(&self, user_id: Uuid) -> Result<Vec<TrashedItemDto>>;
/// Move a file or folder to trash
async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: &str) -> Result<()>;
async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: Uuid) -> Result<()>;
/// Restore an item from trash to its original location
async fn restore_item(&self, trash_id: &str, user_id: &str) -> Result<()>;
async fn restore_item(&self, trash_id: &str, user_id: Uuid) -> Result<()>;
/// Permanently delete an item from trash
async fn delete_permanently(&self, trash_id: &str, user_id: &str) -> Result<()>;
async fn delete_permanently(&self, trash_id: &str, user_id: Uuid) -> Result<()>;
/// Empty the trash for a specific user
async fn empty_trash(&self, user_id: &str) -> Result<()>;
async fn empty_trash(&self, user_id: Uuid) -> Result<()>;
}
@@ -1,4 +1,5 @@
use std::sync::Arc;
use uuid::Uuid;
use crate::application::dtos::settings_dto::{
OidcSettingsDto, OidcTestResultDto, SaveOidcSettingsDto, TestOidcConnectionDto,
@@ -181,7 +182,7 @@ impl AdminSettingsService {
pub async fn save_oidc_settings(
&self,
dto: SaveOidcSettingsDto,
updated_by: &str,
updated_by: Uuid,
) -> Result<(), DomainError> {
let cat = "oidc";
let by = Some(updated_by);
@@ -366,7 +367,7 @@ impl AdminSettingsService {
}
/// Mark the system as initialized after the first admin is created.
pub async fn mark_system_initialized(&self, admin_user_id: &str) -> Result<(), DomainError> {
pub async fn mark_system_initialized(&self, admin_user_id: Uuid) -> Result<(), DomainError> {
self.settings_repo
.set(
"system_initialized",
@@ -384,7 +385,7 @@ impl AdminSettingsService {
/// initialized (the caller "won" the race), or `Ok(false)` if another
/// request already did it. This eliminates the race-condition window
/// between `is_system_initialized()` and `mark_system_initialized()`.
pub async fn try_claim_initialization(&self, admin_user_id: &str) -> Result<bool, DomainError> {
pub async fn try_claim_initialization(&self, admin_user_id: Uuid) -> Result<bool, DomainError> {
self.settings_repo
.try_claim_initialization(admin_user_id)
.await
@@ -412,7 +413,7 @@ impl AdminSettingsService {
pub async fn set_registration_enabled(
&self,
enabled: bool,
updated_by: &str,
updated_by: Uuid,
) -> Result<(), DomainError> {
self.settings_repo
.set(
@@ -17,6 +17,7 @@ use moka::future::Cache;
use rand_core::RngCore;
use std::sync::Arc;
use std::time::Duration as StdDuration;
use uuid::Uuid;
/// App password token length (32 random alphanumeric chars after prefix).
const TOKEN_LENGTH: usize = 32;
@@ -41,7 +42,7 @@ const BASIC_AUTH_CACHE_MAX_ENTRIES: u64 = 10_000;
/// Cached identity returned after a successful Basic Auth verification.
#[derive(Clone)]
struct CachedBasicAuthResult {
user_id: String,
user_id: Uuid,
username: String,
email: String,
role: String,
@@ -118,7 +119,7 @@ impl AppPasswordService {
/// Returns the response DTO that includes the plain-text password (shown only once).
pub async fn create(
&self,
user_id: &str,
user_id: Uuid,
request: CreateAppPasswordRequestDto,
) -> Result<AppPasswordCreatedResponseDto, DomainError> {
// Validate label
@@ -147,7 +148,7 @@ impl AppPasswordService {
// Create entity
let app_password = AppPassword::new(
user_id.to_string(),
user_id,
label.clone(),
password_hash,
prefix.clone(),
@@ -165,7 +166,7 @@ impl AppPasswordService {
);
Ok(AppPasswordCreatedResponseDto {
id: saved.id,
id: saved.id.to_string(),
label,
password: plain_token,
username: username.clone(),
@@ -200,7 +201,7 @@ impl AppPasswordService {
}
/// List all app passwords for a user (excludes plain-text passwords).
pub async fn list(&self, user_id: &str) -> Result<AppPasswordListResponseDto, DomainError> {
pub async fn list(&self, user_id: Uuid) -> Result<AppPasswordListResponseDto, DomainError> {
let passwords = self.repo.list_by_user(user_id).await?;
let total = passwords.len();
@@ -209,7 +210,7 @@ impl AppPasswordService {
.map(|ap| {
let is_active = ap.active && !ap.is_expired();
AppPasswordSummaryDto {
id: ap.id,
id: ap.id.to_string(),
label: ap.label,
prefix: format!("{}...", ap.prefix),
scopes: ap.scopes,
@@ -234,8 +235,8 @@ impl AppPasswordService {
/// up to `BASIC_AUTH_CACHE_TTL_SECS`).
pub async fn revoke(
&self,
user_id: &str,
id: &str,
user_id: Uuid,
id: Uuid,
) -> Result<AppPasswordRevokeResponseDto, DomainError> {
// Ownership enforced at SQL level (WHERE user_id = $2).
// The get_by_id pre-check gives a clear error message when
@@ -250,7 +251,7 @@ impl AppPasswordService {
// Invalidate all cached auth entries for this user so the
// revocation is effective immediately.
let uid = user_id.to_string();
let uid = user_id;
self.auth_cache
.invalidate_entries_if(move |_key, val| val.user_id == uid)
.ok();
@@ -282,7 +283,7 @@ impl AppPasswordService {
&self,
username: &str,
password: &str,
) -> Result<(String, String, String, String), DomainError> {
) -> Result<(Uuid, String, String, String), DomainError> {
// ── 1. Compute cache key = blake3("username:password") ────────
let cache_key: [u8; 32] =
blake3::hash(format!("{}:{}", username, password).as_bytes()).into();
@@ -344,10 +345,10 @@ impl AppPasswordService {
.verify_password(&verify_password, &ap.password_hash)
.await
{
let _ = self.repo.touch_last_used(&ap.id).await;
let _ = self.repo.touch_last_used(ap.id).await;
let result = CachedBasicAuthResult {
user_id: user.id().to_string(),
user_id: user.id(),
username: user.username().to_string(),
email: user.email().to_string(),
role: user.role().to_string(),
@@ -372,16 +373,16 @@ impl AppPasswordService {
/// Returns `(id, plain_password)`.
pub async fn create_nc(
&self,
user_id: &str,
user_id: Uuid,
label: &str,
) -> Result<(String, String), DomainError> {
) -> Result<(Uuid, String), DomainError> {
let password = generate_nc_app_password();
let normalized = nc_normalize_password(&password);
let prefix = nc_token_prefix(&normalized)?;
let hash = self.hasher.hash_password(&normalized).await?;
let ap = AppPassword::new(
user_id.to_string(),
user_id,
label.to_string(),
hash,
prefix,
@@ -397,7 +398,7 @@ impl AppPasswordService {
/// Scoped to the authenticated user (fixes I3 — no global prefix search).
pub async fn revoke_by_password(
&self,
user_id: &str,
user_id: Uuid,
password: &str,
) -> Result<(), DomainError> {
let normalized = nc_normalize_password(password);
@@ -417,10 +418,10 @@ impl AppPasswordService {
.verify_password(&normalized, &ap.password_hash)
.await
{
self.repo.revoke(&ap.id, user_id).await?;
self.repo.revoke(ap.id, user_id).await?;
// Invalidate cache for this user
let uid = user_id.to_string();
let uid = user_id;
self.auth_cache
.invalidate_entries_if(move |_key, val| val.user_id == uid)
.ok();
@@ -432,12 +433,12 @@ impl AppPasswordService {
}
/// List app passwords for a user (simple summary for NC UI).
pub async fn list_nc(&self, user_id: &str) -> Result<Vec<AppPassword>, DomainError> {
pub async fn list_nc(&self, user_id: Uuid) -> Result<Vec<AppPassword>, DomainError> {
self.repo.list_by_user(user_id).await
}
/// Delete an app password by ID, scoped to the owning user.
pub async fn delete_by_user(&self, id: &str, user_id: &str) -> Result<(), DomainError> {
pub async fn delete_by_user(&self, id: Uuid, user_id: Uuid) -> Result<(), DomainError> {
let deleted = self.repo.delete_by_user_and_id(id, user_id).await?;
if !deleted {
return Err(DomainError::new(
@@ -17,6 +17,7 @@ use crate::infrastructure::services::jwt_service::JwtTokenService;
use crate::infrastructure::services::oidc_service::OidcService;
use crate::infrastructure::services::password_hasher::Argon2PasswordHasher;
use moka::sync::Cache;
use uuid::Uuid;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::RwLock;
@@ -33,7 +34,7 @@ pub enum OidcCallbackResult {
/// app password and complete the NC login flow.
NextcloudLogin {
nc_flow_token: String,
user_id: String,
user_id: Uuid,
username: String,
},
}
@@ -411,7 +412,7 @@ impl AuthApplicationService {
// Save session
let session = Session::new(
user.id().to_string(),
user.id(),
refresh_token.clone(),
None, // IP (can be added from the HTTP layer)
None, // User-Agent (can be added from the HTTP layer)
@@ -466,7 +467,7 @@ impl AuthApplicationService {
}
Ok(crate::application::dtos::user_dto::CurrentUser {
id: user.id().to_string(),
id: user.id(),
username: user.username().to_string(),
email: user.email().to_string(),
role: user.role().to_string(),
@@ -514,7 +515,7 @@ impl AuthApplicationService {
// Create new session
let new_session = Session::new(
user.id().to_string(),
user.id(),
new_refresh_token.clone(),
None,
None,
@@ -532,7 +533,7 @@ impl AuthApplicationService {
})
}
pub async fn logout(&self, user_id: &str, refresh_token: &str) -> Result<(), DomainError> {
pub async fn logout(&self, user_id: Uuid, refresh_token: &str) -> Result<(), DomainError> {
// Get session
let session = match self
.session_storage
@@ -559,7 +560,7 @@ impl AuthApplicationService {
Ok(())
}
pub async fn logout_all(&self, user_id: &str) -> Result<u64, DomainError> {
pub async fn logout_all(&self, user_id: Uuid) -> Result<u64, DomainError> {
// Revoke all user sessions
let revoked_count = self
.session_storage
@@ -571,7 +572,7 @@ impl AuthApplicationService {
pub async fn change_password(
&self,
user_id: &str,
user_id: Uuid,
dto: ChangePasswordDto,
) -> Result<(), DomainError> {
// Get user
@@ -627,13 +628,13 @@ impl AuthApplicationService {
Ok(())
}
pub async fn get_user(&self, user_id: &str) -> Result<UserDto, DomainError> {
pub async fn get_user(&self, user_id: Uuid) -> Result<UserDto, DomainError> {
let user = self.user_storage.get_user_by_id(user_id).await?;
Ok(UserDto::from(user))
}
// Alias for consistency with handler method
pub async fn get_user_by_id(&self, user_id: &str) -> Result<UserDto, DomainError> {
pub async fn get_user_by_id(&self, user_id: Uuid) -> Result<UserDto, DomainError> {
self.get_user(user_id).await
}
@@ -778,7 +779,7 @@ impl AuthApplicationService {
/// Admin-only: reset a user's password.
pub async fn admin_reset_password(
&self,
user_id: &str,
user_id: Uuid,
new_password: &str,
) -> Result<(), DomainError> {
// Block password reset for OIDC-provisioned users
@@ -812,13 +813,13 @@ impl AuthApplicationService {
}
/// Get a single user by ID (for admin panel)
pub async fn get_user_admin(&self, user_id: &str) -> Result<UserDto, DomainError> {
pub async fn get_user_admin(&self, user_id: Uuid) -> Result<UserDto, DomainError> {
let user = self.user_storage.get_user_by_id(user_id).await?;
Ok(UserDto::from(user))
}
/// Delete a user by ID (admin only)
pub async fn delete_user_admin(&self, user_id: &str) -> Result<(), DomainError> {
pub async fn delete_user_admin(&self, user_id: Uuid) -> Result<(), DomainError> {
// Prevent deleting yourself
let user = self.user_storage.get_user_by_id(user_id).await?;
tracing::info!("Admin deleting user: {} ({})", user.username(), user_id);
@@ -826,14 +827,14 @@ impl AuthApplicationService {
}
/// Activate or deactivate a user (admin only)
pub async fn set_user_active(&self, user_id: &str, active: bool) -> Result<(), DomainError> {
pub async fn set_user_active(&self, user_id: Uuid, active: bool) -> Result<(), DomainError> {
self.user_storage
.set_user_active_status(user_id, active)
.await
}
/// Change user role (admin only)
pub async fn change_user_role(&self, user_id: &str, role: &str) -> Result<(), DomainError> {
pub async fn change_user_role(&self, user_id: Uuid, role: &str) -> Result<(), DomainError> {
if role != "admin" && role != "user" {
return Err(DomainError::new(
ErrorKind::InvalidInput,
@@ -847,7 +848,7 @@ impl AuthApplicationService {
/// Update user's storage quota (admin only)
pub async fn update_user_quota(
&self,
user_id: &str,
user_id: Uuid,
quota_bytes: i64,
) -> Result<(), DomainError> {
if quota_bytes < 0 {
@@ -865,7 +866,7 @@ impl AuthApplicationService {
/// Check if a user has enough quota for an upload of the given size
pub async fn check_quota(
&self,
user_id: &str,
user_id: Uuid,
additional_bytes: i64,
) -> Result<bool, DomainError> {
let user = self.user_storage.get_user_by_id(user_id).await?;
@@ -1209,7 +1210,7 @@ impl AuthApplicationService {
);
return Ok(OidcCallbackResult::NextcloudLogin {
nc_flow_token: nc_token,
user_id: user.id().to_string(),
user_id: user.id(),
username: user.username().to_string(),
});
}
@@ -1219,7 +1220,7 @@ impl AuthApplicationService {
let refresh_token = self.token_service.generate_refresh_token();
let session = Session::new(
user.id().to_string(),
user.id(),
refresh_token.clone(),
None,
None,
@@ -1282,7 +1283,7 @@ impl AuthApplicationService {
}
/// Helper to create a personal folder for a new user
async fn create_personal_folder(&self, username: &str, user_id: &str) {
async fn create_personal_folder(&self, username: &str, user_id: Uuid) {
if let Some(folder_service) = &self.folder_service {
let folder_name = format!("My Folder - {}", username);
match folder_service
+32 -58
View File
@@ -20,6 +20,7 @@ use crate::application::services::folder_service::FolderService;
use crate::application::services::trash_service::TrashService;
use crate::common::config::AppConfig;
use crate::common::errors::DomainError;
use uuid::Uuid;
/// Specific errors for batch operations
#[derive(Debug, Error)]
@@ -117,7 +118,7 @@ impl BatchOperationService {
&self,
file_ids: Vec<String>,
target_folder_id: Option<String>,
caller_id: &str,
user_id: Uuid,
) -> Result<BatchResult<FileDto>, BatchOperationError> {
info!("Starting batch copy of {} files", file_ids.len());
let start_time = std::time::Instant::now();
@@ -135,17 +136,15 @@ impl BatchOperationService {
// Arc<str> avoids N heap-clones of the same string
let target_folder: Option<Arc<str>> = target_folder_id.map(|s| Arc::from(s.as_str()));
let caller: Arc<str> = Arc::from(caller_id);
// buffer_unordered materialises only max_concurrent futures at a time
let mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| {
let mgmt = self.file_management.clone();
let target_folder = target_folder.clone();
let caller = caller.clone();
async move {
let copy_result = mgmt
.copy_file_owned(&file_id, &caller, target_folder.map(|s| s.to_string()))
.copy_file_owned(&file_id, user_id, target_folder.map(|s| s.to_string()))
.await;
(file_id, copy_result)
}
@@ -187,7 +186,7 @@ impl BatchOperationService {
&self,
file_ids: Vec<String>,
target_folder_id: Option<String>,
caller_id: &str,
user_id: Uuid,
) -> Result<BatchResult<FileDto>, BatchOperationError> {
info!("Starting batch move of {} files", file_ids.len());
let start_time = std::time::Instant::now();
@@ -204,16 +203,14 @@ impl BatchOperationService {
};
let target_folder: Option<Arc<str>> = target_folder_id.map(|s| Arc::from(s.as_str()));
let caller: Arc<str> = Arc::from(caller_id);
let mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| {
let mgmt = self.file_management.clone();
let target_folder = target_folder.clone();
let caller = caller.clone();
async move {
let move_result = mgmt
.move_file_owned(&file_id, &caller, target_folder.map(|s| s.to_string()))
.move_file_owned(&file_id, user_id, target_folder.map(|s| s.to_string()))
.await;
(file_id, move_result)
}
@@ -253,7 +250,7 @@ impl BatchOperationService {
pub async fn delete_files(
&self,
file_ids: Vec<String>,
caller_id: &str,
user_id: Uuid,
) -> Result<BatchResult<String>, BatchOperationError> {
info!("Starting batch deletion of {} files", file_ids.len());
let start_time = std::time::Instant::now();
@@ -268,15 +265,11 @@ impl BatchOperationService {
},
};
// Define the operation to perform for each file
let caller: Arc<str> = Arc::from(caller_id);
let mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| {
let mgmt = self.file_management.clone();
let caller = caller.clone();
async move {
let delete_result = mgmt.delete_file_owned(&file_id, &caller).await;
let delete_result = mgmt.delete_file_owned(&file_id, user_id).await;
let id_for_result = file_id.clone();
(file_id, delete_result.map(|_| id_for_result))
}
@@ -317,7 +310,7 @@ impl BatchOperationService {
pub async fn get_multiple_files(
&self,
file_ids: Vec<String>,
caller_id: &str,
user_id: Uuid,
) -> Result<BatchResult<FileDto>, BatchOperationError> {
info!("Starting batch load of {} files", file_ids.len());
let start_time = std::time::Instant::now();
@@ -332,15 +325,11 @@ impl BatchOperationService {
},
};
// Define the operation to perform for each file
let caller: Arc<str> = Arc::from(caller_id);
let mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| {
let retrieval = self.file_retrieval.clone();
let caller = caller.clone();
async move {
let get_result = retrieval.get_file_owned(&file_id, &caller).await;
let get_result = retrieval.get_file_owned(&file_id, user_id).await;
(file_id, get_result)
}
}))
@@ -381,7 +370,7 @@ impl BatchOperationService {
&self,
folder_ids: Vec<String>,
_recursive: bool,
caller_id: &str,
user_id: Uuid,
) -> Result<BatchResult<String>, BatchOperationError> {
info!("Starting batch deletion of {} folders", folder_ids.len());
let start_time = std::time::Instant::now();
@@ -396,16 +385,11 @@ impl BatchOperationService {
},
};
// Define the operation to perform for each folder
// Arc<str> avoids N heap-clones of the caller string
let caller: Arc<str> = Arc::from(caller_id);
let mut operation_stream = stream::iter(folder_ids.into_iter().map(|folder_id| {
let folder_service = self.folder_service.clone();
let caller = caller.clone();
async move {
let delete_result = folder_service.delete_folder(&folder_id, &caller).await;
let delete_result = folder_service.delete_folder(&folder_id, user_id).await;
let id_for_result = folder_id.clone();
(folder_id, delete_result.map(|_| id_for_result))
}
@@ -445,7 +429,7 @@ impl BatchOperationService {
pub async fn trash_files(
&self,
file_ids: Vec<String>,
user_id: &str,
user_id: Uuid,
) -> Result<BatchResult<String>, BatchOperationError> {
let trash_service = self
.trash_service
@@ -464,14 +448,14 @@ impl BatchOperationService {
},
};
let uid: Arc<str> = Arc::from(user_id);
let uid = user_id;
let mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| {
let trash = trash_service.clone();
let uid = uid.clone();
let uid = uid;
async move {
let trash_result = trash.move_to_trash(&file_id, "file", &uid).await;
let trash_result = trash.move_to_trash(&file_id, "file", uid).await;
let id_for_result = file_id.clone();
(file_id, trash_result.map(|_| id_for_result))
}
@@ -510,7 +494,7 @@ impl BatchOperationService {
pub async fn trash_folders(
&self,
folder_ids: Vec<String>,
user_id: &str,
user_id: Uuid,
) -> Result<BatchResult<String>, BatchOperationError> {
let trash_service = self
.trash_service
@@ -529,14 +513,14 @@ impl BatchOperationService {
},
};
let uid: Arc<str> = Arc::from(user_id);
let uid = user_id;
let mut operation_stream = stream::iter(folder_ids.into_iter().map(|folder_id| {
let trash = trash_service.clone();
let uid = uid.clone();
let uid = uid;
async move {
let trash_result = trash.move_to_trash(&folder_id, "folder", &uid).await;
let trash_result = trash.move_to_trash(&folder_id, "folder", uid).await;
let id_for_result = folder_id.clone();
(folder_id, trash_result.map(|_| id_for_result))
}
@@ -576,7 +560,7 @@ impl BatchOperationService {
&self,
folder_ids: Vec<String>,
target_folder_id: Option<String>,
caller_id: &str,
user_id: Uuid,
) -> Result<BatchResult<FolderDto>, BatchOperationError> {
info!("Starting batch move of {} folders", folder_ids.len());
let start_time = std::time::Instant::now();
@@ -591,18 +575,16 @@ impl BatchOperationService {
};
let target: Option<Arc<str>> = target_folder_id.map(|s| Arc::from(s.as_str()));
let caller: Arc<str> = Arc::from(caller_id);
let mut operation_stream = stream::iter(folder_ids.into_iter().map(|folder_id| {
let folder_service = self.folder_service.clone();
let target = target.clone();
let caller = caller.clone();
async move {
let dto = MoveFolderDto {
parent_id: target.map(|s| s.to_string()),
};
let move_result = folder_service.move_folder(&folder_id, dto, &caller).await;
let move_result = folder_service.move_folder(&folder_id, dto, user_id).await;
(folder_id, move_result)
}
}))
@@ -645,7 +627,7 @@ impl BatchOperationService {
&self,
file_ids: Vec<String>,
folder_ids: Vec<String>,
caller_id: &str,
user_id: Uuid,
) -> Result<NamedTempFile, BatchOperationError> {
info!(
"Starting batch download: {} files, {} folders",
@@ -665,10 +647,10 @@ impl BatchOperationService {
// ── Add individual files at the root of the ZIP ──────────────────
for file_id in &file_ids {
match self.file_retrieval.get_file_owned(file_id, caller_id).await {
match self.file_retrieval.get_file_owned(file_id, user_id).await {
Ok(file_dto) => {
if let Err(e) = self
.add_file_entry_streamed(&mut zip, file_id, &file_dto.name, caller_id)
.add_file_entry_streamed(&mut zip, file_id, &file_dto.name, user_id)
.await
{
info!("Could not add file {} to ZIP: {}", file_dto.name, e);
@@ -684,12 +666,12 @@ impl BatchOperationService {
for folder_id in &folder_ids {
match self
.folder_service
.get_folder_owned(folder_id, caller_id)
.get_folder_owned(folder_id, user_id)
.await
{
Ok(root_folder) => {
if let Err(e) = self
.add_folder_subtree_to_zip(&mut zip, folder_id, &root_folder, caller_id)
.add_folder_subtree_to_zip(&mut zip, folder_id, &root_folder, user_id)
.await
{
info!("Could not add folder {} to ZIP: {}", root_folder.name, e);
@@ -728,7 +710,7 @@ impl BatchOperationService {
zip: &mut ZipFileWriter<tokio_util::compat::Compat<BufWriter<tokio::fs::File>>>,
file_id: &str,
entry_name: &str,
caller_id: &str,
caller_id: Uuid,
) -> Result<(), BatchOperationError> {
let entry = ZipEntryBuilder::new(entry_name.to_string().into(), Compression::Deflate);
let mut writer = zip
@@ -769,7 +751,7 @@ impl BatchOperationService {
zip: &mut ZipFileWriter<tokio_util::compat::Compat<BufWriter<tokio::fs::File>>>,
folder_id: &str,
root_folder: &FolderDto,
caller_id: &str,
caller_id: Uuid,
) -> Result<(), BatchOperationError> {
// Bulk-fetch folder tree (small — one entry per folder)
let all_folders = self
@@ -908,7 +890,7 @@ impl BatchOperationService {
pub async fn create_folders(
&self,
folders: Vec<(String, Option<String>)>, // (name, parent_id)
caller_id: &str,
user_id: Uuid,
) -> Result<BatchResult<FolderDto>, BatchOperationError> {
info!("Starting batch creation of {} folders", folders.len());
let start_time = std::time::Instant::now();
@@ -923,17 +905,13 @@ impl BatchOperationService {
},
};
// Define the operation for each folder
let caller: Arc<str> = Arc::from(caller_id);
let mut operation_stream = stream::iter(folders.into_iter().map(|(name, parent_id)| {
let folder_service = self.folder_service.clone();
let caller = caller.clone();
async move {
// If a parent is specified, verify the caller owns it
if let Some(ref pid) = parent_id
&& let Err(e) = folder_service.get_folder_owned(pid, &caller).await
&& let Err(e) = folder_service.get_folder_owned(pid, user_id).await
{
let id = format!("{}:{}", name, pid);
return (id, Err(e));
@@ -983,7 +961,7 @@ impl BatchOperationService {
pub async fn get_multiple_folders(
&self,
folder_ids: Vec<String>,
caller_id: &str,
user_id: Uuid,
) -> Result<BatchResult<FolderDto>, BatchOperationError> {
info!("Starting batch load of {} folders", folder_ids.len());
let start_time = std::time::Instant::now();
@@ -998,15 +976,11 @@ impl BatchOperationService {
},
};
// Define the operation for each folder
let caller: Arc<str> = Arc::from(caller_id);
let mut operation_stream = stream::iter(folder_ids.into_iter().map(|folder_id| {
let folder_service = self.folder_service.clone();
let caller = caller.clone();
async move {
let get_result = folder_service.get_folder_owned(&folder_id, &caller).await;
let get_result = folder_service.get_folder_owned(&folder_id, user_id).await;
(folder_id, get_result)
}
}))
+22 -21
View File
@@ -1,5 +1,6 @@
use chrono::{DateTime, Utc};
use std::sync::Arc;
use uuid::Uuid;
use crate::application::dtos::calendar_dto::{
CalendarDto, CalendarEventDto, CreateCalendarDto, CreateEventDto, CreateEventICalDto,
@@ -23,7 +24,7 @@ impl CalendarUseCase for CalendarService {
async fn create_calendar(
&self,
calendar: CreateCalendarDto,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarDto, DomainError> {
self.calendar_storage
.create_calendar(calendar, user_id)
@@ -34,7 +35,7 @@ impl CalendarUseCase for CalendarService {
&self,
calendar_id: &str,
update: UpdateCalendarDto,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarDto, DomainError> {
let has_access = self
.calendar_storage
@@ -52,7 +53,7 @@ impl CalendarUseCase for CalendarService {
.await
}
async fn delete_calendar(&self, calendar_id: &str, user_id: &str) -> Result<(), DomainError> {
async fn delete_calendar(&self, calendar_id: &str, user_id: Uuid) -> Result<(), DomainError> {
let has_access = self
.calendar_storage
.check_calendar_access(calendar_id, user_id)
@@ -70,7 +71,7 @@ impl CalendarUseCase for CalendarService {
async fn get_calendar(
&self,
calendar_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarDto, DomainError> {
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
let has_access = self
@@ -87,11 +88,11 @@ impl CalendarUseCase for CalendarService {
Ok(calendar)
}
async fn list_my_calendars(&self, user_id: &str) -> Result<Vec<CalendarDto>, DomainError> {
async fn list_my_calendars(&self, user_id: Uuid) -> Result<Vec<CalendarDto>, DomainError> {
self.calendar_storage.list_calendars_by_owner(user_id).await
}
async fn list_shared_calendars(&self, user_id: &str) -> Result<Vec<CalendarDto>, DomainError> {
async fn list_shared_calendars(&self, user_id: Uuid) -> Result<Vec<CalendarDto>, DomainError> {
self.calendar_storage
.list_calendars_shared_with_user(user_id)
.await
@@ -112,12 +113,12 @@ impl CalendarUseCase for CalendarService {
async fn share_calendar(
&self,
calendar_id: &str,
target_user_id: &str,
target_user_id: Uuid,
access_level: &str,
caller_user_id: &str,
caller_user_id: Uuid,
) -> Result<(), DomainError> {
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
if calendar.owner_id != caller_user_id {
if calendar.owner_id != caller_user_id.to_string() {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"Calendar",
@@ -145,11 +146,11 @@ impl CalendarUseCase for CalendarService {
async fn remove_calendar_sharing(
&self,
calendar_id: &str,
target_user_id: &str,
caller_user_id: &str,
target_user_id: Uuid,
caller_user_id: Uuid,
) -> Result<(), DomainError> {
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
if calendar.owner_id != caller_user_id {
if calendar.owner_id != caller_user_id.to_string() {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"Calendar",
@@ -164,10 +165,10 @@ impl CalendarUseCase for CalendarService {
async fn get_calendar_shares(
&self,
calendar_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<(String, String)>, DomainError> {
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
if calendar.owner_id != user_id {
if calendar.owner_id != user_id.to_string() {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"Calendar",
@@ -180,7 +181,7 @@ impl CalendarUseCase for CalendarService {
async fn create_event(
&self,
event: CreateEventDto,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarEventDto, DomainError> {
let has_access = self
.calendar_storage
@@ -199,7 +200,7 @@ impl CalendarUseCase for CalendarService {
async fn create_event_from_ical(
&self,
event: CreateEventICalDto,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarEventDto, DomainError> {
let has_access = self
.calendar_storage
@@ -219,7 +220,7 @@ impl CalendarUseCase for CalendarService {
&self,
event_id: &str,
update: UpdateEventDto,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarEventDto, DomainError> {
let event = self.calendar_storage.get_event(event_id).await?;
let has_access = self
@@ -236,7 +237,7 @@ impl CalendarUseCase for CalendarService {
self.calendar_storage.update_event(event_id, update).await
}
async fn delete_event(&self, event_id: &str, user_id: &str) -> Result<(), DomainError> {
async fn delete_event(&self, event_id: &str, user_id: Uuid) -> Result<(), DomainError> {
let event = self.calendar_storage.get_event(event_id).await?;
let has_access = self
.calendar_storage
@@ -255,7 +256,7 @@ impl CalendarUseCase for CalendarService {
async fn get_event(
&self,
event_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<CalendarEventDto, DomainError> {
let event = self.calendar_storage.get_event(event_id).await?;
let has_access = self
@@ -281,7 +282,7 @@ impl CalendarUseCase for CalendarService {
calendar_id: &str,
limit: Option<i64>,
offset: Option<i64>,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<CalendarEventDto>, DomainError> {
let has_access = self
.calendar_storage
@@ -313,7 +314,7 @@ impl CalendarUseCase for CalendarService {
calendar_id: &str,
start: DateTime<Utc>,
end: DateTime<Utc>,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<CalendarEventDto>, DomainError> {
let has_access = self
.calendar_storage
+103 -54
View File
@@ -1,5 +1,5 @@
use chrono::Utc;
use sqlx::types::Uuid;
use uuid::Uuid;
use std::sync::Arc;
use crate::application::dtos::address_book_dto::{
@@ -43,7 +43,7 @@ impl ContactService {
async fn check_address_book_access(
&self,
address_book_id: &Uuid,
user_id: &str,
user_id: &Uuid,
) -> Result<AddressBook, DomainError> {
let address_book = self
.address_book_repository
@@ -52,7 +52,7 @@ impl ContactService {
.ok_or_else(|| DomainError::not_found("Address book", "not found"))?;
// Check if user is owner
if address_book.owner_id() == user_id {
if address_book.owner_id() == user_id.to_string() {
return Ok(address_book);
}
@@ -61,7 +61,7 @@ impl ContactService {
.address_book_repository
.get_address_book_shares(address_book_id)
.await?;
if shares.iter().any(|(id, _)| id == user_id) {
if shares.iter().any(|(id, _)| id == &user_id.to_string()) {
return Ok(address_book);
}
@@ -78,7 +78,7 @@ impl ContactService {
async fn check_address_book_write_access(
&self,
address_book_id: &Uuid,
user_id: &str,
user_id: &Uuid,
) -> Result<AddressBook, DomainError> {
let address_book = self
.address_book_repository
@@ -87,7 +87,7 @@ impl ContactService {
.ok_or_else(|| DomainError::not_found("Address book", "not found"))?;
// Check if user is owner
if address_book.owner_id() == user_id {
if address_book.owner_id() == user_id.to_string() {
return Ok(address_book);
}
@@ -98,7 +98,7 @@ impl ContactService {
.await?;
if shares
.iter()
.any(|(id, can_write)| id == user_id && *can_write)
.any(|(id, can_write)| id == &user_id.to_string() && *can_write)
{
return Ok(address_book);
}
@@ -296,7 +296,8 @@ impl AddressBookUseCase for ContactService {
// Check if user has write access to the address book
let address_book = self
.check_address_book_write_access(&id, &update.user_id)
.check_address_book_write_access(&id, &Uuid::parse_str(&update.user_id)
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?)
.await?;
// Apply updates
@@ -327,7 +328,7 @@ impl AddressBookUseCase for ContactService {
async fn delete_address_book(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError> {
let id = Uuid::parse_str(address_book_id)
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
@@ -339,7 +340,7 @@ impl AddressBookUseCase for ContactService {
.await?
.ok_or_else(|| DomainError::not_found("Address book", "not found"))?;
if address_book.owner_id() != user_id {
if address_book.owner_id() != user_id.to_string() {
return Err(DomainError::unauthorized(
"Only the owner can delete an address book",
));
@@ -354,18 +355,18 @@ impl AddressBookUseCase for ContactService {
async fn get_address_book(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<AddressBookDto, DomainError> {
let id = Uuid::parse_str(address_book_id)
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
let address_book = self.check_address_book_access(&id, user_id).await?;
let address_book = self.check_address_book_access(&id, &user_id).await?;
Ok(AddressBookDto::from(address_book))
}
async fn list_user_address_books(
&self,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<AddressBookDto>, DomainError> {
// Get address books owned by the user
let owned_address_books = self
@@ -397,7 +398,7 @@ impl AddressBookUseCase for ContactService {
}
for address_book in public_address_books {
if address_book.owner_id() != user_id
if address_book.owner_id() != user_id.to_string()
&& !address_book_map.contains_key(address_book.id())
{
address_book_map.insert(*address_book.id(), address_book);
@@ -428,7 +429,7 @@ impl AddressBookUseCase for ContactService {
async fn share_address_book(
&self,
dto: ShareAddressBookDto,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError> {
let id = Uuid::parse_str(&dto.address_book_id)
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
@@ -440,21 +441,23 @@ impl AddressBookUseCase for ContactService {
.await?
.ok_or_else(|| DomainError::not_found("Address book", "not found"))?;
if address_book.owner_id() != user_id {
if address_book.owner_id() != user_id.to_string() {
return Err(DomainError::unauthorized(
"Only the owner can share an address book",
));
}
// Don't allow sharing with yourself
if dto.user_id == user_id {
if dto.user_id == user_id.to_string() {
return Err(DomainError::validation_error(
"Cannot share an address book with yourself",
));
}
let target_user_id = Uuid::parse_str(&dto.user_id)
.map_err(|_| DomainError::validation_error("Invalid target user ID format"))?;
self.address_book_repository
.share_address_book(&id, &dto.user_id, dto.can_write)
.share_address_book(&id, target_user_id, dto.can_write)
.await?;
Ok(())
}
@@ -462,7 +465,7 @@ impl AddressBookUseCase for ContactService {
async fn unshare_address_book(
&self,
dto: UnshareAddressBookDto,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError> {
let id = Uuid::parse_str(&dto.address_book_id)
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
@@ -474,14 +477,16 @@ impl AddressBookUseCase for ContactService {
.await?
.ok_or_else(|| DomainError::not_found("Address book", "not found"))?;
if address_book.owner_id() != user_id {
if address_book.owner_id() != user_id.to_string() {
return Err(DomainError::unauthorized(
"Only the owner can unshare an address book",
));
}
let target_user_id = Uuid::parse_str(&dto.user_id)
.map_err(|_| DomainError::validation_error("Invalid target user ID format"))?;
self.address_book_repository
.unshare_address_book(&id, &dto.user_id)
.unshare_address_book(&id, target_user_id)
.await?;
Ok(())
}
@@ -489,7 +494,7 @@ impl AddressBookUseCase for ContactService {
async fn get_address_book_shares(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<(String, bool)>, DomainError> {
let id = Uuid::parse_str(address_book_id)
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
@@ -501,7 +506,7 @@ impl AddressBookUseCase for ContactService {
.await?
.ok_or_else(|| DomainError::not_found("Address book", "not found"))?;
if address_book.owner_id() != user_id {
if address_book.owner_id() != user_id.to_string() {
return Err(DomainError::unauthorized(
"Only the owner can view address book shares",
));
@@ -521,7 +526,8 @@ impl ContactUseCase for ContactService {
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
// Check if user has write access to the address book
self.check_address_book_write_access(&address_book_id, &dto.user_id)
self.check_address_book_write_access(&address_book_id, &Uuid::parse_str(&dto.user_id)
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?)
.await?;
// Convert DTOs to domain entities
@@ -598,7 +604,8 @@ impl ContactUseCase for ContactService {
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
// Check if user has write access to the address book
self.check_address_book_write_access(&address_book_id, &dto.user_id)
self.check_address_book_write_access(&address_book_id, &Uuid::parse_str(&dto.user_id)
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?)
.await?;
// Parse vCard data
@@ -633,7 +640,9 @@ impl ContactUseCase for ContactService {
.ok_or_else(|| DomainError::not_found("Contact", "not found"))?;
// Check if user has write access to the address book
self.check_address_book_write_access(contact.address_book_id(), &update.user_id)
let update_user_id = Uuid::parse_str(&update.user_id)
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?;
self.check_address_book_write_access(contact.address_book_id(), &update_user_id)
.await?;
// Destructure contact into owned parts for updates
@@ -720,7 +729,7 @@ impl ContactUseCase for ContactService {
Ok(ContactDto::from(result))
}
async fn delete_contact(&self, contact_id: &str, user_id: &str) -> Result<(), DomainError> {
async fn delete_contact(&self, contact_id: &str, user_id: Uuid) -> Result<(), DomainError> {
let id = Uuid::parse_str(contact_id)
.map_err(|_| DomainError::validation_error("Invalid contact ID format"))?;
@@ -732,7 +741,7 @@ impl ContactUseCase for ContactService {
.ok_or_else(|| DomainError::not_found("Contact", "not found"))?;
// Check if user has write access to the address book
self.check_address_book_write_access(contact.address_book_id(), user_id)
self.check_address_book_write_access(contact.address_book_id(), &user_id)
.await?;
// Delete the contact
@@ -743,7 +752,7 @@ impl ContactUseCase for ContactService {
async fn get_contact(
&self,
contact_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<ContactDto, DomainError> {
let id = Uuid::parse_str(contact_id)
.map_err(|_| DomainError::validation_error("Invalid contact ID format"))?;
@@ -756,7 +765,7 @@ impl ContactUseCase for ContactService {
.ok_or_else(|| DomainError::not_found("Contact", "not found"))?;
// Check if user has access to the address book
self.check_address_book_access(contact.address_book_id(), user_id)
self.check_address_book_access(contact.address_book_id(), &user_id)
.await?;
Ok(ContactDto::from(contact))
@@ -765,13 +774,13 @@ impl ContactUseCase for ContactService {
async fn list_contacts(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactDto>, DomainError> {
let id = Uuid::parse_str(address_book_id)
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
// Check if user has access to the address book
self.check_address_book_access(&id, user_id).await?;
self.check_address_book_access(&id, &user_id).await?;
// Get contacts
let contacts = self
@@ -787,13 +796,13 @@ impl ContactUseCase for ContactService {
&self,
address_book_id: &str,
query: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactDto>, DomainError> {
let id = Uuid::parse_str(address_book_id)
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
// Check if user has access to the address book
self.check_address_book_access(&id, user_id).await?;
self.check_address_book_access(&id, &user_id).await?;
// Search contacts
let contacts = self.contact_repository.search_contacts(&id, query).await?;
@@ -810,7 +819,8 @@ impl ContactUseCase for ContactService {
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
// Check if user has write access to the address book
self.check_address_book_write_access(&address_book_id, &dto.user_id)
self.check_address_book_write_access(&address_book_id, &Uuid::parse_str(&dto.user_id)
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?)
.await?;
let group = ContactGroup::new(address_book_id, dto.name);
@@ -835,7 +845,8 @@ impl ContactUseCase for ContactService {
.ok_or_else(|| DomainError::not_found("Contact group", "not found"))?;
// Check if user has write access to the address book
self.check_address_book_write_access(group.address_book_id(), &update.user_id)
self.check_address_book_write_access(group.address_book_id(), &Uuid::parse_str(&update.user_id)
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?)
.await?;
// Update the group
@@ -854,7 +865,7 @@ impl ContactUseCase for ContactService {
Ok(ContactGroupDto::from(result))
}
async fn delete_group(&self, group_id: &str, user_id: &str) -> Result<(), DomainError> {
async fn delete_group(&self, group_id: &str, user_id: Uuid) -> Result<(), DomainError> {
let id = Uuid::parse_str(group_id)
.map_err(|_| DomainError::validation_error("Invalid group ID format"))?;
@@ -866,7 +877,7 @@ impl ContactUseCase for ContactService {
.ok_or_else(|| DomainError::not_found("Contact group", "not found"))?;
// Check if user has write access to the address book
self.check_address_book_write_access(group.address_book_id(), user_id)
self.check_address_book_write_access(group.address_book_id(), &user_id)
.await?;
// Delete the group
@@ -877,7 +888,7 @@ impl ContactUseCase for ContactService {
async fn get_group(
&self,
group_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<ContactGroupDto, DomainError> {
let id = Uuid::parse_str(group_id)
.map_err(|_| DomainError::validation_error("Invalid group ID format"))?;
@@ -890,7 +901,7 @@ impl ContactUseCase for ContactService {
.ok_or_else(|| DomainError::not_found("Contact group", "not found"))?;
// Check if user has access to the address book
self.check_address_book_access(group.address_book_id(), user_id)
self.check_address_book_access(group.address_book_id(), &user_id)
.await?;
// Get the number of contacts in the group
@@ -908,13 +919,13 @@ impl ContactUseCase for ContactService {
async fn list_groups(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactGroupDto>, DomainError> {
let id = Uuid::parse_str(address_book_id)
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
// Check if user has access to the address book
self.check_address_book_access(&id, user_id).await?;
self.check_address_book_access(&id, &user_id).await?;
// Get groups
let groups = self
@@ -929,7 +940,7 @@ impl ContactUseCase for ContactService {
async fn add_contact_to_group(
&self,
dto: GroupMembershipDto,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError> {
let group_id = Uuid::parse_str(&dto.group_id)
.map_err(|_| DomainError::validation_error("Invalid group ID format"))?;
@@ -945,7 +956,7 @@ impl ContactUseCase for ContactService {
.ok_or_else(|| DomainError::not_found("Contact group", "not found"))?;
// Check if user has write access to the address book
self.check_address_book_write_access(group.address_book_id(), user_id)
self.check_address_book_write_access(group.address_book_id(), &user_id)
.await?;
// Add contact to group
@@ -958,7 +969,7 @@ impl ContactUseCase for ContactService {
async fn remove_contact_from_group(
&self,
dto: GroupMembershipDto,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError> {
let group_id = Uuid::parse_str(&dto.group_id)
.map_err(|_| DomainError::validation_error("Invalid group ID format"))?;
@@ -974,7 +985,7 @@ impl ContactUseCase for ContactService {
.ok_or_else(|| DomainError::not_found("Contact group", "not found"))?;
// Check if user has write access to the address book
self.check_address_book_write_access(group.address_book_id(), user_id)
self.check_address_book_write_access(group.address_book_id(), &user_id)
.await?;
// Remove contact from group
@@ -987,7 +998,7 @@ impl ContactUseCase for ContactService {
async fn list_contacts_in_group(
&self,
group_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactDto>, DomainError> {
let id = Uuid::parse_str(group_id)
.map_err(|_| DomainError::validation_error("Invalid group ID format"))?;
@@ -1000,7 +1011,7 @@ impl ContactUseCase for ContactService {
.ok_or_else(|| DomainError::not_found("Contact group", "not found"))?;
// Check if user has access to the address book
self.check_address_book_access(group.address_book_id(), user_id)
self.check_address_book_access(group.address_book_id(), &user_id)
.await?;
// Get contacts in group
@@ -1016,7 +1027,7 @@ impl ContactUseCase for ContactService {
async fn list_groups_for_contact(
&self,
contact_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactGroupDto>, DomainError> {
let id = Uuid::parse_str(contact_id)
.map_err(|_| DomainError::validation_error("Invalid contact ID format"))?;
@@ -1029,7 +1040,7 @@ impl ContactUseCase for ContactService {
.ok_or_else(|| DomainError::not_found("Contact", "not found"))?;
// Check if user has access to the address book
self.check_address_book_access(contact.address_book_id(), user_id)
self.check_address_book_access(contact.address_book_id(), &user_id)
.await?;
// Get groups for contact
@@ -1045,7 +1056,7 @@ impl ContactUseCase for ContactService {
async fn get_contact_vcard(
&self,
contact_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<String, DomainError> {
let id = Uuid::parse_str(contact_id)
.map_err(|_| DomainError::validation_error("Invalid contact ID format"))?;
@@ -1058,7 +1069,7 @@ impl ContactUseCase for ContactService {
.ok_or_else(|| DomainError::not_found("Contact", "not found"))?;
// Check if user has access to the address book
self.check_address_book_access(contact.address_book_id(), user_id)
self.check_address_book_access(contact.address_book_id(), &user_id)
.await?;
// Return the vCard data
@@ -1068,13 +1079,13 @@ impl ContactUseCase for ContactService {
async fn get_contacts_as_vcards(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<(String, String)>, DomainError> {
let id = Uuid::parse_str(address_book_id)
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
// Check if user has access to the address book
self.check_address_book_access(&id, user_id).await?;
self.check_address_book_access(&id, &user_id).await?;
// Get all contacts in the address book
let contacts = self
@@ -1130,6 +1141,8 @@ impl StorageUseCase for ContactService {
let user_id = params["user_id"]
.as_str()
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
let user_id = Uuid::parse_str(user_id)
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
self.delete_address_book(address_book_id, user_id).await?;
Ok(serde_json::Value::Null)
@@ -1142,6 +1155,8 @@ impl StorageUseCase for ContactService {
let user_id = params["user_id"]
.as_str()
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
let user_id = Uuid::parse_str(user_id)
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
let result = self.get_address_book(address_book_id, user_id).await?;
Ok(serde_json::to_value(result).unwrap())
@@ -1150,6 +1165,8 @@ impl StorageUseCase for ContactService {
let user_id = params["user_id"]
.as_str()
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
let user_id = Uuid::parse_str(user_id)
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
let result = self.list_user_address_books(user_id).await?;
Ok(serde_json::to_value(result).unwrap())
@@ -1167,6 +1184,8 @@ impl StorageUseCase for ContactService {
let user_id = params["user_id"]
.as_str()
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
let user_id = Uuid::parse_str(user_id)
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
self.share_address_book(dto, user_id).await?;
Ok(serde_json::Value::Null)
@@ -1180,6 +1199,8 @@ impl StorageUseCase for ContactService {
let user_id = params["user_id"]
.as_str()
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
let user_id = Uuid::parse_str(user_id)
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
self.unshare_address_book(dto, user_id).await?;
Ok(serde_json::Value::Null)
@@ -1192,6 +1213,8 @@ impl StorageUseCase for ContactService {
let user_id = params["user_id"]
.as_str()
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
let user_id = Uuid::parse_str(user_id)
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
let result = self
.get_address_book_shares(address_book_id, user_id)
@@ -1239,6 +1262,8 @@ impl StorageUseCase for ContactService {
let user_id = params["user_id"]
.as_str()
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
let user_id = Uuid::parse_str(user_id)
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
self.delete_contact(contact_id, user_id).await?;
Ok(serde_json::Value::Null)
@@ -1251,6 +1276,8 @@ impl StorageUseCase for ContactService {
let user_id = params["user_id"]
.as_str()
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
let user_id = Uuid::parse_str(user_id)
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
let result = self.get_contact(contact_id, user_id).await?;
Ok(serde_json::to_value(result).unwrap())
@@ -1263,6 +1290,8 @@ impl StorageUseCase for ContactService {
let user_id = params["user_id"]
.as_str()
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
let user_id = Uuid::parse_str(user_id)
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
let result = self.list_contacts(address_book_id, user_id).await?;
Ok(serde_json::to_value(result).unwrap())
@@ -1279,6 +1308,8 @@ impl StorageUseCase for ContactService {
let user_id = params["user_id"]
.as_str()
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
let user_id = Uuid::parse_str(user_id)
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
let result = self
.search_contacts(address_book_id, query, user_id)
@@ -1317,6 +1348,8 @@ impl StorageUseCase for ContactService {
let user_id = params["user_id"]
.as_str()
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
let user_id = Uuid::parse_str(user_id)
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
self.delete_group(group_id, user_id).await?;
Ok(serde_json::Value::Null)
@@ -1329,6 +1362,8 @@ impl StorageUseCase for ContactService {
let user_id = params["user_id"]
.as_str()
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
let user_id = Uuid::parse_str(user_id)
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
let result = self.get_group(group_id, user_id).await?;
Ok(serde_json::to_value(result).unwrap())
@@ -1341,6 +1376,8 @@ impl StorageUseCase for ContactService {
let user_id = params["user_id"]
.as_str()
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
let user_id = Uuid::parse_str(user_id)
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
let result = self.list_groups(address_book_id, user_id).await?;
Ok(serde_json::to_value(result).unwrap())
@@ -1356,6 +1393,8 @@ impl StorageUseCase for ContactService {
let user_id = params["user_id"]
.as_str()
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
let user_id = Uuid::parse_str(user_id)
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
self.add_contact_to_group(dto, user_id).await?;
Ok(serde_json::Value::Null)
@@ -1369,6 +1408,8 @@ impl StorageUseCase for ContactService {
let user_id = params["user_id"]
.as_str()
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
let user_id = Uuid::parse_str(user_id)
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
self.remove_contact_from_group(dto, user_id).await?;
Ok(serde_json::Value::Null)
@@ -1381,6 +1422,8 @@ impl StorageUseCase for ContactService {
let user_id = params["user_id"]
.as_str()
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
let user_id = Uuid::parse_str(user_id)
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
let result = self.list_contacts_in_group(group_id, user_id).await?;
Ok(serde_json::to_value(result).unwrap())
@@ -1393,6 +1436,8 @@ impl StorageUseCase for ContactService {
let user_id = params["user_id"]
.as_str()
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
let user_id = Uuid::parse_str(user_id)
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
let result = self.list_groups_for_contact(contact_id, user_id).await?;
Ok(serde_json::to_value(result).unwrap())
@@ -1407,6 +1452,8 @@ impl StorageUseCase for ContactService {
let user_id = params["user_id"]
.as_str()
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
let user_id = Uuid::parse_str(user_id)
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
let result = self.get_contact_vcard(contact_id, user_id).await?;
Ok(serde_json::to_value(result).unwrap())
@@ -1419,6 +1466,8 @@ impl StorageUseCase for ContactService {
let user_id = params["user_id"]
.as_str()
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
let user_id = Uuid::parse_str(user_id)
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
let result = self
.get_contacts_as_vcards(address_book_id, user_id)
@@ -22,6 +22,7 @@ use crate::infrastructure::repositories::pg::DeviceCodePgRepository;
use crate::infrastructure::repositories::pg::SessionPgRepository;
use crate::infrastructure::repositories::pg::UserPgRepository;
use crate::infrastructure::services::jwt_service::JwtTokenService;
use uuid::Uuid;
/// Default device code lifetime: 15 minutes (RFC 8628 recommends 5-30 min).
const DEVICE_CODE_LIFETIME_SECS: i64 = 900;
@@ -155,7 +156,7 @@ impl DeviceAuthService {
///
/// * `user_code` — the code from the verification page
/// * `user_id` — the authenticated user's ID (from session/JWT)
pub async fn approve(&self, user_code: &str, user_id: &str) -> Result<(), DomainError> {
pub async fn approve(&self, user_code: &str, user_id: Uuid) -> Result<(), DomainError> {
let normalized = user_code.trim().to_uppercase().replace(' ', "");
let mut dc = self
@@ -180,7 +181,7 @@ impl DeviceAuthService {
// Persist refresh token as a session
let session = Session::new(
user_id.to_string(),
user_id,
refresh_token.clone(),
None, // ip_address
Some(format!("device:{}", dc.client_name())), // user_agent
@@ -189,7 +190,7 @@ impl DeviceAuthService {
self.session_storage.create_session(session).await?;
// Store tokens on the device code entity
dc.authorize(user_id.to_string(), access_token, refresh_token);
dc.authorize(user_id, access_token, refresh_token);
self.device_code_storage.update_device_code(dc).await?;
tracing::info!(
@@ -297,7 +298,7 @@ impl DeviceAuthService {
pub async fn list_user_devices(
&self,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<DeviceInfoDto>, DomainError> {
let codes = self.device_code_storage.list_by_user(user_id).await?;
Ok(codes
@@ -318,7 +319,7 @@ impl DeviceAuthService {
// 8. Revoke — user revokes a device authorization
// ========================================================================
pub async fn revoke_device(&self, device_id: &str, user_id: &str) -> Result<(), DomainError> {
pub async fn revoke_device(&self, device_id: Uuid, user_id: Uuid) -> Result<(), DomainError> {
// Verify ownership before deleting
let devices = self.device_code_storage.list_by_user(user_id).await?;
let found = devices.iter().any(|d| d.id() == device_id);
@@ -2,6 +2,7 @@ use std::collections::HashSet;
use std::sync::Arc;
use tracing::info;
use uuid::Uuid;
use crate::application::dtos::favorites_dto::{
BatchFavoritesResult, BatchFavoritesStats, FavoriteItemDto,
@@ -27,7 +28,7 @@ impl FavoritesService {
impl FavoritesUseCase for FavoritesService {
/// Get all favorites for a user
async fn get_favorites(&self, user_id: &str) -> Result<Vec<FavoriteItemDto>> {
async fn get_favorites(&self, user_id: Uuid) -> Result<Vec<FavoriteItemDto>> {
info!("Getting favorites for user: {}", user_id);
let favorites = self.repo.get_favorites(user_id).await?;
info!(
@@ -39,7 +40,7 @@ impl FavoritesUseCase for FavoritesService {
}
/// Add an item to user's favorites
async fn add_to_favorites(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()> {
async fn add_to_favorites(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<()> {
info!(
"Adding {} '{}' to favorites for user {}",
item_type, item_id, user_id
@@ -64,7 +65,7 @@ impl FavoritesUseCase for FavoritesService {
/// Remove an item from user's favorites
async fn remove_from_favorites(
&self,
user_id: &str,
user_id: Uuid,
item_id: &str,
item_type: &str,
) -> Result<bool> {
@@ -91,7 +92,7 @@ impl FavoritesUseCase for FavoritesService {
}
/// Check if an item is in user's favorites
async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool> {
async fn is_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<bool> {
info!(
"Checking if {} '{}' is favorite for user {}",
item_type, item_id, user_id
@@ -101,7 +102,7 @@ impl FavoritesUseCase for FavoritesService {
async fn batch_add_to_favorites(
&self,
user_id: &str,
user_id: Uuid,
items: &[(String, String)],
) -> Result<BatchFavoritesResult> {
info!(
@@ -148,7 +149,7 @@ impl FavoritesUseCase for FavoritesService {
async fn batch_check_favorites(
&self,
user_id: &str,
user_id: Uuid,
item_ids: &[(&str, &str)],
) -> Result<HashSet<String>> {
self.repo.batch_check_favorites(user_id, item_ids).await
@@ -9,6 +9,7 @@ use crate::common::errors::DomainError;
use crate::infrastructure::repositories::pg::file_blob_read_repository::FileBlobReadRepository;
use crate::infrastructure::repositories::pg::file_blob_write_repository::FileBlobWriteRepository;
use tracing::{error, info, warn};
use uuid::Uuid;
/// Service for file management operations (move, delete).
///
@@ -46,7 +47,7 @@ impl FileManagementService {
}
/// Verifies ownership via the read repository.
async fn verify_owner(&self, file_id: &str, caller_id: &str) -> Result<(), DomainError> {
async fn verify_owner(&self, file_id: &str, caller_id: Uuid) -> Result<(), DomainError> {
if let Some(read) = &self.file_read {
read.verify_file_owner(file_id, caller_id).await
} else {
@@ -92,7 +93,7 @@ impl FileManagementUseCase for FileManagementService {
async fn move_file_owned(
&self,
file_id: &str,
caller_id: &str,
caller_id: Uuid,
folder_id: Option<String>,
) -> Result<FileDto, DomainError> {
self.verify_owner(file_id, caller_id).await?;
@@ -131,7 +132,7 @@ impl FileManagementUseCase for FileManagementService {
async fn copy_file_owned(
&self,
file_id: &str,
caller_id: &str,
caller_id: Uuid,
target_folder_id: Option<String>,
) -> Result<FileDto, DomainError> {
self.verify_owner(file_id, caller_id).await?;
@@ -162,7 +163,7 @@ impl FileManagementUseCase for FileManagementService {
async fn rename_file_owned(
&self,
file_id: &str,
caller_id: &str,
caller_id: Uuid,
new_name: &str,
) -> Result<FileDto, DomainError> {
self.verify_owner(file_id, caller_id).await?;
@@ -173,7 +174,7 @@ impl FileManagementUseCase for FileManagementService {
self.file_repository.delete_file(id).await
}
async fn delete_file_owned(&self, id: &str, caller_id: &str) -> Result<(), DomainError> {
async fn delete_file_owned(&self, id: &str, caller_id: Uuid) -> Result<(), DomainError> {
self.verify_owner(id, caller_id).await?;
self.delete_file(id).await
}
@@ -184,7 +185,7 @@ impl FileManagementUseCase for FileManagementService {
/// `trg_files_decrement_blob_ref` which fires on DELETE FROM storage.files.
/// We do NOT decrement here — trashing is a soft-delete (UPDATE, not DELETE)
/// so the blob must remain referenced until the file is permanently deleted.
async fn delete_with_cleanup(&self, id: &str, user_id: &str) -> Result<bool, DomainError> {
async fn delete_with_cleanup(&self, id: &str, user_id: Uuid) -> Result<bool, DomainError> {
// Step 1: Try trash (soft delete — file row stays, blob stays referenced)
if let Some(trash) = &self.trash_service {
info!("Moving file to trash: {}", id);
@@ -13,6 +13,7 @@ use crate::infrastructure::services::image_transcode_service::{
ImageTranscodeService, OutputFormat,
};
use tracing::{debug, info};
use uuid::Uuid;
/// Threshold below which files are served from RAM cache (10 MB).
const CACHE_THRESHOLD: u64 = 10 * 1024 * 1024;
@@ -201,7 +202,7 @@ impl FileRetrievalUseCase for FileRetrievalService {
Ok(FileDto::from(file))
}
async fn get_file_owned(&self, id: &str, caller_id: &str) -> Result<FileDto, DomainError> {
async fn get_file_owned(&self, id: &str, caller_id: Uuid) -> Result<FileDto, DomainError> {
let file = self.file_read.get_file_for_owner(id, caller_id).await?;
Ok(FileDto::from(file))
}
@@ -226,7 +227,7 @@ impl FileRetrievalUseCase for FileRetrievalService {
async fn list_files_owned(
&self,
folder_id: Option<&str>,
owner_id: &str,
owner_id: Uuid,
) -> Result<Vec<FileDto>, DomainError> {
let files = self
.file_read
@@ -245,7 +246,7 @@ impl FileRetrievalUseCase for FileRetrievalService {
async fn get_file_stream_owned(
&self,
id: &str,
caller_id: &str,
caller_id: Uuid,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
self.file_read.verify_file_owner(id, caller_id).await?;
self.file_read.get_file_stream(id).await
@@ -267,7 +268,7 @@ impl FileRetrievalUseCase for FileRetrievalService {
async fn get_file_optimized_owned(
&self,
id: &str,
caller_id: &str,
caller_id: Uuid,
accept_webp: bool,
prefer_original: bool,
) -> Result<(FileDto, OptimizedFileContent), DomainError> {
@@ -302,7 +303,7 @@ impl FileRetrievalUseCase for FileRetrievalService {
async fn get_file_range_stream_owned(
&self,
id: &str,
caller_id: &str,
caller_id: Uuid,
start: u64,
end: Option<u64>,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
@@ -336,7 +337,7 @@ impl FileRetrievalUseCase for FileRetrievalService {
async fn list_files_batch_for_owner(
&self,
folder_id: Option<&str>,
owner_id: &str,
owner_id: Uuid,
offset: i64,
limit: i64,
) -> Result<Vec<FileDto>, DomainError> {
+31 -23
View File
@@ -7,6 +7,7 @@ use crate::domain::repositories::folder_repository::FolderRepository;
use crate::domain::services::path_service::StoragePath;
use crate::infrastructure::repositories::pg::folder_db_repository::FolderDbRepository;
use std::sync::Arc;
use uuid::Uuid;
/// Implementation of the use case for folder operations
pub struct FolderService {
@@ -35,7 +36,7 @@ impl FolderService {
async fn get_folder_owned(
&self,
_id: &str,
_caller_id: &str,
_caller_id: Uuid,
) -> Result<FolderDto, DomainError> {
Ok(FolderDto::empty())
}
@@ -54,7 +55,7 @@ impl FolderService {
async fn list_folders_for_owner(
&self,
_parent_id: Option<&str>,
_owner_id: &str,
_owner_id: Uuid,
) -> Result<Vec<FolderDto>, DomainError> {
Ok(vec![])
}
@@ -80,7 +81,7 @@ impl FolderService {
async fn list_folders_for_owner_paginated(
&self,
_parent_id: Option<&str>,
_owner_id: &str,
_owner_id: Uuid,
_pagination: &crate::application::dtos::pagination::PaginationRequestDto,
) -> Result<
crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>,
@@ -100,7 +101,7 @@ impl FolderService {
&self,
_id: &str,
_dto: RenameFolderDto,
_caller_id: &str,
_caller_id: Uuid,
) -> Result<FolderDto, DomainError> {
Ok(FolderDto::empty())
}
@@ -109,18 +110,18 @@ impl FolderService {
&self,
_id: &str,
_dto: MoveFolderDto,
_caller_id: &str,
_caller_id: Uuid,
) -> Result<FolderDto, DomainError> {
Ok(FolderDto::empty())
}
async fn delete_folder(&self, _id: &str, _caller_id: &str) -> Result<(), DomainError> {
async fn delete_folder(&self, _id: &str, _caller_id: Uuid) -> Result<(), DomainError> {
Ok(())
}
async fn create_home_folder(
&self,
_user_id: &str,
_user_id: Uuid,
_name: String,
) -> Result<FolderDto, DomainError> {
Ok(FolderDto::empty())
@@ -170,12 +171,13 @@ impl FolderUseCase for FolderService {
/// Creates a root-level home folder for a user during registration.
async fn create_home_folder(
&self,
user_id: &str,
user_id: Uuid,
name: String,
) -> Result<FolderDto, DomainError> {
let user_id_str = user_id.to_string();
let folder = self
.folder_storage
.create_home_folder(user_id, name)
.create_home_folder(&user_id_str, name)
.await
.map_err(|e| {
DomainError::internal_error(
@@ -205,9 +207,10 @@ impl FolderUseCase for FolderService {
}
/// Gets a folder by its ID, enforcing that `caller_id` is the owner.
async fn get_folder_owned(&self, id: &str, caller_id: &str) -> Result<FolderDto, DomainError> {
async fn get_folder_owned(&self, id: &str, caller_id: Uuid) -> Result<FolderDto, DomainError> {
let caller_id_str = caller_id.to_string();
let folder_dto = self.get_folder(id).await?;
if folder_dto.owner_id.as_deref() != Some(caller_id) {
if folder_dto.owner_id.as_deref() != Some(caller_id_str.as_str()) {
tracing::warn!(
"get_folder_owned: user '{}' attempted to access folder '{}' owned by '{:?}'",
caller_id,
@@ -260,11 +263,12 @@ impl FolderUseCase for FolderService {
async fn list_folders_for_owner(
&self,
parent_id: Option<&str>,
owner_id: &str,
owner_id: Uuid,
) -> Result<Vec<FolderDto>, DomainError> {
let owner_id_str = owner_id.to_string();
let folders = self
.folder_storage
.list_folders_by_owner(parent_id, owner_id)
.list_folders_by_owner(parent_id, &owner_id_str)
.await
.map_err(|e| {
DomainError::internal_error(
@@ -283,10 +287,10 @@ impl FolderUseCase for FolderService {
"No root folders found for user {}, creating home folder automatically",
owner_id
);
let folder_name = format!("My Folder - {}", &owner_id[..8.min(owner_id.len())]);
let folder_name = format!("My Folder - {}", &owner_id_str[..8.min(owner_id_str.len())]);
match self
.folder_storage
.create_home_folder(owner_id, folder_name.clone())
.create_home_folder(&owner_id_str, folder_name.clone())
.await
{
Ok(home_folder) => {
@@ -346,17 +350,18 @@ impl FolderUseCase for FolderService {
async fn list_folders_for_owner_paginated(
&self,
parent_id: Option<&str>,
owner_id: &str,
owner_id: Uuid,
pagination: &crate::application::dtos::pagination::PaginationRequestDto,
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>
{
let owner_id_str = owner_id.to_string();
let pagination = pagination.validate_and_adjust();
let (folders, total_items) = self
.folder_storage
.list_folders_by_owner_paginated(
parent_id,
owner_id,
&owner_id_str,
pagination.offset(),
pagination.limit(),
true,
@@ -389,8 +394,9 @@ impl FolderUseCase for FolderService {
&self,
id: &str,
dto: RenameFolderDto,
caller_id: &str,
caller_id: Uuid,
) -> Result<FolderDto, DomainError> {
let caller_id_str = caller_id.to_string();
// Input validation
if dto.name.is_empty() {
return Err(DomainError::new(
@@ -408,7 +414,7 @@ impl FolderUseCase for FolderService {
)
})?;
if existing_folder.owner_id() != Some(caller_id) {
if existing_folder.owner_id() != Some(caller_id_str.as_str()) {
tracing::warn!(
"rename_folder: user '{}' attempted to rename folder '{}' owned by '{:?}'",
caller_id,
@@ -438,8 +444,9 @@ impl FolderUseCase for FolderService {
&self,
id: &str,
dto: MoveFolderDto,
caller_id: &str,
caller_id: Uuid,
) -> Result<FolderDto, DomainError> {
let caller_id_str = caller_id.to_string();
// Verify the source folder exists and belongs to the caller
let source_folder = self.folder_storage.get_folder(id).await.map_err(|e| {
DomainError::internal_error(
@@ -448,7 +455,7 @@ impl FolderUseCase for FolderService {
)
})?;
if source_folder.owner_id() != Some(caller_id) {
if source_folder.owner_id() != Some(caller_id_str.as_str()) {
tracing::warn!(
"move_folder: user '{}' attempted to move folder '{}' owned by '{:?}'",
caller_id,
@@ -495,7 +502,8 @@ impl FolderUseCase for FolderService {
}
/// Deletes a folder after verifying ownership.
async fn delete_folder(&self, id: &str, caller_id: &str) -> Result<(), DomainError> {
async fn delete_folder(&self, id: &str, caller_id: Uuid) -> Result<(), DomainError> {
let caller_id_str = caller_id.to_string();
// Verify the folder exists and belongs to the caller
let folder = self.folder_storage.get_folder(id).await.map_err(|e| {
DomainError::internal_error(
@@ -504,7 +512,7 @@ impl FolderUseCase for FolderService {
)
})?;
if folder.owner_id() != Some(caller_id) {
if folder.owner_id() != Some(caller_id_str.as_str()) {
tracing::warn!(
"delete_folder: user '{}' attempted to delete folder '{}' owned by '{:?}'",
caller_id,
+5 -4
View File
@@ -4,6 +4,7 @@ use crate::common::errors::{DomainError, ErrorKind, Result};
use crate::infrastructure::repositories::pg::RecentItemsPgRepository;
use std::sync::Arc;
use tracing::info;
use uuid::Uuid;
/// Implementation of the use case for managing recent items.
///
@@ -28,7 +29,7 @@ impl RecentItemsUseCase for RecentService {
/// Get recent items for a user
async fn get_recent_items(
&self,
user_id: &str,
user_id: Uuid,
limit: Option<i32>,
) -> Result<Vec<RecentItemDto>> {
info!("Getting recent items for user: {}", user_id);
@@ -47,7 +48,7 @@ impl RecentItemsUseCase for RecentService {
/// Record access to an item
async fn record_item_access(
&self,
user_id: &str,
user_id: Uuid,
item_id: &str,
item_type: &str,
) -> Result<()> {
@@ -77,7 +78,7 @@ impl RecentItemsUseCase for RecentService {
/// Remove an item from recent
async fn remove_from_recent(
&self,
user_id: &str,
user_id: Uuid,
item_id: &str,
item_type: &str,
) -> Result<bool> {
@@ -101,7 +102,7 @@ impl RecentItemsUseCase for RecentService {
}
/// Clear all recent items
async fn clear_recent_items(&self, user_id: &str) -> Result<()> {
async fn clear_recent_items(&self, user_id: Uuid) -> Result<()> {
info!("Clearing all recent items for user {}", user_id);
self.repo.clear_all(user_id).await?;
info!("Cleared all recent items for user {}", user_id);
+7 -5
View File
@@ -19,6 +19,7 @@ use crate::domain::repositories::folder_repository::FolderRepository;
use crate::infrastructure::repositories::pg::file_blob_read_repository::FileBlobReadRepository;
use crate::infrastructure::repositories::pg::folder_db_repository::FolderDbRepository;
use std::hash::{Hash, Hasher};
use uuid::Uuid;
/**
* High-performance search service implementation for files and folders.
@@ -285,12 +286,13 @@ impl SearchUseCase for SearchService {
async fn search(
&self,
criteria: SearchCriteriaDto,
user_id: &str,
user_id: Uuid,
) -> Result<Arc<SearchResultsDto>> {
let start = Instant::now();
let user_id_str = user_id.to_string();
// Try to get from cache
let cache_key = Self::create_cache_key(&criteria, user_id);
let cache_key = Self::create_cache_key(&criteria, &user_id_str);
if let Some(cached_results) = self.get_from_cache(cache_key).await {
return Ok(cached_results);
}
@@ -321,7 +323,7 @@ impl SearchUseCase for SearchService {
.search_folders(
criteria.folder_id.as_deref(),
criteria.name_contains.as_deref(),
user_id,
&user_id_str,
false,
)
.await?;
@@ -403,7 +405,7 @@ impl SearchUseCase for SearchService {
.search_folders(
criteria.folder_id.as_deref(),
criteria.name_contains.as_deref(),
user_id,
&user_id_str,
true,
)
.await?;
@@ -504,7 +506,7 @@ impl SearchService {
async fn search(
&self,
_criteria: SearchCriteriaDto,
_user_id: &str,
_user_id: Uuid,
) -> Result<Arc<SearchResultsDto>> {
Ok(Arc::new(SearchResultsDto::empty()))
}
+33 -30
View File
@@ -2,6 +2,7 @@ use std::sync::Arc;
use thiserror::Error;
use tokio::sync::Semaphore;
use uuid::Uuid;
use crate::domain::repositories::folder_repository::FolderRepository;
use crate::infrastructure::repositories::pg::SharePgRepository;
@@ -150,7 +151,7 @@ impl ShareService {
/// but belongs to a different user — this prevents share-ID enumeration
/// attacks where an attacker probes IDs and uses 403-vs-404 to learn
/// which ones are valid.
async fn fetch_owned_share(&self, id: &str, requester_id: &str) -> Result<Share, DomainError> {
async fn fetch_owned_share(&self, id: Uuid, requester_id: Uuid) -> Result<Share, DomainError> {
let share = self
.share_repository
.find_share_by_id_for_user(id, requester_id)
@@ -163,7 +164,7 @@ impl ShareService {
impl ShareUseCase for ShareService {
async fn create_shared_link(
&self,
user_id: &str,
user_id: Uuid,
dto: CreateShareDto,
) -> Result<ShareDto, DomainError> {
// Convert the item type
@@ -187,7 +188,7 @@ impl ShareUseCase for ShareService {
dto.item_id.clone(),
dto.item_name.clone(),
item_type,
user_id.to_string(),
user_id,
permissions,
password_hash,
dto.expires_at,
@@ -205,7 +206,7 @@ impl ShareUseCase for ShareService {
Ok(ShareDto::from_entity(&saved_share, &self.config.base_url()))
}
async fn get_shared_link(&self, id: &str, requester_id: &str) -> Result<ShareDto, DomainError> {
async fn get_shared_link(&self, id: Uuid, requester_id: Uuid) -> Result<ShareDto, DomainError> {
// SECURITY: ownership-verified lookup — returns 404 if the share
// doesn't exist OR belongs to another user.
let share = self.fetch_owned_share(id, requester_id).await?;
@@ -253,7 +254,7 @@ impl ShareUseCase for ShareService {
&self,
item_id: &str,
item_type: &ShareItemType,
requester_id: &str,
requester_id: Uuid,
) -> Result<Vec<ShareDto>, DomainError> {
// SECURITY: only return shares created by the requester
let shares = self
@@ -276,8 +277,8 @@ impl ShareUseCase for ShareService {
async fn update_shared_link(
&self,
id: &str,
requester_id: &str,
id: Uuid,
requester_id: Uuid,
dto: UpdateShareDto,
) -> Result<ShareDto, DomainError> {
// SECURITY: ownership-verified lookup — prevents IDOR
@@ -322,7 +323,7 @@ impl ShareUseCase for ShareService {
))
}
async fn delete_shared_link(&self, id: &str, requester_id: &str) -> Result<(), DomainError> {
async fn delete_shared_link(&self, id: Uuid, requester_id: Uuid) -> Result<(), DomainError> {
// SECURITY: ownership-verified delete — only the creator can remove
self.share_repository
.delete_share_for_user(id, requester_id)
@@ -333,7 +334,7 @@ impl ShareUseCase for ShareService {
async fn get_user_shared_links(
&self,
user_id: &str,
user_id: Uuid,
page: usize,
per_page: usize,
) -> Result<PaginatedResponseDto<ShareDto>, DomainError> {
@@ -519,7 +520,7 @@ mod tests {
{
async fn create_shared_link(
&self,
user_id: &str,
user_id: Uuid,
dto: CreateShareDto,
) -> Result<ShareDto, DomainError> {
let item_type = ShareItemType::try_from(dto.item_type.as_str())
@@ -534,7 +535,7 @@ mod tests {
dto.item_id.clone(),
dto.item_name.clone(),
item_type,
user_id.to_string(),
user_id,
permissions,
password_hash,
dto.expires_at,
@@ -550,8 +551,8 @@ mod tests {
async fn get_shared_link(
&self,
id: &str,
requester_id: &str,
id: Uuid,
requester_id: Uuid,
) -> Result<ShareDto, DomainError> {
let share = self
.share_repository
@@ -584,7 +585,7 @@ mod tests {
&self,
item_id: &str,
item_type: &ShareItemType,
requester_id: &str,
requester_id: Uuid,
) -> Result<Vec<ShareDto>, DomainError> {
let shares = self
.share_repository
@@ -600,8 +601,8 @@ mod tests {
async fn update_shared_link(
&self,
id: &str,
requester_id: &str,
id: Uuid,
requester_id: Uuid,
dto: UpdateShareDto,
) -> Result<ShareDto, DomainError> {
let mut share = self
@@ -635,8 +636,8 @@ mod tests {
async fn delete_shared_link(
&self,
id: &str,
requester_id: &str,
id: Uuid,
requester_id: Uuid,
) -> Result<(), DomainError> {
self.share_repository
.delete_share_for_user(id, requester_id)
@@ -647,7 +648,7 @@ mod tests {
async fn get_user_shared_links(
&self,
user_id: &str,
user_id: Uuid,
page: usize,
per_page: usize,
) -> Result<PaginatedResponseDto<ShareDto>, DomainError> {
@@ -1018,28 +1019,30 @@ mod tests {
async fn find_share_by_id_for_user(
&self,
id: &str,
user_id: &str,
id: Uuid,
user_id: Uuid,
) -> Result<Share, DomainError> {
let shares = self.shares.lock().unwrap();
let id_str = id.to_string();
shares
.get(id)
.get(&id_str)
.filter(|s| s.created_by() == user_id)
.cloned()
.ok_or_else(|| DomainError::not_found("Share", id))
.ok_or_else(|| DomainError::not_found("Share", &id_str))
}
async fn delete_share_for_user(&self, id: &str, user_id: &str) -> Result<(), DomainError> {
async fn delete_share_for_user(&self, id: Uuid, user_id: Uuid) -> Result<(), DomainError> {
let mut shares = self.shares.lock().unwrap();
let mut tokens = self.tokens.lock().unwrap();
let id_str = id.to_string();
let share = shares
.get(id)
.get(&id_str)
.filter(|s| s.created_by() == user_id)
.ok_or_else(|| DomainError::not_found("Share", id))?;
.ok_or_else(|| DomainError::not_found("Share", &id_str))?;
tokens.remove(share.token());
shares.remove(id);
shares.remove(&id_str);
Ok(())
}
@@ -1047,7 +1050,7 @@ mod tests {
&self,
item_id: &str,
item_type: &ShareItemType,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<Share>, DomainError> {
let shares = self.shares.lock().unwrap();
let type_str = item_type.to_string();
@@ -1078,7 +1081,7 @@ mod tests {
async fn find_shares_by_user(
&self,
user_id: &str,
user_id: Uuid,
offset: usize,
limit: usize,
) -> Result<(Vec<Share>, usize), DomainError> {
@@ -1125,7 +1128,7 @@ mod tests {
}),
};
let result = service.create_shared_link("user123", dto).await;
let result = service.create_shared_link(Uuid::new_v4(), dto).await;
assert!(result.is_ok());
let share_dto = result.unwrap();
@@ -6,6 +6,7 @@ use sqlx::PgPool;
use std::sync::Arc;
use tokio::task;
use tracing::{debug, error, info};
use uuid::Uuid;
/**
* Service for managing and updating user storage usage statistics.
@@ -31,7 +32,7 @@ impl StorageUsageService {
}
/// Calculates and updates storage usage for a specific user
pub async fn update_user_storage_usage(&self, user_id: &str) -> Result<i64, DomainError> {
pub async fn update_user_storage_usage(&self, user_id: Uuid) -> Result<i64, DomainError> {
info!("Updating storage usage for user: {}", user_id);
// Calculate storage usage directly from database
@@ -52,12 +53,11 @@ impl StorageUsageService {
/// Calculates a user's storage usage by summing all their file sizes.
/// Uses a direct SQL query for O(1) performance.
async fn calculate_user_storage_usage(&self, user_id: &str) -> Result<i64, DomainError> {
async fn calculate_user_storage_usage(&self, user_id: Uuid) -> Result<i64, DomainError> {
debug!("Calculating storage for user: {}", user_id);
// Direct SQL query to sum all file sizes for this user
// This is much more efficient than recursively walking folders
// Note: user_id is stored as varchar, not uuid, so we bind it directly as text
let total_size: i64 = sqlx::query_scalar(
r#"
SELECT COALESCE(SUM(size), 0)::bigint
@@ -88,14 +88,14 @@ impl StorageUsageService {
info!("Updating storage usage for username: {}", username);
let user = self.user_repository.get_user_by_username(username).await?;
let user_id = user.id().to_string();
let user_id = user.id();
// Reuse the existing calculation logic
let total_usage = self.calculate_user_storage_usage(&user_id).await?;
let total_usage = self.calculate_user_storage_usage(user_id).await?;
// Update the user's storage usage in the database
self.user_repository
.update_storage_usage(&user_id, total_usage)
.update_storage_usage(user_id, total_usage)
.await?;
info!(
@@ -112,7 +112,7 @@ impl StorageUsageService {
* to the application layer.
*/
impl StorageUsagePort for StorageUsageService {
async fn update_user_storage_usage(&self, user_id: &str) -> Result<i64, DomainError> {
async fn update_user_storage_usage(&self, user_id: Uuid) -> Result<i64, DomainError> {
StorageUsageService::update_user_storage_usage(self, user_id).await
}
@@ -133,12 +133,12 @@ impl StorageUsagePort for StorageUsageService {
// Process users in parallel
for user in users {
let user_id = user.id().to_string();
let user_id = user.id();
let service_clone = self.clone();
// Spawn a background task for each user
let task = task::spawn(async move {
match service_clone.update_user_storage_usage(&user_id).await {
match service_clone.update_user_storage_usage(user_id).await {
Ok(usage) => {
debug!(
"Updated storage usage for user {}: {} bytes",
@@ -168,7 +168,7 @@ impl StorageUsagePort for StorageUsageService {
async fn check_storage_quota(
&self,
user_id: &str,
user_id: Uuid,
additional_bytes: u64,
) -> Result<(), DomainError> {
let user = self.user_repository.get_user_by_id(user_id).await?;
@@ -206,7 +206,7 @@ impl StorageUsagePort for StorageUsageService {
Ok(())
}
async fn get_user_storage_info(&self, user_id: &str) -> Result<(i64, i64), DomainError> {
async fn get_user_storage_info(&self, user_id: Uuid) -> Result<(i64, i64), DomainError> {
let user = self.user_repository.get_user_by_id(user_id).await?;
Ok((user.storage_used_bytes(), user.storage_quota_bytes()))
}
+11 -54
View File
@@ -123,13 +123,10 @@ impl TrashService {
impl TrashUseCase for TrashService {
#[instrument(skip(self))]
async fn get_trash_items(&self, user_id: &str) -> Result<Vec<TrashedItemDto>> {
async fn get_trash_items(&self, user_id: Uuid) -> Result<Vec<TrashedItemDto>> {
debug!("Getting trash items for user: {}", user_id);
let user_uuid = Uuid::parse_str(user_id)
.map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?;
let items = self.trash_repository.get_trash_items(&user_uuid).await?;
let items = self.trash_repository.get_trash_items(&user_id).await?;
let dtos = items.into_iter().map(|item| self.to_dto(item)).collect();
@@ -137,7 +134,7 @@ impl TrashUseCase for TrashService {
}
#[instrument(skip(self))]
async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: &str) -> Result<()> {
async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: Uuid) -> Result<()> {
info!(
"Moving to trash: type={}, id={}, user={}",
item_type, item_id, user_id
@@ -163,20 +160,7 @@ impl TrashUseCase for TrashService {
}
};
debug!("Validating user UUID: {}", user_id);
let user_uuid = match Uuid::parse_str(user_id) {
Ok(uuid) => {
debug!("Valid user UUID: {}", uuid);
uuid
}
Err(e) => {
error!("Invalid user UUID: {} - Error: {}", user_id, e);
return Err(DomainError::validation_error(format!(
"Invalid user ID: {}",
e
)));
}
};
let user_uuid = user_id;
match item_type {
"file" => {
@@ -276,7 +260,7 @@ impl TrashUseCase for TrashService {
// Ownership check — return NotFound (not Forbidden) to
// prevent leaking whether the folder exists.
if folder.owner_id().is_none_or(|o| o != user_id) {
if folder.owner_id().is_none_or(|o| o != user_id.to_string()) {
return Err(DomainError::not_found(
"Folder",
format!("Folder not found: {}", item_id),
@@ -331,7 +315,7 @@ impl TrashUseCase for TrashService {
}
#[instrument(skip(self))]
async fn restore_item(&self, trash_id: &str, user_id: &str) -> Result<()> {
async fn restore_item(&self, trash_id: &str, user_id: Uuid) -> Result<()> {
info!("Restoring item {} for user {}", trash_id, user_id);
let trash_uuid = match Uuid::parse_str(trash_id) {
@@ -348,19 +332,7 @@ impl TrashUseCase for TrashService {
}
};
let user_uuid = match Uuid::parse_str(user_id) {
Ok(id) => {
info!("User UUID parsed successfully: {}", id);
id
}
Err(e) => {
error!("Invalid user ID format: {} - {}", user_id, e);
return Err(DomainError::validation_error(format!(
"Invalid user ID: {}",
e
)));
}
};
let user_uuid = user_id;
// Get the trash item
info!("Retrieving trash item from repository: ID={}", trash_id);
@@ -514,7 +486,7 @@ impl TrashUseCase for TrashService {
}
#[instrument(skip(self))]
async fn delete_permanently(&self, trash_id: &str, user_id: &str) -> Result<()> {
async fn delete_permanently(&self, trash_id: &str, user_id: Uuid) -> Result<()> {
info!(
"Permanently deleting item {} for user {}",
trash_id, user_id
@@ -534,19 +506,7 @@ impl TrashUseCase for TrashService {
}
};
let user_uuid = match Uuid::parse_str(user_id) {
Ok(id) => {
info!("User UUID parsed successfully: {}", id);
id
}
Err(e) => {
error!("Invalid user ID format: {} - {}", user_id, e);
return Err(DomainError::validation_error(format!(
"Invalid user ID: {}",
e
)));
}
};
let user_uuid = user_id;
// Get the trash item
info!("Retrieving trash item from repository: ID={}", trash_id);
@@ -684,12 +644,9 @@ impl TrashUseCase for TrashService {
}
#[instrument(skip(self))]
async fn empty_trash(&self, user_id: &str) -> Result<()> {
async fn empty_trash(&self, user_id: Uuid) -> Result<()> {
info!("Emptying trash for user {}", user_id);
let user_uuid = Uuid::parse_str(user_id)
.map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?;
// clear_trash() already performs bulk SQL DELETEs in 2 queries:
// 1. DELETE FROM storage.files WHERE user_id = $1 AND is_trashed = TRUE
// 2. DELETE FROM storage.folders WHERE user_id = $1 AND is_trashed = TRUE
@@ -700,7 +657,7 @@ impl TrashUseCase for TrashService {
// remove_reference() call is needed.
//
// Finally it clears the trash_items index for the user.
self.trash_repository.clear_trash(&user_uuid).await?;
self.trash_repository.clear_trash(&user_id).await?;
info!("Trash emptied for user {}", user_id);
Ok(())
+22 -21
View File
@@ -12,6 +12,7 @@ use std::sync::Arc;
use bytes::Bytes;
use futures::Stream;
use uuid::Uuid;
use crate::application::dtos::file_dto::FileDto;
use crate::application::dtos::folder_dto::{
@@ -108,7 +109,7 @@ impl FileReadPort for StubFileReadPort {
&self,
_folder_id: Option<&str>,
_criteria: &SearchCriteriaDto,
_user_id: &str,
_user_id: Uuid,
) -> Result<(Vec<File>, usize), DomainError> {
Ok((Vec::new(), 0))
}
@@ -117,7 +118,7 @@ impl FileReadPort for StubFileReadPort {
&self,
_folder_id: Option<&str>,
_criteria: &SearchCriteriaDto,
_user_id: &str,
_user_id: Uuid,
) -> Result<usize, DomainError> {
Ok(0)
}
@@ -129,7 +130,7 @@ impl FileReadPort for StubFileReadPort {
Ok(Box::pin(futures::stream::empty()))
}
async fn get_file_for_owner(&self, _id: &str, _owner_id: &str) -> Result<File, DomainError> {
async fn get_file_for_owner(&self, _id: &str, _owner_id: Uuid) -> Result<File, DomainError> {
Ok(File::default())
}
}
@@ -362,7 +363,7 @@ impl FolderUseCase for StubFolderUseCase {
async fn get_folder_owned(
&self,
_id: &str,
_caller_id: &str,
_caller_id: Uuid,
) -> Result<FolderDto, DomainError> {
Ok(FolderDto::default())
}
@@ -378,7 +379,7 @@ impl FolderUseCase for StubFolderUseCase {
async fn list_folders_for_owner(
&self,
_parent_id: Option<&str>,
_owner_id: &str,
_owner_id: Uuid,
) -> Result<Vec<FolderDto>, DomainError> {
Ok(Vec::new())
}
@@ -394,7 +395,7 @@ impl FolderUseCase for StubFolderUseCase {
async fn list_folders_for_owner_paginated(
&self,
_parent_id: Option<&str>,
_owner_id: &str,
_owner_id: Uuid,
_pagination: &PaginationRequestDto,
) -> Result<PaginatedResponseDto<FolderDto>, DomainError> {
Ok(PaginatedResponseDto::new(Vec::new(), 0, 10, 0))
@@ -404,7 +405,7 @@ impl FolderUseCase for StubFolderUseCase {
&self,
_id: &str,
_dto: RenameFolderDto,
_caller_id: &str,
_caller_id: Uuid,
) -> Result<FolderDto, DomainError> {
Ok(FolderDto::default())
}
@@ -413,18 +414,18 @@ impl FolderUseCase for StubFolderUseCase {
&self,
_id: &str,
_dto: MoveFolderDto,
_caller_id: &str,
_caller_id: Uuid,
) -> Result<FolderDto, DomainError> {
Ok(FolderDto::default())
}
async fn delete_folder(&self, _id: &str, _caller_id: &str) -> Result<(), DomainError> {
async fn delete_folder(&self, _id: &str, _caller_id: Uuid) -> Result<(), DomainError> {
Ok(())
}
async fn create_home_folder(
&self,
_user_id: &str,
_user_id: Uuid,
_name: String,
) -> Result<FolderDto, DomainError> {
Ok(FolderDto::default())
@@ -510,7 +511,7 @@ impl FileRetrievalUseCase for StubFileRetrievalUseCase {
async fn list_files_owned(
&self,
_folder_id: Option<&str>,
_owner_id: &str,
_owner_id: Uuid,
) -> Result<Vec<FileDto>, DomainError> {
Ok(Vec::new())
}
@@ -526,7 +527,7 @@ impl FileRetrievalUseCase for StubFileRetrievalUseCase {
async fn get_file_stream_owned(
&self,
_id: &str,
_caller_id: &str,
_caller_id: Uuid,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
let empty_stream = futures::stream::empty::<Result<Bytes, std::io::Error>>();
Ok(Box::new(empty_stream))
@@ -569,14 +570,14 @@ impl FileRetrievalUseCase for StubFileRetrievalUseCase {
Ok(Box::pin(futures::stream::empty()))
}
async fn get_file_owned(&self, _id: &str, _caller_id: &str) -> Result<FileDto, DomainError> {
async fn get_file_owned(&self, _id: &str, _caller_id: Uuid) -> Result<FileDto, DomainError> {
Ok(FileDto::default())
}
async fn get_file_optimized_owned(
&self,
_id: &str,
_caller_id: &str,
_caller_id: Uuid,
_accept_webp: bool,
_prefer_original: bool,
) -> Result<(FileDto, OptimizedFileContent), DomainError> {
@@ -593,7 +594,7 @@ impl FileRetrievalUseCase for StubFileRetrievalUseCase {
async fn get_file_range_stream_owned(
&self,
_id: &str,
_caller_id: &str,
_caller_id: Uuid,
_start: u64,
_end: Option<u64>,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
@@ -628,7 +629,7 @@ impl FileManagementUseCase for StubFileManagementUseCase {
async fn copy_file_owned(
&self,
_file_id: &str,
_caller_id: &str,
_caller_id: Uuid,
_folder_id: Option<String>,
) -> Result<FileDto, DomainError> {
Ok(FileDto::default())
@@ -642,18 +643,18 @@ impl FileManagementUseCase for StubFileManagementUseCase {
Ok(())
}
async fn delete_file_owned(&self, _id: &str, _caller_id: &str) -> Result<(), DomainError> {
async fn delete_file_owned(&self, _id: &str, _caller_id: Uuid) -> Result<(), DomainError> {
Ok(())
}
async fn delete_with_cleanup(&self, _id: &str, _user_id: &str) -> Result<bool, DomainError> {
async fn delete_with_cleanup(&self, _id: &str, _user_id: Uuid) -> Result<bool, DomainError> {
Ok(false)
}
async fn move_file_owned(
&self,
_file_id: &str,
_caller_id: &str,
_caller_id: Uuid,
_folder_id: Option<String>,
) -> Result<FileDto, DomainError> {
Ok(FileDto::default())
@@ -662,7 +663,7 @@ impl FileManagementUseCase for StubFileManagementUseCase {
async fn rename_file_owned(
&self,
_file_id: &str,
_caller_id: &str,
_caller_id: Uuid,
_new_name: &str,
) -> Result<FileDto, DomainError> {
Ok(FileDto::default())
@@ -679,7 +680,7 @@ impl SearchUseCase for StubSearchUseCase {
async fn search(
&self,
_criteria: SearchCriteriaDto,
_user_id: &str,
_user_id: Uuid,
) -> Result<Arc<SearchResultsDto>, DomainError> {
Ok(Arc::new(SearchResultsDto::empty()))
}
+4 -4
View File
@@ -11,9 +11,9 @@ use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct AppPassword {
/// Unique identifier.
pub id: String,
pub id: Uuid,
/// Owner user ID.
pub user_id: String,
pub user_id: Uuid,
/// Human-readable label chosen by the user (e.g. "DAVx5 on Pixel 8").
pub label: String,
/// Argon2 hash of the generated password token.
@@ -41,7 +41,7 @@ impl AppPassword {
/// The caller is responsible for hashing the raw token and passing
/// the hash and prefix.
pub fn new(
user_id: String,
user_id: Uuid,
label: String,
password_hash: String,
prefix: String,
@@ -49,7 +49,7 @@ impl AppPassword {
expires_at: Option<DateTime<Utc>>,
) -> Self {
Self {
id: Uuid::new_v4().to_string(),
id: Uuid::new_v4(),
user_id,
label,
password_hash,
+10 -10
View File
@@ -48,13 +48,13 @@ impl std::fmt::Display for DeviceCodeStatus {
/// Domain entity for a Device Authorization flow.
#[derive(Debug, Clone)]
pub struct DeviceCode {
id: String,
id: Uuid,
device_code: String,
user_code: String,
client_name: String,
scopes: String,
status: DeviceCodeStatus,
user_id: Option<String>,
user_id: Option<Uuid>,
access_token: Option<String>,
refresh_token: Option<String>,
verification_uri: String,
@@ -89,7 +89,7 @@ impl DeviceCode {
) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4().to_string(),
id: Uuid::new_v4(),
device_code,
user_code,
client_name,
@@ -111,13 +111,13 @@ impl DeviceCode {
/// Reconstruct from database row.
#[allow(clippy::too_many_arguments)]
pub fn from_raw(
id: String,
id: Uuid,
device_code: String,
user_code: String,
client_name: String,
scopes: String,
status: DeviceCodeStatus,
user_id: Option<String>,
user_id: Option<Uuid>,
access_token: Option<String>,
refresh_token: Option<String>,
verification_uri: String,
@@ -150,8 +150,8 @@ impl DeviceCode {
// ── Getters ──────────────────────────────────────────────────
pub fn id(&self) -> &str {
&self.id
pub fn id(&self) -> Uuid {
self.id
}
pub fn device_code(&self) -> &str {
@@ -174,8 +174,8 @@ impl DeviceCode {
self.status
}
pub fn user_id(&self) -> Option<&str> {
self.user_id.as_deref()
pub fn user_id(&self) -> Option<Uuid> {
self.user_id
}
pub fn access_token(&self) -> Option<&str> {
@@ -243,7 +243,7 @@ impl DeviceCode {
}
/// Authorize this device code for a specific user, storing the tokens.
pub fn authorize(&mut self, user_id: String, access_token: String, refresh_token: String) {
pub fn authorize(&mut self, user_id: Uuid, access_token: String, refresh_token: String) {
self.status = DeviceCodeStatus::Authorized;
self.user_id = Some(user_id);
self.access_token = Some(access_token);
+10 -13
View File
@@ -3,8 +3,8 @@ use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct Session {
id: String,
user_id: String,
id: Uuid,
user_id: Uuid,
refresh_token: String,
expires_at: DateTime<Utc>,
ip_address: Option<String>,
@@ -15,22 +15,19 @@ pub struct Session {
impl Session {
pub fn new(
user_id: String,
user_id: Uuid,
refresh_token: String,
ip_address: Option<String>,
user_agent: Option<String>,
expires_in_days: i64,
) -> Self {
if user_id.is_empty() {
panic!("Session user_id cannot be empty");
}
if refresh_token.is_empty() {
panic!("Session refresh_token cannot be empty");
}
let now = Utc::now();
Self {
id: Uuid::new_v4().to_string(),
id: Uuid::new_v4(),
user_id,
refresh_token,
expires_at: now + Duration::days(expires_in_days),
@@ -43,8 +40,8 @@ impl Session {
#[allow(clippy::too_many_arguments)]
pub fn from_raw(
id: String,
user_id: String,
id: Uuid,
user_id: Uuid,
refresh_token: String,
expires_at: DateTime<Utc>,
ip_address: Option<String>,
@@ -65,12 +62,12 @@ impl Session {
}
// Getters
pub fn id(&self) -> &str {
&self.id
pub fn id(&self) -> Uuid {
self.id
}
pub fn user_id(&self) -> &str {
&self.user_id
pub fn user_id(&self) -> Uuid {
self.user_id
}
pub fn refresh_token(&self) -> &str {
+21 -16
View File
@@ -6,7 +6,7 @@ pub use super::entity_errors::ShareError;
#[derive(Debug, Clone, PartialEq)]
pub struct Share {
id: String,
id: Uuid,
item_id: String,
item_name: Option<String>,
item_type: ShareItemType,
@@ -15,7 +15,7 @@ pub struct Share {
expires_at: Option<u64>,
permissions: SharePermissions,
created_at: u64,
created_by: String,
created_by: Uuid,
access_count: u64,
}
@@ -37,7 +37,7 @@ impl Share {
item_id: String,
item_name: Option<String>,
item_type: ShareItemType,
created_by: String,
created_by: Uuid,
permissions: Option<SharePermissions>,
password_hash: Option<String>,
expires_at: Option<u64>,
@@ -69,7 +69,7 @@ impl Share {
.as_secs();
Ok(Self {
id: Uuid::new_v4().to_string(),
id: Uuid::new_v4(),
item_id,
item_name,
item_type,
@@ -89,7 +89,7 @@ impl Share {
#[allow(clippy::too_many_arguments)]
pub fn from_raw(
id: String,
id: Uuid,
item_id: String,
item_name: Option<String>,
item_type: ShareItemType,
@@ -98,7 +98,7 @@ impl Share {
expires_at: Option<u64>,
permissions: SharePermissions,
created_at: u64,
created_by: String,
created_by: Uuid,
access_count: u64,
) -> Self {
Self {
@@ -118,8 +118,8 @@ impl Share {
// ── Getters ──
pub fn id(&self) -> &str {
&self.id
pub fn id(&self) -> Uuid {
self.id
}
pub fn item_id(&self) -> &str {
@@ -150,8 +150,8 @@ impl Share {
self.created_at
}
pub fn created_by(&self) -> &str {
&self.created_by
pub fn created_by(&self) -> Uuid {
self.created_by
}
pub fn access_count(&self) -> u64 {
@@ -262,13 +262,18 @@ impl TryFrom<&str> for ShareItemType {
mod tests {
use super::*;
fn test_user_id() -> Uuid {
Uuid::new_v4()
}
#[test]
fn test_create_share() {
let uid = test_user_id();
let share = Share::new(
"test_file_id".to_string(),
None,
ShareItemType::File,
"user123".to_string(),
uid,
None,
None,
None,
@@ -277,7 +282,7 @@ mod tests {
assert_eq!(share.item_id(), "test_file_id");
assert_eq!(*share.item_type(), ShareItemType::File);
assert_eq!(share.created_by(), "user123");
assert_eq!(share.created_by(), uid);
assert!(share.permissions().read());
assert!(!share.permissions().write());
assert!(!share.permissions().reshare());
@@ -299,7 +304,7 @@ mod tests {
"test_file_id".to_string(),
None,
ShareItemType::File,
"user123".to_string(),
test_user_id(),
None,
None,
Some(future),
@@ -314,7 +319,7 @@ mod tests {
"test_file_id".to_string(),
None,
ShareItemType::File,
"user123".to_string(),
test_user_id(),
None,
None,
Some(past),
@@ -349,7 +354,7 @@ mod tests {
"test_file_id".to_string(),
None,
ShareItemType::File,
"user123".to_string(),
test_user_id(),
None,
Some("some_hash_value".to_string()),
None,
@@ -366,7 +371,7 @@ mod tests {
"test_file_id".to_string(),
None,
ShareItemType::File,
"user123".to_string(),
test_user_id(),
None,
None, // No password
None,
+7 -7
View File
@@ -22,7 +22,7 @@ impl std::fmt::Display for UserRole {
#[derive(Debug, Clone)]
pub struct User {
id: String,
id: Uuid,
username: String,
email: String,
password_hash: String,
@@ -70,7 +70,7 @@ impl User {
let now = Utc::now();
Ok(Self {
id: Uuid::new_v4().to_string(),
id: Uuid::new_v4(),
username,
email,
password_hash,
@@ -99,7 +99,7 @@ impl User {
Self::validate_email(&email)?;
let now = Utc::now();
Ok(Self {
id: Uuid::new_v4().to_string(),
id: Uuid::new_v4(),
username,
email,
password_hash: "__OIDC_NO_PASSWORD__".to_string(),
@@ -117,7 +117,7 @@ impl User {
#[allow(clippy::too_many_arguments)]
pub fn from_data(
id: String,
id: Uuid,
username: String,
email: String,
password_hash: String,
@@ -148,7 +148,7 @@ impl User {
#[allow(clippy::too_many_arguments)]
pub fn from_data_full(
id: String,
id: Uuid,
username: String,
email: String,
password_hash: String,
@@ -180,8 +180,8 @@ impl User {
}
// Getters
pub fn id(&self) -> &str {
&self.id
pub fn id(&self) -> Uuid {
self.id
}
pub fn username(&self) -> &str {
@@ -22,23 +22,23 @@ pub trait AddressBookRepository: Send + Sync + 'static {
) -> AddressBookRepositoryResult<Option<AddressBook>>;
async fn get_address_books_by_owner(
&self,
owner_id: &str,
owner_id: Uuid,
) -> AddressBookRepositoryResult<Vec<AddressBook>>;
async fn get_shared_address_books(
&self,
user_id: &str,
user_id: Uuid,
) -> AddressBookRepositoryResult<Vec<AddressBook>>;
async fn get_public_address_books(&self) -> AddressBookRepositoryResult<Vec<AddressBook>>;
async fn share_address_book(
&self,
address_book_id: &Uuid,
user_id: &str,
user_id: Uuid,
can_write: bool,
) -> AddressBookRepositoryResult<()>;
async fn unshare_address_book(
&self,
address_book_id: &Uuid,
user_id: &str,
user_id: Uuid,
) -> AddressBookRepositoryResult<()>;
async fn get_address_book_shares(
&self,
@@ -21,20 +21,20 @@ pub trait CalendarRepository: Send + Sync + 'static {
/// Lists all calendars for a specific user
async fn list_calendars_by_owner(
&self,
owner_id: &str,
owner_id: Uuid,
) -> CalendarRepositoryResult<Vec<Calendar>>;
/// Finds a calendar by name and owner
async fn find_calendar_by_name_and_owner(
&self,
name: &str,
owner_id: &str,
owner_id: Uuid,
) -> CalendarRepositoryResult<Calendar>;
/// Lists calendars shared with a specific user
async fn list_calendars_shared_with_user(
&self,
user_id: &str,
user_id: Uuid,
) -> CalendarRepositoryResult<Vec<Calendar>>;
/// List public calendars
@@ -48,7 +48,7 @@ pub trait CalendarRepository: Send + Sync + 'static {
async fn user_has_calendar_access(
&self,
calendar_id: &Uuid,
user_id: &str,
user_id: Uuid,
) -> CalendarRepositoryResult<bool>;
/// Gets a custom property for a calendar
@@ -83,7 +83,7 @@ pub trait CalendarRepository: Send + Sync + 'static {
async fn share_calendar(
&self,
calendar_id: &Uuid,
user_id: &str,
user_id: Uuid,
access_level: &str,
) -> CalendarRepositoryResult<()>;
@@ -91,7 +91,7 @@ pub trait CalendarRepository: Send + Sync + 'static {
async fn remove_calendar_sharing(
&self,
calendar_id: &Uuid,
user_id: &str,
user_id: Uuid,
) -> CalendarRepositoryResult<()>;
/// Get calendar sharing information (who has access to this calendar)
@@ -1,5 +1,6 @@
use crate::common::errors::DomainError;
use crate::domain::entities::session::Session;
use uuid::Uuid;
#[derive(Debug, thiserror::Error)]
pub enum SessionRepositoryError {
@@ -33,7 +34,7 @@ pub trait SessionRepository: Send + Sync + 'static {
async fn create_session(&self, session: Session) -> SessionRepositoryResult<Session>;
/// Gets a session by ID
async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult<Session>;
async fn get_session_by_id(&self, id: Uuid) -> SessionRepositoryResult<Session>;
/// Gets a session by refresh token
async fn get_session_by_refresh_token(
@@ -42,14 +43,14 @@ pub trait SessionRepository: Send + Sync + 'static {
) -> SessionRepositoryResult<Session>;
/// Gets all sessions for a user
async fn get_sessions_by_user_id(&self, user_id: &str)
async fn get_sessions_by_user_id(&self, user_id: Uuid)
-> SessionRepositoryResult<Vec<Session>>;
/// Revokes a specific session
async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()>;
async fn revoke_session(&self, session_id: Uuid) -> SessionRepositoryResult<()>;
/// Revokes all sessions for a user
async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult<u64>;
async fn revoke_all_user_sessions(&self, user_id: Uuid) -> SessionRepositoryResult<u64>;
/// Deletes expired sessions
async fn delete_expired_sessions(&self) -> SessionRepositoryResult<u64>;
@@ -1,5 +1,6 @@
use crate::common::errors::DomainError;
use std::collections::HashMap;
use uuid::Uuid;
/// Repository for platform settings stored in the database.
/// Settings are key-value pairs organized by category (e.g., "oidc", "general").
@@ -18,7 +19,7 @@ pub trait SettingsRepository: Send + Sync + 'static {
value: &str,
category: &str,
is_secret: bool,
updated_by: Option<&str>,
updated_by: Option<Uuid>,
) -> Result<(), DomainError>;
/// Delete a setting by key
@@ -34,7 +35,7 @@ pub trait SettingsRepository: Send + Sync + 'static {
/// The default implementation falls back to the non-atomic
/// get-then-set pattern for repositories that don't support a native
/// atomic upsert.
async fn try_claim_initialization(&self, admin_user_id: &str) -> Result<bool, DomainError> {
async fn try_claim_initialization(&self, admin_user_id: Uuid) -> Result<bool, DomainError> {
// Default: non-atomic fallback (overridden by PG implementation)
match self.get("system_initialized").await? {
Some(v) if v == "true" => Ok(false),
+4 -3
View File
@@ -1,4 +1,5 @@
use thiserror::Error;
use uuid::Uuid;
use crate::domain::{
entities::share::{Share, ShareItemType},
@@ -24,7 +25,7 @@ pub trait ShareRepository: Send + Sync + 'static {
async fn save(&self, share: &Share) -> Result<Share, ShareRepositoryError>;
/// Find a share by its ID
async fn find_by_id(&self, id: &str) -> Result<Share, ShareRepositoryError>;
async fn find_by_id(&self, id: Uuid) -> Result<Share, ShareRepositoryError>;
/// Find a share by its token
async fn find_by_token(&self, token: &str) -> Result<Share, ShareRepositoryError>;
@@ -37,10 +38,10 @@ pub trait ShareRepository: Send + Sync + 'static {
) -> Result<Vec<Share>, ShareRepositoryError>;
/// Delete a share by its ID
async fn delete(&self, id: &str) -> Result<(), ShareRepositoryError>;
async fn delete(&self, id: Uuid) -> Result<(), ShareRepositoryError>;
/// Find all shares created by a specific user
async fn find_by_user(&self, user_id: &str) -> Result<Vec<Share>, ShareRepositoryError>;
async fn find_by_user(&self, user_id: Uuid) -> Result<Vec<Share>, ShareRepositoryError>;
/// Find all shares (admin operation)
async fn find_all(&self) -> Result<Vec<Share>, ShareRepositoryError>;
+9 -8
View File
@@ -1,5 +1,6 @@
use crate::common::errors::DomainError;
use crate::domain::entities::user::{User, UserRole};
use uuid::Uuid;
#[derive(Debug, thiserror::Error)]
pub enum UserRepositoryError {
@@ -45,7 +46,7 @@ pub trait UserRepository: Send + Sync + 'static {
async fn create_user(&self, user: User) -> UserRepositoryResult<User>;
/// Gets a user by ID
async fn get_user_by_id(&self, id: &str) -> UserRepositoryResult<User>;
async fn get_user_by_id(&self, id: Uuid) -> UserRepositoryResult<User>;
/// Gets a user by username
async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult<User>;
@@ -59,12 +60,12 @@ pub trait UserRepository: Send + Sync + 'static {
/// Updates only a user's storage usage
async fn update_storage_usage(
&self,
user_id: &str,
user_id: Uuid,
usage_bytes: i64,
) -> UserRepositoryResult<()>;
/// Updates the last login date
async fn update_last_login(&self, user_id: &str) -> UserRepositoryResult<()>;
async fn update_last_login(&self, user_id: Uuid) -> UserRepositoryResult<()>;
/// Lists users with pagination
async fn list_users(&self, limit: i64, offset: i64) -> UserRepositoryResult<Vec<User>>;
@@ -73,21 +74,21 @@ pub trait UserRepository: Send + Sync + 'static {
async fn search_users(&self, query: &str, limit: i64) -> UserRepositoryResult<Vec<User>>;
/// Activates or deactivates a user
async fn set_user_active_status(&self, user_id: &str, active: bool)
async fn set_user_active_status(&self, user_id: Uuid, active: bool)
-> UserRepositoryResult<()>;
/// Changes a user's password
async fn change_password(&self, user_id: &str, password_hash: &str)
async fn change_password(&self, user_id: Uuid, password_hash: &str)
-> UserRepositoryResult<()>;
/// Changes a user's role
async fn change_role(&self, user_id: &str, role: UserRole) -> UserRepositoryResult<()>;
async fn change_role(&self, user_id: Uuid, role: UserRole) -> UserRepositoryResult<()>;
/// Lists users by role (admin or user)
async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult<Vec<User>>;
/// Deletes a user
async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()>;
async fn delete_user(&self, user_id: Uuid) -> UserRepositoryResult<()>;
/// Finds a user by OIDC provider + subject pair
async fn get_user_by_oidc_subject(
@@ -99,7 +100,7 @@ pub trait UserRepository: Send + Sync + 'static {
/// Updates a user's storage quota
async fn update_storage_quota(
&self,
user_id: &str,
user_id: Uuid,
quota_bytes: i64,
) -> UserRepositoryResult<()>;
@@ -47,7 +47,7 @@ impl CalendarStoragePort for CalendarStorageAdapter {
async fn create_calendar(
&self,
dto: CreateCalendarDto,
owner_id: &str,
owner_id: Uuid,
) -> Result<CalendarDto, DomainError> {
let calendar = Calendar::new(dto.name, owner_id.to_string(), dto.description, dto.color)?;
@@ -117,7 +117,7 @@ impl CalendarStoragePort for CalendarStorageAdapter {
async fn list_calendars_by_owner(
&self,
owner_id: &str,
owner_id: Uuid,
) -> Result<Vec<CalendarDto>, DomainError> {
let calendars = self
.calendar_repository
@@ -128,7 +128,7 @@ impl CalendarStoragePort for CalendarStorageAdapter {
async fn list_calendars_shared_with_user(
&self,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<CalendarDto>, DomainError> {
let calendars = self
.calendar_repository
@@ -152,7 +152,7 @@ impl CalendarStoragePort for CalendarStorageAdapter {
async fn check_calendar_access(
&self,
calendar_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<bool, DomainError> {
let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
DomainError::new(
@@ -172,7 +172,7 @@ impl CalendarStoragePort for CalendarStorageAdapter {
async fn share_calendar(
&self,
calendar_id: &str,
user_id: &str,
user_id: Uuid,
access_level: &str,
) -> Result<(), DomainError> {
let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
@@ -191,7 +191,7 @@ impl CalendarStoragePort for CalendarStorageAdapter {
async fn remove_calendar_sharing(
&self,
calendar_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError> {
let uuid = Uuid::parse_str(calendar_id).map_err(|_| {
DomainError::new(
@@ -61,7 +61,7 @@ impl ContactStorageAdapter {
async fn check_address_book_access(
&self,
address_book_id: &Uuid,
user_id: &str,
user_id: Uuid,
) -> Result<AddressBook, DomainError> {
let address_book = self
.address_book_repository
@@ -72,7 +72,7 @@ impl ContactStorageAdapter {
})?;
// Check if user is owner
if address_book.owner_id() == user_id {
if address_book.owner_id() == user_id.to_string() {
return Ok(address_book);
}
@@ -86,7 +86,7 @@ impl ContactStorageAdapter {
.address_book_repository
.get_address_book_shares(address_book_id)
.await?;
if shares.iter().any(|(shared_user, _)| shared_user == user_id) {
if shares.iter().any(|(shared_user, _)| shared_user == &user_id.to_string()) {
return Ok(address_book);
}
@@ -101,7 +101,7 @@ impl ContactStorageAdapter {
async fn check_write_access(
&self,
address_book_id: &Uuid,
user_id: &str,
user_id: Uuid,
) -> Result<AddressBook, DomainError> {
let address_book = self
.address_book_repository
@@ -112,7 +112,7 @@ impl ContactStorageAdapter {
})?;
// Owner always has write access
if address_book.owner_id() == user_id {
if address_book.owner_id() == user_id.to_string() {
return Ok(address_book);
}
@@ -123,7 +123,7 @@ impl ContactStorageAdapter {
.await?;
if shares
.iter()
.any(|(shared_user, can_write)| shared_user == user_id && *can_write)
.any(|(shared_user, can_write)| shared_user == &user_id.to_string() && *can_write)
{
return Ok(address_book);
}
@@ -247,7 +247,10 @@ impl AddressBookUseCase for ContactStorageAdapter {
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
// Check write access
let mut address_book = self.check_write_access(&uuid, &update.user_id).await?;
let user_id = Uuid::parse_str(&update.user_id).map_err(|_| {
DomainError::new(ErrorKind::InvalidInput, "AddressBook", "Invalid user ID format")
})?;
let mut address_book = self.check_write_access(&uuid, user_id).await?;
if let Some(name) = update.name {
address_book.set_name(name);
@@ -273,7 +276,7 @@ impl AddressBookUseCase for ContactStorageAdapter {
async fn delete_address_book(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError> {
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
@@ -286,7 +289,7 @@ impl AddressBookUseCase for ContactStorageAdapter {
DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found")
})?;
if address_book.owner_id() != user_id {
if address_book.owner_id() != user_id.to_string() {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"AddressBook",
@@ -302,7 +305,7 @@ impl AddressBookUseCase for ContactStorageAdapter {
async fn get_address_book(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<AddressBookDto, DomainError> {
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
let address_book = self.check_address_book_access(&uuid, user_id).await?;
@@ -311,7 +314,7 @@ impl AddressBookUseCase for ContactStorageAdapter {
async fn list_user_address_books(
&self,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<AddressBookDto>, DomainError> {
let owned = self
.address_book_repository
@@ -339,7 +342,7 @@ impl AddressBookUseCase for ContactStorageAdapter {
async fn share_address_book(
&self,
dto: ShareAddressBookDto,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError> {
let uuid = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
@@ -352,7 +355,7 @@ impl AddressBookUseCase for ContactStorageAdapter {
DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found")
})?;
if address_book.owner_id() != user_id {
if address_book.owner_id() != user_id.to_string() {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"AddressBook",
@@ -360,15 +363,19 @@ impl AddressBookUseCase for ContactStorageAdapter {
));
}
let target_user_id = Uuid::parse_str(&dto.user_id).map_err(|_| {
DomainError::new(ErrorKind::InvalidInput, "AddressBook", "Invalid target user ID format")
})?;
self.address_book_repository
.share_address_book(&uuid, &dto.user_id, dto.can_write)
.share_address_book(&uuid, target_user_id, dto.can_write)
.await
}
async fn unshare_address_book(
&self,
dto: UnshareAddressBookDto,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError> {
let uuid = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
@@ -381,7 +388,7 @@ impl AddressBookUseCase for ContactStorageAdapter {
DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found")
})?;
if address_book.owner_id() != user_id {
if address_book.owner_id() != user_id.to_string() {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"AddressBook",
@@ -389,15 +396,19 @@ impl AddressBookUseCase for ContactStorageAdapter {
));
}
let target_user_id = Uuid::parse_str(&dto.user_id).map_err(|_| {
DomainError::new(ErrorKind::InvalidInput, "AddressBook", "Invalid target user ID format")
})?;
self.address_book_repository
.unshare_address_book(&uuid, &dto.user_id)
.unshare_address_book(&uuid, target_user_id)
.await
}
async fn get_address_book_shares(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<(String, bool)>, DomainError> {
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
@@ -410,7 +421,7 @@ impl AddressBookUseCase for ContactStorageAdapter {
DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found")
})?;
if address_book.owner_id() != user_id {
if address_book.owner_id() != user_id.to_string() {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"AddressBook",
@@ -429,7 +440,10 @@ impl ContactUseCase for ContactStorageAdapter {
let address_book_id = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
// Check write access
self.check_write_access(&address_book_id, &dto.user_id)
let user_id = Uuid::parse_str(&dto.user_id).map_err(|_| {
DomainError::new(ErrorKind::InvalidInput, "Contact", "Invalid user ID format")
})?;
self.check_write_access(&address_book_id, user_id)
.await?;
let now = chrono::Utc::now();
@@ -471,7 +485,10 @@ impl ContactUseCase for ContactStorageAdapter {
let address_book_id = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
// Check write access
self.check_write_access(&address_book_id, &dto.user_id)
let user_id = Uuid::parse_str(&dto.user_id).map_err(|_| {
DomainError::new(ErrorKind::InvalidInput, "Contact", "Invalid user ID format")
})?;
self.check_write_access(&address_book_id, user_id)
.await?;
// Parse vCard fields
@@ -591,7 +608,10 @@ impl ContactUseCase for ContactStorageAdapter {
.ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?;
// Check write access to the address book
self.check_write_access(contact.address_book_id(), &update.user_id)
let user_id = Uuid::parse_str(&update.user_id).map_err(|_| {
DomainError::new(ErrorKind::InvalidInput, "Contact", "Invalid user ID format")
})?;
self.check_write_access(contact.address_book_id(), user_id)
.await?;
if let Some(full_name) = update.full_name {
@@ -643,7 +663,7 @@ impl ContactUseCase for ContactStorageAdapter {
Ok(ContactDto::from(updated))
}
async fn delete_contact(&self, contact_id: &str, user_id: &str) -> Result<(), DomainError> {
async fn delete_contact(&self, contact_id: &str, user_id: Uuid) -> Result<(), DomainError> {
let uuid = Self::parse_uuid(contact_id, "Contact")?;
let contact = self
@@ -662,7 +682,7 @@ impl ContactUseCase for ContactStorageAdapter {
async fn get_contact(
&self,
contact_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<ContactDto, DomainError> {
let uuid = Self::parse_uuid(contact_id, "Contact")?;
@@ -682,7 +702,7 @@ impl ContactUseCase for ContactStorageAdapter {
async fn list_contacts(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactDto>, DomainError> {
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
@@ -700,7 +720,7 @@ impl ContactUseCase for ContactStorageAdapter {
&self,
address_book_id: &str,
query: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactDto>, DomainError> {
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
@@ -721,7 +741,10 @@ impl ContactUseCase for ContactStorageAdapter {
let address_book_id = Self::parse_uuid(&dto.address_book_id, "AddressBook")?;
// Check write access
self.check_write_access(&address_book_id, &dto.user_id)
let user_id = Uuid::parse_str(&dto.user_id).map_err(|_| {
DomainError::new(ErrorKind::InvalidInput, "ContactGroup", "Invalid user ID format")
})?;
self.check_write_access(&address_book_id, user_id)
.await?;
let group = ContactGroup::new(address_book_id, dto.name);
@@ -746,7 +769,10 @@ impl ContactUseCase for ContactStorageAdapter {
})?;
// Check write access
self.check_write_access(group.address_book_id(), &update.user_id)
let user_id = Uuid::parse_str(&update.user_id).map_err(|_| {
DomainError::new(ErrorKind::InvalidInput, "ContactGroup", "Invalid user ID format")
})?;
self.check_write_access(group.address_book_id(), user_id)
.await?;
group.set_name(update.name);
@@ -756,7 +782,7 @@ impl ContactUseCase for ContactStorageAdapter {
Ok(ContactGroupDto::from(updated))
}
async fn delete_group(&self, group_id: &str, user_id: &str) -> Result<(), DomainError> {
async fn delete_group(&self, group_id: &str, user_id: Uuid) -> Result<(), DomainError> {
let uuid = Self::parse_uuid(group_id, "ContactGroup")?;
let group = self
@@ -777,7 +803,7 @@ impl ContactUseCase for ContactStorageAdapter {
async fn get_group(
&self,
group_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<ContactGroupDto, DomainError> {
let uuid = Self::parse_uuid(group_id, "ContactGroup")?;
@@ -799,7 +825,7 @@ impl ContactUseCase for ContactStorageAdapter {
async fn list_groups(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactGroupDto>, DomainError> {
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
@@ -816,7 +842,7 @@ impl ContactUseCase for ContactStorageAdapter {
async fn add_contact_to_group(
&self,
dto: GroupMembershipDto,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError> {
let group_id = Self::parse_uuid(&dto.group_id, "ContactGroup")?;
let contact_id = Self::parse_uuid(&dto.contact_id, "Contact")?;
@@ -841,7 +867,7 @@ impl ContactUseCase for ContactStorageAdapter {
async fn remove_contact_from_group(
&self,
dto: GroupMembershipDto,
user_id: &str,
user_id: Uuid,
) -> Result<(), DomainError> {
let group_id = Self::parse_uuid(&dto.group_id, "ContactGroup")?;
let contact_id = Self::parse_uuid(&dto.contact_id, "Contact")?;
@@ -866,7 +892,7 @@ impl ContactUseCase for ContactStorageAdapter {
async fn list_contacts_in_group(
&self,
group_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactDto>, DomainError> {
let uuid = Self::parse_uuid(group_id, "ContactGroup")?;
@@ -889,7 +915,7 @@ impl ContactUseCase for ContactStorageAdapter {
async fn list_groups_for_contact(
&self,
contact_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<ContactGroupDto>, DomainError> {
let uuid = Self::parse_uuid(contact_id, "Contact")?;
@@ -910,7 +936,7 @@ impl ContactUseCase for ContactStorageAdapter {
async fn get_contact_vcard(
&self,
contact_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<String, DomainError> {
let uuid = Self::parse_uuid(contact_id, "Contact")?;
@@ -930,7 +956,7 @@ impl ContactUseCase for ContactStorageAdapter {
async fn get_contacts_as_vcards(
&self,
address_book_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<(String, String)>, DomainError> {
let uuid = Self::parse_uuid(address_book_id, "AddressBook")?;
@@ -144,7 +144,7 @@ impl AddressBookRepository for AddressBookPgRepository {
async fn get_address_books_by_owner(
&self,
owner_id: &str,
owner_id: Uuid,
) -> AddressBookRepositoryResult<Vec<AddressBook>> {
let rows = sqlx::query(
r#"
@@ -182,7 +182,7 @@ impl AddressBookRepository for AddressBookPgRepository {
async fn get_shared_address_books(
&self,
user_id: &str,
user_id: Uuid,
) -> AddressBookRepositoryResult<Vec<AddressBook>> {
let rows = sqlx::query(
r#"
@@ -254,7 +254,7 @@ impl AddressBookRepository for AddressBookPgRepository {
async fn share_address_book(
&self,
address_book_id: &Uuid,
user_id: &str,
user_id: Uuid,
can_write: bool,
) -> AddressBookRepositoryResult<()> {
sqlx::query(
@@ -277,7 +277,7 @@ impl AddressBookRepository for AddressBookPgRepository {
async fn unshare_address_book(
&self,
address_book_id: &Uuid,
user_id: &str,
user_id: Uuid,
) -> AddressBookRepositoryResult<()> {
sqlx::query(
r#"
@@ -6,6 +6,7 @@ use crate::domain::entities::app_password::AppPassword;
use chrono::{DateTime, Utc};
use sqlx::PgPool;
use std::sync::Arc;
use uuid::Uuid;
pub struct AppPasswordPgRepository {
pool: Arc<PgPool>,
@@ -48,7 +49,7 @@ impl AppPasswordStoragePort for AppPasswordPgRepository {
Ok(ap)
}
async fn list_by_user(&self, user_id: &str) -> Result<Vec<AppPassword>, DomainError> {
async fn list_by_user(&self, user_id: Uuid) -> Result<Vec<AppPassword>, DomainError> {
let rows = sqlx::query_as::<_, AppPasswordRow>(
r#"
SELECT id, user_id, label, password_hash, prefix, scopes,
@@ -66,7 +67,7 @@ impl AppPasswordStoragePort for AppPasswordPgRepository {
Ok(rows.into_iter().map(|r| r.into()).collect())
}
async fn get_by_id(&self, id: &str) -> Result<AppPassword, DomainError> {
async fn get_by_id(&self, id: Uuid) -> Result<AppPassword, DomainError> {
let row = sqlx::query_as::<_, AppPasswordRow>(
r#"
SELECT id, user_id, label, password_hash, prefix, scopes,
@@ -79,12 +80,12 @@ impl AppPasswordStoragePort for AppPasswordPgRepository {
.fetch_optional(self.pool())
.await
.map_err(|e| DomainError::internal_error("AppPasswordPg", format!("get_by_id: {e}")))?
.ok_or_else(|| DomainError::not_found("AppPassword", id))?;
.ok_or_else(|| DomainError::not_found("AppPassword", id.to_string()))?;
Ok(row.into())
}
async fn get_active_by_user_id(&self, user_id: &str) -> Result<Vec<AppPassword>, DomainError> {
async fn get_active_by_user_id(&self, user_id: Uuid) -> Result<Vec<AppPassword>, DomainError> {
let rows = sqlx::query_as::<_, AppPasswordRow>(
r#"
SELECT id, user_id, label, password_hash, prefix, scopes,
@@ -105,7 +106,7 @@ impl AppPasswordStoragePort for AppPasswordPgRepository {
async fn get_active_by_user_prefix(
&self,
user_id: &str,
user_id: Uuid,
prefix: &str,
) -> Result<Vec<AppPassword>, DomainError> {
let rows = sqlx::query_as::<_, AppPasswordRow>(
@@ -131,7 +132,7 @@ impl AppPasswordStoragePort for AppPasswordPgRepository {
Ok(rows.into_iter().map(|r| r.into()).collect())
}
async fn touch_last_used(&self, id: &str) -> Result<(), DomainError> {
async fn touch_last_used(&self, id: Uuid) -> Result<(), DomainError> {
sqlx::query("UPDATE auth.app_passwords SET last_used_at = NOW() WHERE id = $1")
.bind(id)
.execute(self.pool())
@@ -140,7 +141,7 @@ impl AppPasswordStoragePort for AppPasswordPgRepository {
Ok(())
}
async fn revoke(&self, id: &str, user_id: &str) -> Result<(), DomainError> {
async fn revoke(&self, id: Uuid, user_id: Uuid) -> Result<(), DomainError> {
let result = sqlx::query(
"UPDATE auth.app_passwords SET active = FALSE WHERE id = $1 AND user_id = $2",
)
@@ -151,12 +152,12 @@ impl AppPasswordStoragePort for AppPasswordPgRepository {
.map_err(|e| DomainError::internal_error("AppPasswordPg", format!("revoke: {e}")))?;
if result.rows_affected() == 0 {
return Err(DomainError::not_found("AppPassword", id));
return Err(DomainError::not_found("AppPassword", id.to_string()));
}
Ok(())
}
async fn delete_by_user_and_id(&self, id: &str, user_id: &str) -> Result<bool, DomainError> {
async fn delete_by_user_and_id(&self, id: Uuid, user_id: Uuid) -> Result<bool, DomainError> {
let result = sqlx::query("DELETE FROM auth.app_passwords WHERE id = $1 AND user_id = $2")
.bind(id)
.bind(user_id)
@@ -190,8 +191,8 @@ impl AppPasswordStoragePort for AppPasswordPgRepository {
/// Internal row struct for sqlx mapping.
#[derive(sqlx::FromRow)]
struct AppPasswordRow {
id: String,
user_id: String,
id: Uuid,
user_id: Uuid,
label: String,
password_hash: String,
prefix: String,
@@ -140,7 +140,7 @@ impl CalendarRepository for CalendarPgRepository {
async fn list_calendars_by_owner(
&self,
owner_id: &str,
owner_id: Uuid,
) -> CalendarRepositoryResult<Vec<Calendar>> {
let rows = sqlx::query(
r#"
@@ -180,7 +180,7 @@ impl CalendarRepository for CalendarPgRepository {
async fn find_calendar_by_name_and_owner(
&self,
name: &str,
owner_id: &str,
owner_id: Uuid,
) -> CalendarRepositoryResult<Calendar> {
let row = sqlx::query(
r#"
@@ -218,7 +218,7 @@ impl CalendarRepository for CalendarPgRepository {
async fn list_calendars_shared_with_user(
&self,
user_id: &str,
user_id: Uuid,
) -> CalendarRepositoryResult<Vec<Calendar>> {
let rows = sqlx::query(
r#"
@@ -299,7 +299,7 @@ impl CalendarRepository for CalendarPgRepository {
async fn user_has_calendar_access(
&self,
calendar_id: &Uuid,
user_id: &str,
user_id: Uuid,
) -> CalendarRepositoryResult<bool> {
// Check if the user is the owner of the calendar or has a share
let row = sqlx::query(
@@ -327,7 +327,7 @@ impl CalendarRepository for CalendarPgRepository {
async fn share_calendar(
&self,
calendar_id: &Uuid,
user_id: &str,
user_id: Uuid,
access_level: &str,
) -> CalendarRepositoryResult<()> {
// Validate access level
@@ -358,7 +358,7 @@ impl CalendarRepository for CalendarPgRepository {
async fn remove_calendar_sharing(
&self,
calendar_id: &Uuid,
user_id: &str,
user_id: Uuid,
) -> CalendarRepositoryResult<()> {
sqlx::query(
r#"
@@ -2,6 +2,7 @@
use sqlx::{PgPool, Row};
use std::sync::Arc;
use uuid::Uuid;
use crate::application::ports::auth_ports::DeviceCodeStoragePort;
use crate::common::errors::{DomainError, ErrorKind};
@@ -28,7 +29,7 @@ impl DeviceCodePgRepository {
let status = DeviceCodeStatus::parse(&status_str).unwrap_or(DeviceCodeStatus::Expired);
Ok(DeviceCode::from_raw(
row.try_get("id").unwrap_or_default(),
row.try_get("id").unwrap(),
row.try_get("device_code").unwrap_or_default(),
row.try_get("user_code").unwrap_or_default(),
row.try_get("client_name").unwrap_or_default(),
@@ -212,7 +213,7 @@ impl DeviceCodeStoragePort for DeviceCodePgRepository {
Ok(result.rows_affected())
}
async fn list_by_user(&self, user_id: &str) -> Result<Vec<DeviceCode>, DomainError> {
async fn list_by_user(&self, user_id: Uuid) -> Result<Vec<DeviceCode>, DomainError> {
let rows = sqlx::query(
r#"
SELECT id, device_code, user_code, client_name, scopes,
@@ -239,7 +240,7 @@ impl DeviceCodeStoragePort for DeviceCodePgRepository {
rows.iter().map(Self::map_row).collect()
}
async fn delete_by_id(&self, id: &str) -> Result<(), DomainError> {
async fn delete_by_id(&self, id: Uuid) -> Result<(), DomainError> {
sqlx::query("DELETE FROM auth.device_codes WHERE id = $1")
.bind(id)
.execute(self.pool.as_ref())
@@ -20,9 +20,7 @@ impl FavoritesPgRepository {
}
impl FavoritesRepositoryPort for FavoritesPgRepository {
async fn get_favorites(&self, user_id: &str) -> Result<Vec<FavoriteItemDto>> {
let user_uuid = Uuid::parse_str(user_id)?;
async fn get_favorites(&self, user_id: Uuid) -> Result<Vec<FavoriteItemDto>> {
let rows = sqlx::query(
r#"
SELECT
@@ -41,12 +39,12 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
AND f.id = uf.item_id::UUID
LEFT JOIN storage.folders fld ON uf.item_type = 'folder'
AND fld.id = uf.item_id::UUID
WHERE uf.user_id = $1::TEXT
WHERE uf.user_id = $1
ORDER BY uf.created_at DESC
LIMIT 500
"#,
)
.bind(user_uuid)
.bind(user_id)
.fetch_all(&*self.db_pool)
.await
.map_err(|e| {
@@ -85,17 +83,15 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
Ok(favorites)
}
async fn add_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()> {
let user_uuid = Uuid::parse_str(user_id)?;
async fn add_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<()> {
sqlx::query(
r#"
INSERT INTO auth.user_favorites (user_id, item_id, item_type)
VALUES ($1::TEXT, $2, $3)
VALUES ($1, $2, $3)
ON CONFLICT (user_id, item_id, item_type) DO NOTHING
"#,
)
.bind(user_uuid)
.bind(user_id)
.bind(item_id)
.bind(item_type)
.execute(&*self.db_pool)
@@ -112,16 +108,14 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
Ok(())
}
async fn remove_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool> {
let user_uuid = Uuid::parse_str(user_id)?;
async fn remove_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<bool> {
let result = sqlx::query(
r#"
DELETE FROM auth.user_favorites
WHERE user_id = $1::TEXT AND item_id = $2 AND item_type = $3
WHERE user_id = $1 AND item_id = $2 AND item_type = $3
"#,
)
.bind(user_uuid)
.bind(user_id)
.bind(item_id)
.bind(item_type)
.execute(&*self.db_pool)
@@ -138,18 +132,16 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
Ok(result.rows_affected() > 0)
}
async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool> {
let user_uuid = Uuid::parse_str(user_id)?;
async fn is_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<bool> {
let row = sqlx::query(
r#"
SELECT EXISTS (
SELECT 1 FROM auth.user_favorites
WHERE user_id = $1::TEXT AND item_id = $2 AND item_type = $3
WHERE user_id = $1 AND item_id = $2 AND item_type = $3
) AS "is_favorite"
"#,
)
.bind(user_uuid)
.bind(user_id)
.bind(item_id)
.bind(item_type)
.fetch_one(&*self.db_pool)
@@ -166,13 +158,11 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
Ok(row.try_get("is_favorite").unwrap_or(false))
}
async fn add_favorites_batch(&self, user_id: &str, items: &[(String, String)]) -> Result<u64> {
async fn add_favorites_batch(&self, user_id: Uuid, items: &[(String, String)]) -> Result<u64> {
if items.is_empty() {
return Ok(0);
}
let user_uuid = Uuid::parse_str(user_id)?;
// Validate all item_types upfront
for (_, item_type) in items {
if item_type != "file" && item_type != "folder" {
@@ -210,7 +200,7 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
query.push_str(", ");
}
query.push_str(&format!(
"(${}::TEXT, ${}, ${})",
"(${}, ${}, ${})",
param_idx,
param_idx + 1,
param_idx + 2
@@ -222,7 +212,7 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
let mut q = sqlx::query(&query);
for (item_id, item_type) in chunk {
q = q.bind(user_uuid).bind(item_id).bind(item_type);
q = q.bind(user_id).bind(item_id).bind(item_type);
}
let result = q.execute(&mut *tx).await.map_err(|e| {
@@ -251,22 +241,20 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
async fn batch_check_favorites(
&self,
user_id: &str,
user_id: Uuid,
item_ids: &[(&str, &str)],
) -> Result<HashSet<String>> {
if item_ids.is_empty() {
return Ok(HashSet::new());
}
let user_uuid = Uuid::parse_str(user_id)?;
// Collect just the IDs for the IN clause
let ids: Vec<String> = item_ids.iter().map(|(id, _)| id.to_string()).collect();
let rows = sqlx::query(
"SELECT item_id FROM auth.user_favorites WHERE user_id = $1::TEXT AND item_id = ANY($2)",
"SELECT item_id FROM auth.user_favorites WHERE user_id = $1 AND item_id = ANY($2)",
)
.bind(user_uuid)
.bind(user_id)
.bind(&ids)
.fetch_all(&*self.db_pool)
.await
@@ -35,6 +35,7 @@ use crate::common::errors::DomainError;
use crate::domain::entities::file::File;
use crate::domain::services::path_service::StoragePath;
use crate::infrastructure::services::dedup_service::DedupService;
use uuid::Uuid;
/// Type alias for file metadata rows from SQL queries.
type FileRow = (
@@ -166,7 +167,7 @@ impl FileBlobReadRepository {
/// `sort_date` epoch for each file (used as pagination cursor).
pub async fn list_media_files(
&self,
owner_id: &str,
owner_id: Uuid,
before: Option<i64>,
limit: i64,
) -> Result<(Vec<File>, Vec<i64>), DomainError> {
@@ -255,7 +256,7 @@ impl FileReadPort for FileBlobReadRepository {
)
}
async fn get_file_for_owner(&self, id: &str, owner_id: &str) -> Result<File, DomainError> {
async fn get_file_for_owner(&self, id: &str, owner_id: Uuid) -> Result<File, DomainError> {
let row = sqlx::query_as::<
_,
(
@@ -286,7 +287,7 @@ impl FileReadPort for FileBlobReadRepository {
"#,
)
.bind(id)
.bind(owner_id)
.bind(owner_id.to_string())
.fetch_optional(self.pool.as_ref())
.await
.map_err(|e| DomainError::internal_error("FileBlobRead", format!("get_for_owner: {e}")))?
@@ -350,8 +351,9 @@ impl FileReadPort for FileBlobReadRepository {
async fn list_files_for_owner(
&self,
folder_id: Option<&str>,
owner_id: &str,
owner_id: Uuid,
) -> Result<Vec<File>, DomainError> {
let owner_str = owner_id.to_string();
let rows: Vec<FileRow> = if let Some(fid) = folder_id {
sqlx::query_as(
r#"
@@ -368,7 +370,7 @@ impl FileReadPort for FileBlobReadRepository {
"#,
)
.bind(fid)
.bind(owner_id)
.bind(&owner_str)
.fetch_all(self.pool.as_ref())
.await
} else {
@@ -386,7 +388,7 @@ impl FileReadPort for FileBlobReadRepository {
ORDER BY fi.name
"#,
)
.bind(owner_id)
.bind(&owner_str)
.fetch_all(self.pool.as_ref())
.await
}
@@ -468,10 +470,11 @@ impl FileReadPort for FileBlobReadRepository {
async fn list_files_batch_for_owner(
&self,
folder_id: Option<&str>,
owner_id: &str,
owner_id: Uuid,
offset: i64,
limit: i64,
) -> Result<Vec<File>, DomainError> {
let owner_str = owner_id.to_string();
let rows: Vec<FileRow> = if let Some(fid) = folder_id {
sqlx::query_as(
r#"
@@ -491,7 +494,7 @@ impl FileReadPort for FileBlobReadRepository {
.bind(fid)
.bind(limit)
.bind(offset)
.bind(owner_id)
.bind(&owner_str)
.fetch_all(self.pool.as_ref())
.await
} else {
@@ -512,7 +515,7 @@ impl FileReadPort for FileBlobReadRepository {
)
.bind(limit)
.bind(offset)
.bind(owner_id)
.bind(&owner_str)
.fetch_all(self.pool.as_ref())
.await
}
@@ -753,7 +756,7 @@ impl FileReadPort for FileBlobReadRepository {
&self,
folder_id: Option<&str>,
criteria: &SearchCriteriaDto,
user_id: &str,
user_id: Uuid,
) -> Result<(Vec<File>, usize), DomainError> {
let offset = criteria.offset as i64;
let limit = criteria.limit as i64;
@@ -822,7 +825,7 @@ impl FileReadPort for FileBlobReadRepository {
i64,
),
>(&sql)
.bind(user_id);
.bind(user_id.to_string());
if let Some(fid) = folder_id {
query = query.bind(fid);
@@ -867,7 +870,7 @@ impl FileReadPort for FileBlobReadRepository {
&self,
root_folder_id: Option<&str>,
criteria: &SearchCriteriaDto,
user_id: &str,
user_id: Uuid,
) -> Result<(Vec<File>, usize), DomainError> {
// When no root folder specified, delegate to existing paginated search
let root_id = match root_folder_id {
@@ -983,7 +986,7 @@ impl FileReadPort for FileBlobReadRepository {
i64,
),
>(&sql)
.bind(user_id)
.bind(user_id.to_string())
.bind(root_id);
if let Some(name) = &criteria.name_contains
@@ -1043,7 +1046,7 @@ impl FileReadPort for FileBlobReadRepository {
&self,
folder_id: Option<&str>,
criteria: &SearchCriteriaDto,
user_id: &str,
user_id: Uuid,
) -> Result<usize, DomainError> {
let (_, count) = self
.search_files_paginated(folder_id, criteria, user_id)
@@ -19,9 +19,7 @@ impl RecentItemsPgRepository {
}
impl RecentItemsRepositoryPort for RecentItemsPgRepository {
async fn get_recent_items(&self, user_id: &str, limit: i32) -> Result<Vec<RecentItemDto>> {
let user_uuid = Uuid::parse_str(user_id)?;
async fn get_recent_items(&self, user_id: Uuid, limit: i32) -> Result<Vec<RecentItemDto>> {
let rows = sqlx::query(
r#"
SELECT
@@ -39,12 +37,12 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
AND f.id = ur.item_id::UUID
LEFT JOIN storage.folders fld ON ur.item_type = 'folder'
AND fld.id = ur.item_id::UUID
WHERE ur.user_id = $1::TEXT
WHERE ur.user_id = $1
ORDER BY ur.accessed_at DESC
LIMIT $2
"#,
)
.bind(user_uuid)
.bind(user_id)
.bind(limit)
.fetch_all(&*self.db_pool)
.await
@@ -83,18 +81,16 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
Ok(items)
}
async fn upsert_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()> {
let user_uuid = Uuid::parse_str(user_id)?;
async fn upsert_access(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<()> {
sqlx::query(
r#"
INSERT INTO auth.user_recent_files (user_id, item_id, item_type, accessed_at)
VALUES ($1::TEXT, $2, $3, CURRENT_TIMESTAMP)
VALUES ($1, $2, $3, CURRENT_TIMESTAMP)
ON CONFLICT (user_id, item_id, item_type)
DO UPDATE SET accessed_at = CURRENT_TIMESTAMP
"#,
)
.bind(user_uuid)
.bind(user_id)
.bind(item_id)
.bind(item_type)
.execute(&*self.db_pool)
@@ -111,16 +107,14 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
Ok(())
}
async fn remove_item(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool> {
let user_uuid = Uuid::parse_str(user_id)?;
async fn remove_item(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<bool> {
let result = sqlx::query(
r#"
DELETE FROM auth.user_recent_files
WHERE user_id = $1::TEXT AND item_id = $2 AND item_type = $3
WHERE user_id = $1 AND item_id = $2 AND item_type = $3
"#,
)
.bind(user_uuid)
.bind(user_id)
.bind(item_id)
.bind(item_type)
.execute(&*self.db_pool)
@@ -137,16 +131,14 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
Ok(result.rows_affected() > 0)
}
async fn clear_all(&self, user_id: &str) -> Result<()> {
let user_uuid = Uuid::parse_str(user_id)?;
async fn clear_all(&self, user_id: Uuid) -> Result<()> {
sqlx::query(
r#"
DELETE FROM auth.user_recent_files
WHERE user_id = $1::TEXT
WHERE user_id = $1
"#,
)
.bind(user_uuid)
.bind(user_id)
.execute(&*self.db_pool)
.await
.map_err(|e| {
@@ -161,21 +153,19 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
Ok(())
}
async fn prune(&self, user_id: &str, max_items: i32) -> Result<()> {
let user_uuid = Uuid::parse_str(user_id)?;
async fn prune(&self, user_id: Uuid, max_items: i32) -> Result<()> {
sqlx::query(
r#"
DELETE FROM auth.user_recent_files
WHERE id IN (
SELECT id FROM auth.user_recent_files
WHERE user_id = $1::TEXT
WHERE user_id = $1
ORDER BY accessed_at DESC
OFFSET $2
)
"#,
)
.bind(user_uuid)
.bind(user_id)
.bind(max_items)
.execute(&*self.db_pool)
.await
@@ -2,6 +2,7 @@ use chrono::Utc;
use futures::future::BoxFuture;
use sqlx::{PgPool, Row};
use std::sync::Arc;
use uuid::Uuid;
use crate::application::ports::auth_ports::SessionStoragePort;
use crate::common::errors::DomainError;
@@ -104,7 +105,7 @@ impl SessionRepository for SessionPgRepository {
}
/// Gets a session by ID
async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult<Session> {
async fn get_session_by_id(&self, id: Uuid) -> SessionRepositoryResult<Session> {
let row = sqlx::query(
r#"
SELECT
@@ -165,7 +166,7 @@ impl SessionRepository for SessionPgRepository {
/// Gets all sessions for a user
async fn get_sessions_by_user_id(
&self,
user_id: &str,
user_id: Uuid,
) -> SessionRepositoryResult<Vec<Session>> {
let rows = sqlx::query(
r#"
@@ -202,8 +203,8 @@ impl SessionRepository for SessionPgRepository {
}
/// Revokes a specific session using a transaction
async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()> {
let id = session_id.to_string(); // Clone for use in closure
async fn revoke_session(&self, session_id: Uuid) -> SessionRepositoryResult<()> {
let id = session_id; // Copy for use in closure
with_transaction(&self.pool, "revoke_session", |tx| {
Box::pin(async move {
@@ -216,14 +217,14 @@ impl SessionRepository for SessionPgRepository {
RETURNING user_id
"#,
)
.bind(&id)
.bind(id)
.fetch_optional(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
// If we found the session, we can log a security event
if let Some(row) = result {
let user_id: String = row.try_get("user_id").unwrap_or_default();
let user_id: Uuid = row.try_get("user_id").unwrap_or_default();
// Log security event (in a security table)
// This is optional but shows how additional operations
@@ -238,8 +239,8 @@ impl SessionRepository for SessionPgRepository {
}
/// Revokes all sessions for a user using a transaction
async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult<u64> {
let user_id_clone = user_id.to_string(); // Clone for use in closure
async fn revoke_all_user_sessions(&self, user_id: Uuid) -> SessionRepositoryResult<u64> {
let user_id_copy = user_id; // Copy for use in closure
with_transaction(&self.pool, "revoke_all_user_sessions", |tx| {
Box::pin(async move {
@@ -251,7 +252,7 @@ impl SessionRepository for SessionPgRepository {
WHERE user_id = $1 AND revoked = false
"#,
)
.bind(&user_id_clone)
.bind(user_id_copy)
.execute(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
@@ -260,7 +261,7 @@ impl SessionRepository for SessionPgRepository {
// Log security event
if affected > 0 {
tracing::info!("Revoked {} sessions for user {}", affected, user_id_clone);
tracing::info!("Revoked {} sessions for user {}", affected, user_id_copy);
}
Ok(affected)
@@ -305,13 +306,13 @@ impl SessionStoragePort for SessionPgRepository {
.map_err(DomainError::from)
}
async fn revoke_session(&self, session_id: &str) -> Result<(), DomainError> {
async fn revoke_session(&self, session_id: Uuid) -> Result<(), DomainError> {
SessionRepository::revoke_session(self, session_id)
.await
.map_err(DomainError::from)
}
async fn revoke_all_user_sessions(&self, user_id: &str) -> Result<u64, DomainError> {
async fn revoke_all_user_sessions(&self, user_id: Uuid) -> Result<u64, DomainError> {
SessionRepository::revoke_all_user_sessions(self, user_id)
.await
.map_err(DomainError::from)
@@ -1,6 +1,7 @@
use sqlx::PgPool;
use std::collections::HashMap;
use std::sync::Arc;
use uuid::Uuid;
use crate::common::errors::{DomainError, ErrorKind};
use crate::domain::repositories::settings_repository::SettingsRepository;
@@ -60,7 +61,7 @@ impl SettingsRepository for SettingsPgRepository {
value: &str,
category: &str,
is_secret: bool,
updated_by: Option<&str>,
updated_by: Option<Uuid>,
) -> Result<(), DomainError> {
sqlx::query(
"INSERT INTO auth.admin_settings (key, value, category, is_secret, updated_by, updated_at)
@@ -102,7 +103,7 @@ impl SettingsRepository for SettingsPgRepository {
///
/// Only the first caller that inserts the row gets `rows_affected == 1`;
/// concurrent callers see 0 rows affected and receive `false`.
async fn try_claim_initialization(&self, admin_user_id: &str) -> Result<bool, DomainError> {
async fn try_claim_initialization(&self, admin_user_id: Uuid) -> Result<bool, DomainError> {
let result = sqlx::query(
"INSERT INTO auth.admin_settings (key, value, category, is_secret, updated_by, updated_at)
VALUES ('system_initialized', 'true', 'system', false, $1, NOW())
@@ -1,5 +1,6 @@
use sqlx::{PgPool, Row};
use std::sync::Arc;
use uuid::Uuid;
use crate::{
application::ports::share_ports::ShareStoragePort,
@@ -37,7 +38,7 @@ impl SharePgRepository {
/// Maps a [`sqlx::postgres::PgRow`] to the domain [`Share`] entity.
fn row_to_entity(row: &sqlx::postgres::PgRow) -> Result<Share, DomainError> {
let id: String = row
let id: Uuid = row
.try_get("id")
.map_err(|e| DomainError::internal_error("Share", format!("Failed to read id: {e}")))?;
let item_id: String = row.try_get("item_id").map_err(|e| {
@@ -58,7 +59,7 @@ impl SharePgRepository {
let created_at: i64 = row.try_get("created_at").map_err(|e| {
DomainError::internal_error("Share", format!("Failed to read created_at: {e}"))
})?;
let created_by: String = row.try_get("created_by").map_err(|e| {
let created_by: Uuid = row.try_get("created_by").map_err(|e| {
DomainError::internal_error("Share", format!("Failed to read created_by: {e}"))
})?;
let access_count: i64 = row.try_get("access_count").unwrap_or(0);
@@ -93,7 +94,7 @@ impl ShareStoragePort for SharePgRepository {
expires_at, permissions_read, permissions_write, permissions_reshare,
created_at, created_by, access_count)
VALUES
($1::UUID, $2, $3, $4, $5, $6,
($1, $2, $3, $4, $5, $6,
$7, $8, $9, $10,
$11, $12, $13)
ON CONFLICT (id) DO UPDATE SET
@@ -105,7 +106,7 @@ impl ShareStoragePort for SharePgRepository {
permissions_reshare = EXCLUDED.permissions_reshare,
access_count = EXCLUDED.access_count
RETURNING
id::TEXT, item_id, item_name, item_type, token, password_hash,
id, item_id, item_name, item_type, token, password_hash,
expires_at, permissions_read, permissions_write, permissions_reshare,
created_at, created_by, access_count
"#,
@@ -136,7 +137,7 @@ impl ShareStoragePort for SharePgRepository {
async fn find_share_by_token(&self, token: &str) -> Result<Share, DomainError> {
let row = sqlx::query(
r#"
SELECT id::TEXT, item_id, item_name, item_type, token, password_hash,
SELECT id, item_id, item_name, item_type, token, password_hash,
expires_at, permissions_read, permissions_write, permissions_reshare,
created_at, created_by, access_count
FROM storage.shares
@@ -162,16 +163,16 @@ impl ShareStoragePort for SharePgRepository {
async fn find_share_by_id_for_user(
&self,
id: &str,
user_id: &str,
id: Uuid,
user_id: Uuid,
) -> Result<Share, DomainError> {
let row = sqlx::query(
r#"
SELECT id::TEXT, item_id, item_name, item_type, token, password_hash,
SELECT id, item_id, item_name, item_type, token, password_hash,
expires_at, permissions_read, permissions_write, permissions_reshare,
created_at, created_by, access_count
FROM storage.shares
WHERE id = $1::UUID AND created_by = $2
WHERE id = $1 AND created_by = $2
"#,
)
.bind(id)
@@ -193,9 +194,9 @@ impl ShareStoragePort for SharePgRepository {
}
}
async fn delete_share_for_user(&self, id: &str, user_id: &str) -> Result<(), DomainError> {
async fn delete_share_for_user(&self, id: Uuid, user_id: Uuid) -> Result<(), DomainError> {
let result =
sqlx::query("DELETE FROM storage.shares WHERE id = $1::UUID AND created_by = $2")
sqlx::query("DELETE FROM storage.shares WHERE id = $1 AND created_by = $2")
.bind(id)
.bind(user_id)
.execute(&*self.db_pool)
@@ -220,11 +221,11 @@ impl ShareStoragePort for SharePgRepository {
&self,
item_id: &str,
item_type: &ShareItemType,
user_id: &str,
user_id: Uuid,
) -> Result<Vec<Share>, DomainError> {
let rows = sqlx::query(
r#"
SELECT id::TEXT, item_id, item_name, item_type, token, password_hash,
SELECT id, item_id, item_name, item_type, token, password_hash,
expires_at, permissions_read, permissions_write, permissions_reshare,
created_at, created_by, access_count
FROM storage.shares
@@ -256,9 +257,9 @@ impl ShareStoragePort for SharePgRepository {
permissions_write = $6,
permissions_reshare = $7,
access_count = $8
WHERE id = $1::UUID
WHERE id = $1
RETURNING
id::TEXT, item_id, item_name, item_type, token, password_hash,
id, item_id, item_name, item_type, token, password_hash,
expires_at, permissions_read, permissions_write, permissions_reshare,
created_at, created_by, access_count
"#,
@@ -289,14 +290,14 @@ impl ShareStoragePort for SharePgRepository {
async fn find_shares_by_user(
&self,
user_id: &str,
user_id: Uuid,
offset: usize,
limit: usize,
) -> Result<(Vec<Share>, usize), DomainError> {
// Single query with window function — count + rows in one roundtrip
let rows = sqlx::query(
r#"
SELECT id::TEXT, item_id, item_name, item_type, token, password_hash,
SELECT id, item_id, item_name, item_type, token, password_hash,
expires_at, permissions_read, permissions_write, permissions_reshare,
created_at, created_by, access_count,
COUNT(*) OVER() AS total_count
@@ -50,7 +50,7 @@ impl TrashDbRepository {
id: Uuid,
name: String,
item_type: String,
user_id: String,
user_id: Uuid,
trashed_at: Option<DateTime<Utc>>,
) -> TrashedItem {
let trashed_at = trashed_at.unwrap_or_else(Utc::now);
@@ -61,14 +61,12 @@ impl TrashDbRepository {
_ => TrashedItemType::File,
};
let user_uuid = Uuid::parse_str(&user_id).unwrap_or_else(|_| Uuid::nil());
// In the soft-delete model, the trash entry ID is the same as the
// original item ID since there is no separate trash table.
TrashedItem::from_raw(
id, // trash entry id (same as original)
id, // original item id
user_uuid, // owner
id, // trash entry id (same as original)
id, // original item id
user_id, // owner
item_type_enum,
name.clone(),
String::new(), // original_path — not stored separately in soft-delete model
@@ -87,7 +85,7 @@ impl TrashRepository for TrashDbRepository {
}
async fn get_trash_items(&self, user_id: &Uuid) -> Result<Vec<TrashedItem>> {
let rows = sqlx::query_as::<_, (Uuid, String, String, String, Option<DateTime<Utc>>)>(
let rows = sqlx::query_as::<_, (Uuid, String, String, Uuid, Option<DateTime<Utc>>)>(
r#"
SELECT id, name, item_type, user_id, trashed_at
FROM storage.trash_items
@@ -95,7 +93,7 @@ impl TrashRepository for TrashDbRepository {
ORDER BY trashed_at DESC
"#,
)
.bind(user_id.to_string())
.bind(user_id)
.fetch_all(self.pool.as_ref())
.await
.map_err(|e| DomainError::internal_error("TrashDb", format!("list: {e}")))?;
@@ -109,7 +107,7 @@ impl TrashRepository for TrashDbRepository {
}
async fn get_trash_item(&self, id: &Uuid, user_id: &Uuid) -> Result<Option<TrashedItem>> {
let row = sqlx::query_as::<_, (Uuid, String, String, String, Option<DateTime<Utc>>)>(
let row = sqlx::query_as::<_, (Uuid, String, String, Uuid, Option<DateTime<Utc>>)>(
r#"
SELECT id, name, item_type, user_id, trashed_at
FROM storage.trash_items
@@ -117,7 +115,7 @@ impl TrashRepository for TrashDbRepository {
"#,
)
.bind(id)
.bind(user_id.to_string())
.bind(user_id)
.fetch_optional(self.pool.as_ref())
.await
.map_err(|e| DomainError::internal_error("TrashDb", format!("get: {e}")))?;
@@ -144,14 +142,14 @@ impl TrashRepository for TrashDbRepository {
async fn clear_trash(&self, user_id: &Uuid) -> Result<()> {
// Delete all trashed files for this user
sqlx::query("DELETE FROM storage.files WHERE user_id = $1 AND is_trashed = TRUE")
.bind(user_id.to_string())
.bind(user_id)
.execute(self.pool.as_ref())
.await
.map_err(|e| DomainError::internal_error("TrashDb", format!("clear files: {e}")))?;
// Delete all trashed folders for this user
sqlx::query("DELETE FROM storage.folders WHERE user_id = $1 AND is_trashed = TRUE")
.bind(user_id.to_string())
.bind(user_id)
.execute(self.pool.as_ref())
.await
.map_err(|e| DomainError::internal_error("TrashDb", format!("clear folders: {e}")))?;
@@ -1,6 +1,7 @@
use futures::future::BoxFuture;
use sqlx::{PgPool, Row};
use std::sync::Arc;
use uuid::Uuid;
use crate::application::ports::auth_ports::UserStoragePort;
use crate::common::errors::DomainError;
@@ -101,7 +102,7 @@ impl UserRepository for UserPgRepository {
}
/// Gets a user by ID
async fn get_user_by_id(&self, id: &str) -> UserRepositoryResult<User> {
async fn get_user_by_id(&self, id: Uuid) -> UserRepositoryResult<User> {
let row = sqlx::query(
r#"
SELECT
@@ -278,7 +279,7 @@ impl UserRepository for UserPgRepository {
/// Updates only the storage usage of a user
async fn update_storage_usage(
&self,
user_id: &str,
user_id: Uuid,
usage_bytes: i64,
) -> UserRepositoryResult<()> {
sqlx::query(
@@ -300,7 +301,7 @@ impl UserRepository for UserPgRepository {
}
/// Updates the last login date
async fn update_last_login(&self, user_id: &str) -> UserRepositoryResult<()> {
async fn update_last_login(&self, user_id: Uuid) -> UserRepositoryResult<()> {
sqlx::query(
r#"
UPDATE auth.users
@@ -423,7 +424,7 @@ impl UserRepository for UserPgRepository {
/// Activates or deactivates a user
async fn set_user_active_status(
&self,
user_id: &str,
user_id: Uuid,
active: bool,
) -> UserRepositoryResult<()> {
sqlx::query(
@@ -447,7 +448,7 @@ impl UserRepository for UserPgRepository {
/// Changes a user's password
async fn change_password(
&self,
user_id: &str,
user_id: Uuid,
password_hash: &str,
) -> UserRepositoryResult<()> {
sqlx::query(
@@ -469,7 +470,7 @@ impl UserRepository for UserPgRepository {
}
/// Changes a user's role
async fn change_role(&self, user_id: &str, role: UserRole) -> UserRepositoryResult<()> {
async fn change_role(&self, user_id: Uuid, role: UserRole) -> UserRepositoryResult<()> {
// Convert the role to string for the binding
let role_str = role.to_string();
@@ -542,7 +543,7 @@ impl UserRepository for UserPgRepository {
}
/// Deletes a user
async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()> {
async fn delete_user(&self, user_id: Uuid) -> UserRepositoryResult<()> {
sqlx::query(
r#"
DELETE FROM auth.users
@@ -606,7 +607,7 @@ impl UserRepository for UserPgRepository {
/// Updates a user's storage quota
async fn update_storage_quota(
&self,
user_id: &str,
user_id: Uuid,
quota_bytes: i64,
) -> UserRepositoryResult<()> {
sqlx::query(
@@ -675,7 +676,7 @@ impl UserStoragePort for UserPgRepository {
.map_err(DomainError::from)
}
async fn get_user_by_id(&self, id: &str) -> Result<User, DomainError> {
async fn get_user_by_id(&self, id: Uuid) -> Result<User, DomainError> {
UserRepository::get_user_by_id(self, id)
.await
.map_err(DomainError::from)
@@ -701,7 +702,7 @@ impl UserStoragePort for UserPgRepository {
async fn update_storage_usage(
&self,
user_id: &str,
user_id: Uuid,
usage_bytes: i64,
) -> Result<(), DomainError> {
UserRepository::update_storage_usage(self, user_id, usage_bytes)
@@ -727,13 +728,13 @@ impl UserStoragePort for UserPgRepository {
.map_err(DomainError::from)
}
async fn delete_user(&self, user_id: &str) -> Result<(), DomainError> {
async fn delete_user(&self, user_id: Uuid) -> Result<(), DomainError> {
UserRepository::delete_user(self, user_id)
.await
.map_err(DomainError::from)
}
async fn change_password(&self, user_id: &str, password_hash: &str) -> Result<(), DomainError> {
async fn change_password(&self, user_id: Uuid, password_hash: &str) -> Result<(), DomainError> {
UserRepository::change_password(self, user_id, password_hash)
.await
.map_err(DomainError::from)
@@ -749,13 +750,13 @@ impl UserStoragePort for UserPgRepository {
.map_err(DomainError::from)
}
async fn set_user_active_status(&self, user_id: &str, active: bool) -> Result<(), DomainError> {
async fn set_user_active_status(&self, user_id: Uuid, active: bool) -> Result<(), DomainError> {
UserRepository::set_user_active_status(self, user_id, active)
.await
.map_err(DomainError::from)
}
async fn change_role(&self, user_id: &str, role: &str) -> Result<(), DomainError> {
async fn change_role(&self, user_id: Uuid, role: &str) -> Result<(), DomainError> {
let user_role = match role {
"admin" => UserRole::Admin,
_ => UserRole::User,
@@ -767,7 +768,7 @@ impl UserStoragePort for UserPgRepository {
async fn update_storage_quota(
&self,
user_id: &str,
user_id: Uuid,
quota_bytes: i64,
) -> Result<(), DomainError> {
UserRepository::update_storage_quota(self, user_id, quota_bytes)
@@ -801,7 +801,7 @@ impl ChunkedUploadService {
impl ChunkedUploadPort for ChunkedUploadService {
async fn create_session(
&self,
user_id: &str,
user_id: Uuid,
filename: String,
folder_id: Option<String>,
content_type: String,
@@ -809,7 +809,7 @@ impl ChunkedUploadPort for ChunkedUploadService {
chunk_size: Option<usize>,
) -> Result<CreateUploadResponseDto, DomainError> {
self.create_session_inner(
user_id.to_owned(),
user_id.to_string(),
filename,
folder_id,
content_type,
@@ -823,12 +823,12 @@ impl ChunkedUploadPort for ChunkedUploadService {
async fn upload_chunk(
&self,
upload_id: &str,
user_id: &str,
user_id: Uuid,
chunk_index: usize,
data: bytes::Bytes,
checksum: Option<String>,
) -> Result<ChunkUploadResponseDto, DomainError> {
self.upload_chunk_inner(upload_id, user_id, chunk_index, data, checksum)
self.upload_chunk_inner(upload_id, &user_id.to_string(), chunk_index, data, checksum)
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))
}
@@ -836,9 +836,9 @@ impl ChunkedUploadPort for ChunkedUploadService {
async fn get_status(
&self,
upload_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<UploadStatusResponseDto, DomainError> {
self.get_status_inner(upload_id, user_id)
self.get_status_inner(upload_id, &user_id.to_string())
.await
.map_err(|e| DomainError::new(ErrorKind::NotFound, "ChunkedUpload", e))
}
@@ -846,21 +846,21 @@ impl ChunkedUploadPort for ChunkedUploadService {
async fn complete_upload(
&self,
upload_id: &str,
user_id: &str,
user_id: Uuid,
) -> Result<(PathBuf, String, Option<String>, String, u64, String), DomainError> {
self.complete_upload_inner(upload_id, user_id)
self.complete_upload_inner(upload_id, &user_id.to_string())
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))
}
async fn finalize_upload(&self, upload_id: &str, user_id: &str) -> Result<(), DomainError> {
self.finalize_upload_inner(upload_id, user_id)
async fn finalize_upload(&self, upload_id: &str, user_id: Uuid) -> Result<(), DomainError> {
self.finalize_upload_inner(upload_id, &user_id.to_string())
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))
}
async fn cancel_upload(&self, upload_id: &str, user_id: &str) -> Result<(), DomainError> {
self.cancel_upload_inner(upload_id, user_id)
async fn cancel_upload(&self, upload_id: &str, user_id: Uuid) -> Result<(), DomainError> {
self.cancel_upload_inner(upload_id, &user_id.to_string())
.await
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))
}
@@ -7,6 +7,7 @@
use sqlx::PgPool;
use std::sync::Arc;
use uuid::Uuid;
use crate::application::dtos::display_helpers::{
category_for, format_file_size, icon_class_for, icon_special_class_for,
@@ -39,7 +40,7 @@ impl PathResolverService {
pub async fn resolve_path_for_user(
&self,
path: &str,
user_id: &str,
user_id: Uuid,
) -> Result<ResolvedResource, DomainError> {
let path = path.trim_start_matches('/').trim_end_matches('/');
if path.is_empty() {
@@ -180,7 +181,7 @@ impl PathResolverService {
}
/// Returns `true` if the resource at `path` belongs to `user_id`.
pub async fn exists_for_user(&self, path: &str, user_id: &str) -> Result<bool, DomainError> {
pub async fn exists_for_user(&self, path: &str, user_id: Uuid) -> Result<bool, DomainError> {
let path = path.trim_start_matches('/').trim_end_matches('/');
if path.is_empty() {
return Ok(false);
+23 -10
View File
@@ -15,6 +15,7 @@ use crate::application::ports::auth_ports::TokenServicePort;
use crate::common::di::AppState;
use crate::interfaces::errors::AppError;
use std::sync::Arc;
use uuid::Uuid;
/// Admin API routes — all require admin role.
pub fn admin_routes() -> Router<Arc<AppState>> {
@@ -41,7 +42,7 @@ pub fn admin_routes() -> Router<Arc<AppState>> {
}
/// Validate JWT and require admin role. Returns (user_id, role).
async fn admin_guard(state: &AppState, headers: &HeaderMap) -> Result<(String, String), AppError> {
async fn admin_guard(state: &AppState, headers: &HeaderMap) -> Result<(Uuid, String), AppError> {
let auth = state
.auth_service
.as_ref()
@@ -72,7 +73,7 @@ async fn admin_guard(state: &AppState, headers: &HeaderMap) -> Result<(String, S
));
}
Ok((claims.sub, claims.role))
Ok((Uuid::parse_str(&claims.sub).map_err(|_| AppError::internal_error("Invalid user ID in token"))?, claims.role))
}
/// GET /api/admin/settings/oidc — get OIDC settings for the admin panel
@@ -108,7 +109,7 @@ async fn save_oidc_settings(
.as_ref()
.ok_or_else(|| AppError::internal_error("Admin settings service not available"))?;
svc.save_oidc_settings(dto, &user_id)
svc.save_oidc_settings(dto, user_id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to save settings: {}", e)))?;
@@ -292,6 +293,8 @@ async fn get_user(
) -> Result<impl IntoResponse, AppError> {
admin_guard(&state, &headers).await?;
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
let auth = state
.auth_service
.as_ref()
@@ -299,7 +302,7 @@ async fn get_user(
let user = auth
.auth_application_service
.get_user_admin(&id)
.get_user_admin(id)
.await
.map_err(|e| AppError::not_found(format!("User not found: {}", e)))?;
@@ -314,6 +317,8 @@ async fn delete_user(
) -> Result<impl IntoResponse, AppError> {
let (admin_id, _) = admin_guard(&state, &headers).await?;
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
// Prevent self-deletion
if admin_id == id {
return Err(AppError::new(
@@ -329,7 +334,7 @@ async fn delete_user(
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
auth.auth_application_service
.delete_user_admin(&id)
.delete_user_admin(id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to delete user: {}", e)))?;
@@ -350,6 +355,8 @@ async fn update_user_role(
) -> Result<impl IntoResponse, AppError> {
let (admin_id, _) = admin_guard(&state, &headers).await?;
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
// Prevent changing own role
if admin_id == id {
return Err(AppError::new(
@@ -365,7 +372,7 @@ async fn update_user_role(
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
auth.auth_application_service
.change_user_role(&id, &dto.role)
.change_user_role(id, &dto.role)
.await
.map_err(|e| AppError::internal_error(format!("Failed to change role: {}", e)))?;
@@ -386,6 +393,8 @@ async fn update_user_active(
) -> Result<impl IntoResponse, AppError> {
let (admin_id, _) = admin_guard(&state, &headers).await?;
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
// Prevent deactivating yourself
if admin_id == id && !dto.active {
return Err(AppError::new(
@@ -401,7 +410,7 @@ async fn update_user_active(
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
auth.auth_application_service
.set_user_active(&id, dto.active)
.set_user_active(id, dto.active)
.await
.map_err(|e| AppError::internal_error(format!("Failed to update user status: {}", e)))?;
@@ -427,13 +436,15 @@ async fn update_user_quota(
) -> Result<impl IntoResponse, AppError> {
admin_guard(&state, &headers).await?;
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
let auth = state
.auth_service
.as_ref()
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
auth.auth_application_service
.update_user_quota(&id, dto.quota_bytes)
.update_user_quota(id, dto.quota_bytes)
.await
.map_err(|e| AppError::internal_error(format!("Failed to update quota: {}", e)))?;
@@ -487,13 +498,15 @@ async fn reset_user_password(
) -> Result<impl IntoResponse, AppError> {
admin_guard(&state, &headers).await?;
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
let auth = state
.auth_service
.as_ref()
.ok_or_else(|| AppError::internal_error("Auth service not configured"))?;
auth.auth_application_service
.admin_reset_password(&id, &dto.new_password)
.admin_reset_password(id, &dto.new_password)
.await
.map_err(|e| {
AppError::new(
@@ -558,7 +571,7 @@ async fn set_registration_setting(
.as_ref()
.ok_or_else(|| AppError::internal_error("Admin settings service not available"))?;
svc.set_registration_enabled(enabled, &admin_id)
svc.set_registration_enabled(enabled, admin_id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to save setting: {}", e)))?;
@@ -11,6 +11,7 @@ use axum::extract::State;
use axum::routing::{delete, get, post};
use axum::{Json, Router};
use std::sync::Arc;
use uuid::Uuid;
/// Protected routes — require JWT auth middleware.
pub fn app_password_routes() -> Router<Arc<AppState>> {
@@ -35,7 +36,7 @@ async fn create_app_password(
.ok_or_else(|| AppError::internal_error("App password service not configured"))?;
let response = service
.create(&user.id, request)
.create(user.id, request)
.await
.map_err(AppError::from)?;
@@ -55,7 +56,7 @@ async fn list_app_passwords(
.as_ref()
.ok_or_else(|| AppError::internal_error("App password service not configured"))?;
let response = service.list(&user.id).await.map_err(AppError::from)?;
let response = service.list(user.id).await.map_err(AppError::from)?;
Ok(Json(response))
}
@@ -72,8 +73,10 @@ async fn revoke_app_password(
.as_ref()
.ok_or_else(|| AppError::internal_error("App password service not configured"))?;
let id = Uuid::parse_str(&id).map_err(|_| AppError::bad_request("Invalid UUID"))?;
let response = service
.revoke(&user.id, &id)
.revoke(user.id, id)
.await
.map_err(AppError::from)?;
+10 -8
View File
@@ -6,6 +6,7 @@ use axum::{
routing::{get, post, put},
};
use std::sync::Arc;
use uuid::Uuid;
use crate::application::dtos::user_dto::{
ChangePasswordDto, LoginDto, OidcCallbackQueryDto, OidcExchangeDto, OidcProviderInfoDto,
@@ -273,7 +274,7 @@ async fn get_current_user(
if let Some(storage_usage_service) = state.storage_usage_service.as_ref() {
// Calculate storage synchronously (we await the result)
match storage_usage_service
.update_user_storage_usage(&user_id)
.update_user_storage_usage(user_id)
.await
{
Ok(usage) => {
@@ -293,7 +294,7 @@ async fn get_current_user(
// Now get the user data WITH the updated storage
let user = auth_service
.auth_application_service
.get_user_by_id(&user_id)
.get_user_by_id(user_id)
.await?;
Ok((StatusCode::OK, Json(user)))
@@ -311,7 +312,7 @@ async fn change_password(
auth_service
.auth_application_service
.change_password(&user_id, dto)
.change_password(user_id, dto)
.await?;
Ok(StatusCode::OK)
@@ -341,7 +342,7 @@ async fn logout(
auth_service
.auth_application_service
.logout(&user_id, &refresh_token)
.logout(user_id, &refresh_token)
.await?;
// Clear HttpOnly + CSRF cookies so the browser forgets the session
@@ -393,10 +394,10 @@ async fn setup_admin(
}
// 4. ATOMIC: claim initialization — only one concurrent request can win.
// We use a placeholder user_id ("pending") because the admin user
// We use Uuid::nil() as a placeholder because the admin user
// doesn't exist yet. It will be updated to the real id below.
let claimed = admin_svc
.try_claim_initialization("pending")
.try_claim_initialization(Uuid::nil())
.await
.map_err(|e| {
tracing::error!("Failed to claim system initialization: {}", e);
@@ -426,7 +427,8 @@ async fn setup_admin(
})?;
// 5. Update the initialization record with the real admin user_id
if let Err(e) = admin_svc.mark_system_initialized(&user.id).await {
let real_user_id = Uuid::parse_str(&user.id).unwrap_or_default();
if let Err(e) = admin_svc.mark_system_initialized(real_user_id).await {
// Not fatal — the claim already prevents concurrent re-initialization,
// and the "pending" marker is still "true" so the system stays locked.
tracing::error!(
@@ -603,7 +605,7 @@ async fn oidc_callback(
let (_id, app_password) = nextcloud
.app_passwords
.create_nc(&user_id, "Nextcloud (OIDC)")
.create_nc(user_id, "Nextcloud (OIDC)")
.await
.map_err(|e| {
tracing::error!(error = %e, user = %username, "OIDC+NC: failed to create app password");
+11 -11
View File
@@ -160,7 +160,7 @@ pub async fn move_files_batch(
// Execute batch operation
let result = state
.batch_service
.move_files(request.file_ids, request.target_folder_id, &auth_user.id)
.move_files(request.file_ids, request.target_folder_id, auth_user.id)
.await
.map_err(|e| {
tracing::error!("Batch move_files failed: {}", e);
@@ -216,7 +216,7 @@ pub async fn copy_files_batch(
// Execute batch operation
let result = state
.batch_service
.copy_files(request.file_ids, request.target_folder_id, &auth_user.id)
.copy_files(request.file_ids, request.target_folder_id, auth_user.id)
.await
.map_err(|e| {
tracing::error!("Batch copy_files failed: {}", e);
@@ -272,7 +272,7 @@ pub async fn delete_files_batch(
// Execute batch operation
let result = state
.batch_service
.delete_files(request.file_ids, &auth_user.id)
.delete_files(request.file_ids, auth_user.id)
.await
.map_err(|e| {
tracing::error!("Batch delete_files failed: {}", e);
@@ -336,7 +336,7 @@ pub async fn delete_folders_batch(
// Execute batch operation
let result = state
.batch_service
.delete_folders(request.folder_ids, request.recursive, &auth_user.id)
.delete_folders(request.folder_ids, request.recursive, auth_user.id)
.await
.map_err(|e| {
tracing::error!("Batch delete_folders failed: {}", e);
@@ -407,7 +407,7 @@ pub async fn create_folders_batch(
// Execute batch operation
let result = state
.batch_service
.create_folders(folders, &auth_user.id)
.create_folders(folders, auth_user.id)
.await
.map_err(|e| {
tracing::error!("Batch create_folders failed: {}", e);
@@ -463,7 +463,7 @@ pub async fn get_files_batch(
// Execute batch operation
let result = state
.batch_service
.get_multiple_files(request.file_ids, &auth_user.id)
.get_multiple_files(request.file_ids, auth_user.id)
.await
.map_err(|e| {
tracing::error!("Batch get_files failed: {}", e);
@@ -519,7 +519,7 @@ pub async fn get_folders_batch(
// Execute batch operation
let result = state
.batch_service
.get_multiple_folders(request.folder_ids, &auth_user.id)
.get_multiple_folders(request.folder_ids, auth_user.id)
.await
.map_err(|e| {
tracing::error!("Batch get_folders failed: {}", e);
@@ -603,7 +603,7 @@ pub async fn trash_batch(
if !request.file_ids.is_empty() {
match state
.batch_service
.trash_files(request.file_ids, &auth_user.id)
.trash_files(request.file_ids, auth_user.id)
.await
{
Ok(result) => {
@@ -630,7 +630,7 @@ pub async fn trash_batch(
if !request.folder_ids.is_empty() {
match state
.batch_service
.trash_folders(request.folder_ids, &auth_user.id)
.trash_folders(request.folder_ids, auth_user.id)
.await
{
Ok(result) => {
@@ -707,7 +707,7 @@ pub async fn move_folders_batch(
let result = state
.batch_service
.move_folders(request.folder_ids, request.target_folder_id, &auth_user.id)
.move_folders(request.folder_ids, request.target_folder_id, auth_user.id)
.await
.map_err(|e| {
tracing::error!("Batch move_folders failed: {}", e);
@@ -760,7 +760,7 @@ pub async fn download_batch(
let temp_file = state
.batch_service
.download_zip(request.file_ids, request.folder_ids, &auth_user.id)
.download_zip(request.file_ids, request.folder_ids, auth_user.id)
.await
.map_err(|e| {
tracing::error!("Batch download ZIP failed: {}", e);
+24 -24
View File
@@ -209,7 +209,7 @@ async fn handle_propfind(
vec![]
} else {
calendar_service
.list_my_calendars(&user.id)
.list_my_calendars(user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to list calendars: {}", e)))?
};
@@ -264,13 +264,13 @@ async fn handle_propfind(
if parts.len() == 1 {
// Single path segment: try as calendar ID first, fall back to user home
let calendar_result = calendar_service.get_calendar(first_segment, &user.id).await;
let calendar_result = calendar_service.get_calendar(first_segment, user.id).await;
if let Ok(calendar) = calendar_result {
// Valid calendar ID — return calendar collection
let events = if depth != "0" {
calendar_service
.list_events(first_segment, None, None, &user.id)
.list_events(first_segment, None, None, user.id)
.await
.unwrap_or_default()
} else {
@@ -300,7 +300,7 @@ async fn handle_propfind(
// List all calendars for this user
let calendars =
calendar_service
.list_my_calendars(&user.id)
.list_my_calendars(user.id)
.await
.map_err(|e| {
AppError::internal_error(format!("Failed to list calendars: {}", e))
@@ -328,7 +328,7 @@ async fn handle_propfind(
let rest = parts[1];
// Check if first_segment is a valid calendar ID
let calendar_result = calendar_service.get_calendar(first_segment, &user.id).await;
let calendar_result = calendar_service.get_calendar(first_segment, user.id).await;
let (calendar_id, event_path) = if calendar_result.is_ok() {
// first_segment is a calendar ID, rest is event path
@@ -341,13 +341,13 @@ async fn handle_propfind(
// /caldav/{username}/{calendar_id}
// Try to get this as a calendar collection
let cal = calendar_service
.get_calendar(sub_parts[0], &user.id)
.get_calendar(sub_parts[0], user.id)
.await
.map_err(|e| AppError::not_found(format!("Calendar not found: {}", e)))?;
let events = if depth != "0" {
calendar_service
.list_events(sub_parts[0], None, None, &user.id)
.list_events(sub_parts[0], None, None, user.id)
.await
.unwrap_or_default()
} else {
@@ -384,7 +384,7 @@ async fn handle_propfind(
let ical_uid = event_path.trim_end_matches(".ics");
let events = calendar_service
.list_events(calendar_id, None, None, &user.id)
.list_events(calendar_id, None, None, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?;
@@ -444,14 +444,14 @@ async fn handle_report(
CalDavReportType::CalendarQuery { time_range, .. } => {
if let Some((start, end)) = time_range {
calendar_service
.get_events_in_range(calendar_id, *start, *end, &user.id)
.get_events_in_range(calendar_id, *start, *end, user.id)
.await
.map_err(|e| {
AppError::internal_error(format!("Failed to query events: {}", e))
})?
} else {
calendar_service
.list_events(calendar_id, None, None, &user.id)
.list_events(calendar_id, None, None, user.id)
.await
.map_err(|e| {
AppError::internal_error(format!("Failed to list events: {}", e))
@@ -460,7 +460,7 @@ async fn handle_report(
}
CalDavReportType::CalendarMultiget { hrefs, .. } => {
let all_events = calendar_service
.list_events(calendar_id, None, None, &user.id)
.list_events(calendar_id, None, None, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?;
@@ -470,7 +470,7 @@ async fn handle_report(
.collect()
}
CalDavReportType::SyncCollection { .. } => calendar_service
.list_events(calendar_id, None, None, &user.id)
.list_events(calendar_id, None, None, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?,
};
@@ -526,7 +526,7 @@ async fn handle_mkcalendar(
};
calendar_service
.create_calendar(create_dto, &user.id)
.create_calendar(create_dto, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to create calendar: {}", e)))?;
@@ -566,7 +566,7 @@ async fn handle_put(
let existing = if let Some(ref uid) = ical_uid {
let events = calendar_service
.list_events(calendar_id, None, None, &user.id)
.list_events(calendar_id, None, None, user.id)
.await
.unwrap_or_default();
events.into_iter().find(|e| e.ical_uid == *uid)
@@ -577,7 +577,7 @@ async fn handle_put(
if let Some(existing_event) = existing {
// Update existing event — re-create from iCal for full fidelity
calendar_service
.delete_event(&existing_event.id, &user.id)
.delete_event(&existing_event.id, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to update event: {}", e)))?;
@@ -586,7 +586,7 @@ async fn handle_put(
ical_data,
};
let event = calendar_service
.create_event_from_ical(create_dto, &user.id)
.create_event_from_ical(create_dto, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to recreate event: {}", e)))?;
@@ -602,7 +602,7 @@ async fn handle_put(
};
let event = calendar_service
.create_event_from_ical(create_dto, &user.id)
.create_event_from_ical(create_dto, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to create event: {}", e)))?;
@@ -641,12 +641,12 @@ async fn handle_get(
if parts.len() < 2 {
// GET on calendar collection
let events = calendar_service
.list_events(calendar_id, None, None, &user.id)
.list_events(calendar_id, None, None, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?;
let calendar = calendar_service
.get_calendar(calendar_id, &user.id)
.get_calendar(calendar_id, user.id)
.await
.map_err(|e| AppError::not_found(format!("Calendar not found: {}", e)))?;
@@ -664,7 +664,7 @@ async fn handle_get(
let ical_uid = event_file.trim_end_matches(".ics");
let events = calendar_service
.list_events(calendar_id, None, None, &user.id)
.list_events(calendar_id, None, None, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?;
@@ -760,7 +760,7 @@ async fn handle_delete(
if parts.len() < 2 {
calendar_service
.delete_calendar(calendar_id, &user.id)
.delete_calendar(calendar_id, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to delete calendar: {}", e)))?;
} else {
@@ -768,7 +768,7 @@ async fn handle_delete(
let ical_uid = event_file.trim_end_matches(".ics");
let events = calendar_service
.list_events(calendar_id, None, None, &user.id)
.list_events(calendar_id, None, None, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?;
@@ -778,7 +778,7 @@ async fn handle_delete(
.ok_or_else(|| AppError::not_found(format!("Event not found: {}", ical_uid)))?;
calendar_service
.delete_event(&event.id, &user.id)
.delete_event(&event.id, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to delete event: {}", e)))?;
}
@@ -833,7 +833,7 @@ async fn handle_proppatch(
if update.name.is_some() || update.description.is_some() || update.color.is_some() {
calendar_service
.update_calendar(calendar_id, update, &user.id)
.update_calendar(calendar_id, update, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to update calendar: {}", e)))?;
}
+18 -18
View File
@@ -203,7 +203,7 @@ async fn handle_propfind(
if path.is_empty() {
// Root CardDAV path — list user's address books
let address_books = addressbook_service
.list_user_address_books(&user.id)
.list_user_address_books(user.id)
.await
.map_err(|e| {
AppError::internal_error(format!("Failed to list address books: {}", e))
@@ -231,13 +231,13 @@ async fn handle_propfind(
if parts.len() == 1 {
// Address book collection
let address_book = addressbook_service
.get_address_book(address_book_id, &user.id)
.get_address_book(address_book_id, user.id)
.await
.map_err(|e| AppError::not_found(format!("Address book not found: {}", e)))?;
let contacts = if depth != "0" {
contact_svc
.list_contacts(address_book_id, &user.id)
.list_contacts(address_book_id, user.id)
.await
.unwrap_or_default()
} else {
@@ -269,7 +269,7 @@ async fn handle_propfind(
// Look up by UID across all contacts in this address book
let contacts = contact_svc
.list_contacts(address_book_id, &user.id)
.list_contacts(address_book_id, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?;
@@ -331,12 +331,12 @@ async fn handle_report(
let contacts = match &report {
CardDavReportType::AddressbookQuery { .. } => contact_svc
.list_contacts(address_book_id, &user.id)
.list_contacts(address_book_id, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?,
CardDavReportType::AddressbookMultiget { hrefs, .. } => {
let all_contacts = contact_svc
.list_contacts(address_book_id, &user.id)
.list_contacts(address_book_id, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?;
@@ -346,7 +346,7 @@ async fn handle_report(
.collect()
}
CardDavReportType::SyncCollection { .. } => contact_svc
.list_contacts(address_book_id, &user.id)
.list_contacts(address_book_id, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?,
};
@@ -403,7 +403,7 @@ async fn handle_mkcol(
let create_dto = CreateAddressBookDto {
name,
owner_id: user.id.clone(),
owner_id: user.id.to_string(),
description,
color,
is_public: Some(false),
@@ -452,7 +452,7 @@ async fn handle_put(
// Check if contact already exists
let existing = if let Some(ref uid) = vcard_uid {
let contacts = contact_svc
.list_contacts(address_book_id, &user.id)
.list_contacts(address_book_id, user.id)
.await
.unwrap_or_default();
contacts.into_iter().find(|c| c.uid == *uid)
@@ -463,14 +463,14 @@ async fn handle_put(
if let Some(existing_contact) = existing {
// Update: delete + recreate from vCard
contact_svc
.delete_contact(&existing_contact.id, &user.id)
.delete_contact(&existing_contact.id, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to update contact: {}", e)))?;
let create_dto = CreateContactVCardDto {
address_book_id: address_book_id.to_string(),
vcard: vcard_data,
user_id: user.id.clone(),
user_id: user.id.to_string(),
};
let contact = contact_svc
.create_contact_from_vcard(create_dto)
@@ -486,7 +486,7 @@ async fn handle_put(
let create_dto = CreateContactVCardDto {
address_book_id: address_book_id.to_string(),
vcard: vcard_data,
user_id: user.id.clone(),
user_id: user.id.to_string(),
};
let contact = contact_svc
@@ -529,7 +529,7 @@ async fn handle_get(
if parts.len() < 2 {
// GET on address book collection — return all contacts as vcf
let contacts = contact_svc
.list_contacts(address_book_id, &user.id)
.list_contacts(address_book_id, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?;
@@ -549,7 +549,7 @@ async fn handle_get(
let contact_uid = contact_file.trim_end_matches(".vcf");
let contacts = contact_svc
.list_contacts(address_book_id, &user.id)
.list_contacts(address_book_id, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?;
@@ -590,7 +590,7 @@ async fn handle_delete(
if parts.len() < 2 {
// Delete address book
addressbook_service
.delete_address_book(address_book_id, &user.id)
.delete_address_book(address_book_id, user.id)
.await
.map_err(|e| {
AppError::internal_error(format!("Failed to delete address book: {}", e))
@@ -601,7 +601,7 @@ async fn handle_delete(
let contact_uid = contact_file.trim_end_matches(".vcf");
let contacts = contact_svc
.list_contacts(address_book_id, &user.id)
.list_contacts(address_book_id, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?;
@@ -611,7 +611,7 @@ async fn handle_delete(
.ok_or_else(|| AppError::not_found(format!("Contact not found: {}", contact_uid)))?;
contact_svc
.delete_contact(&contact.id, &user.id)
.delete_contact(&contact.id, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to delete contact: {}", e)))?;
}
@@ -653,7 +653,7 @@ async fn handle_proppatch(
description: None,
color: None,
is_public: None,
user_id: user.id.clone(),
user_id: user.id.to_string(),
};
for prop in &props_to_set {
@@ -108,7 +108,7 @@ impl ChunkedUploadHandler {
// ── Quota enforcement ────────────────────────────────────
if let Some(storage_svc) = state.storage_usage_service.as_ref()
&& let Err(err) = storage_svc
.check_storage_quota(&auth_user.id, request.total_size)
.check_storage_quota(auth_user.id, request.total_size)
.await
{
tracing::warn!(
@@ -146,7 +146,7 @@ impl ChunkedUploadHandler {
match chunked_service
.create_session(
&auth_user.id,
auth_user.id,
request.filename,
request.folder_id,
content_type,
@@ -192,7 +192,7 @@ impl ChunkedUploadHandler {
match chunked_service
.upload_chunk(
&upload_id,
&auth_user.id,
auth_user.id,
params.chunk_index,
body,
checksum,
@@ -233,7 +233,7 @@ impl ChunkedUploadHandler {
) -> impl IntoResponse {
let chunked_service = &state.core.chunked_upload_service;
match chunked_service.get_status(&upload_id, &auth_user.id).await {
match chunked_service.get_status(&upload_id, auth_user.id).await {
Ok(status) => Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "application/json")
@@ -268,7 +268,7 @@ impl ChunkedUploadHandler {
// Assemble chunks (hash-on-write: SHA-256 computed during assembly)
let (assembled_path, filename, folder_id, content_type, total_size, hash) =
match chunked_service
.complete_upload(&upload_id, &auth_user.id)
.complete_upload(&upload_id, auth_user.id)
.await
{
Ok(result) => result,
@@ -299,7 +299,7 @@ impl ChunkedUploadHandler {
Ok(file) => {
// Cleanup session
let _ = chunked_service
.finalize_upload(&upload_id, &auth_user.id)
.finalize_upload(&upload_id, auth_user.id)
.await;
tracing::info!(
@@ -338,7 +338,7 @@ impl ChunkedUploadHandler {
let chunked_service = &state.core.chunked_upload_service;
match chunked_service
.cancel_upload(&upload_id, &auth_user.id)
.cancel_upload(&upload_id, auth_user.id)
.await
{
Ok(_) => StatusCode::NO_CONTENT.into_response(),
+2 -2
View File
@@ -99,7 +99,7 @@ impl DedupHandler {
}
// Only reveal whether THIS user has the blob — no global oracle
let user_has_it = dedup.user_owns_blob_reference(&hash, &auth_user.id).await;
let user_has_it = dedup.user_owns_blob_reference(&hash, &auth_user.id.to_string()).await;
if user_has_it {
// Fetch size from metadata (safe — user owns a reference)
@@ -346,7 +346,7 @@ impl DedupHandler {
}
// Verify the user owns at least one file referencing this blob
if !dedup.user_owns_blob_reference(&hash, &auth_user.id).await {
if !dedup.user_owns_blob_reference(&hash, &auth_user.id.to_string()).await {
return Response::builder()
.status(StatusCode::NOT_FOUND)
.header(header::CONTENT_TYPE, "application/json")
@@ -16,6 +16,7 @@ use axum::{
routing::{delete, get, post},
};
use std::sync::Arc;
use uuid::Uuid;
use crate::application::dtos::device_auth_dto::*;
use crate::application::services::device_auth_service::DeviceAuthService;
@@ -145,7 +146,7 @@ async fn device_verify_action(
match body.action.to_lowercase().as_str() {
"approve" | "allow" | "accept" => {
device_service
.approve(&body.user_code, &auth_user.id)
.approve(&body.user_code, auth_user.id)
.await
.map_err(|e| {
tracing::error!("Device approve failed: {}", e);
@@ -181,7 +182,7 @@ async fn list_devices(
let device_service = get_device_service(&state)?;
let devices = device_service
.list_user_devices(&auth_user.id)
.list_user_devices(auth_user.id)
.await
.map_err(|e| {
tracing::error!("List devices failed: {}", e);
@@ -202,8 +203,10 @@ async fn revoke_device(
) -> Result<impl IntoResponse, AppError> {
let device_service = get_device_service(&state)?;
let device_id = Uuid::parse_str(&device_id).map_err(|_| AppError::bad_request("Invalid device ID"))?;
device_service
.revoke_device(&device_id, &auth_user.id)
.revoke_device(device_id, auth_user.id)
.await
.map_err(|e| {
tracing::error!("Revoke device failed: {}", e);
@@ -30,7 +30,7 @@ pub async fn get_favorites(
State(favorites_service): State<Arc<FavoritesService>>,
auth_user: AuthUser,
) -> impl IntoResponse {
let user_id = &auth_user.id;
let user_id = auth_user.id;
match favorites_service.get_favorites(user_id).await {
Ok(favorites) => {
@@ -56,7 +56,7 @@ pub async fn add_favorite(
auth_user: AuthUser,
Path((item_type, item_id)): Path<(String, String)>,
) -> impl IntoResponse {
let user_id = &auth_user.id;
let user_id = auth_user.id;
// Validate item_type
if item_type != "file" && item_type != "folder" {
@@ -99,7 +99,7 @@ pub async fn remove_favorite(
auth_user: AuthUser,
Path((item_type, item_id)): Path<(String, String)>,
) -> impl IntoResponse {
let user_id = &auth_user.id;
let user_id = auth_user.id;
match favorites_service
.remove_from_favorites(user_id, &item_id, &item_type)
@@ -143,7 +143,7 @@ pub async fn batch_add_favorites(
auth_user: AuthUser,
Json(body): Json<BatchFavoritesRequest>,
) -> impl IntoResponse {
let user_id = &auth_user.id;
let user_id = auth_user.id;
if body.items.is_empty() {
return (
+12 -12
View File
@@ -109,7 +109,7 @@ impl FileHandler {
if let Some(ref fid) = folder_id {
use crate::application::ports::inbound::FolderUseCase;
let folder_service = &state.applications.folder_service;
if folder_service.get_folder_owned(fid, &auth_user.id).await.is_err() {
if folder_service.get_folder_owned(fid, auth_user.id).await.is_err() {
tracing::warn!(
"⛔ UPLOAD REJECTED (IDOR): user='{}' attempted upload to folder '{}' owned by another user",
auth_user.username,
@@ -130,7 +130,7 @@ impl FileHandler {
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(0);
if let Err(err) = storage_svc
.check_storage_quota(&auth_user.id, estimated_size)
.check_storage_quota(auth_user.id, estimated_size)
.await
{
tracing::warn!(
@@ -235,7 +235,7 @@ impl FileHandler {
// ── Quota enforcement ────────────────────────────────
if let Some(storage_svc) = state.storage_usage_service.as_ref()
&& let Err(err) = storage_svc
.check_storage_quota(&auth_user.id, total_size)
.check_storage_quota(auth_user.id, total_size)
.await
{
let _ = tokio::fs::remove_file(&temp_path).await;
@@ -321,7 +321,7 @@ impl FileHandler {
};
let file = match file_retrieval_service
.get_file_owned(&id, &auth_user.id)
.get_file_owned(&id, auth_user.id)
.await
{
Ok(f) => f,
@@ -397,7 +397,7 @@ impl FileHandler {
let retrieval = &state.applications.file_retrieval_service;
// ── Get file metadata (ownership-scoped) ────────────────────────
let file_dto = match retrieval.get_file_owned(&id, &auth_user.id).await {
let file_dto = match retrieval.get_file_owned(&id, auth_user.id).await {
Ok(f) => f,
Err(err) => {
return AppError::from(err).into_response();
@@ -455,7 +455,7 @@ impl FileHandler {
Self::content_disposition(&file_dto.name, &file_dto.mime_type, &params);
match retrieval
.get_file_range_stream_owned(&id, &auth_user.id, start, Some(end + 1))
.get_file_range_stream_owned(&id, auth_user.id, start, Some(end + 1))
.await
{
Ok(stream) => {
@@ -569,7 +569,7 @@ impl FileHandler {
tracing::info!("API: Listing files with folder_id: {:?}", folder_id);
let retrieval = &state.applications.file_retrieval_service;
match retrieval.list_files_owned(folder_id, &auth_user.id).await {
match retrieval.list_files_owned(folder_id, auth_user.id).await {
Ok(files) => {
// Compute lightweight ETag from max modified_at + count
let max_mod = files.iter().map(|f| f.modified_at).max().unwrap_or(0);
@@ -655,7 +655,7 @@ impl FileHandler {
) -> impl IntoResponse {
// Verify ownership
let file_read = &state.repositories.file_read_repository;
if let Err(e) = file_read.verify_file_owner(&file_id, &auth_user.id).await {
if let Err(e) = file_read.verify_file_owner(&file_id, auth_user.id).await {
let msg = e.to_string();
return (
StatusCode::NOT_FOUND,
@@ -703,7 +703,7 @@ impl FileHandler {
// Auth required: trash-first with dedup cleanup + ownership verification
let result = mgmt
.delete_with_cleanup(&id, &auth_user.id)
.delete_with_cleanup(&id, auth_user.id)
.await
.map(|was_trashed| {
if was_trashed {
@@ -745,7 +745,7 @@ impl FileHandler {
tracing::info!("Renaming file {} to \"{}\"", id, new_name);
let mgmt = &state.applications.file_management_service;
match mgmt.rename_file_owned(&id, &auth_user.id, &new_name).await {
match mgmt.rename_file_owned(&id, auth_user.id, &new_name).await {
Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(),
Err(err) => AppError::from(err).into_response()
}
@@ -763,7 +763,7 @@ impl FileHandler {
let mgmt = &state.applications.file_management_service;
match mgmt
.move_file_owned(&id, &auth_user.id, payload.folder_id)
.move_file_owned(&id, auth_user.id, payload.folder_id)
.await
{
Ok(file) => (StatusCode::OK, Json(file)).into_response(),
@@ -784,7 +784,7 @@ impl FileHandler {
.map(|s| s.to_string());
let mgmt = &state.applications.file_management_service;
match mgmt.move_file_owned(&id, &auth_user.id, folder_id).await {
match mgmt.move_file_owned(&id, auth_user.id, folder_id).await {
Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(),
Err(err) => AppError::from(err).into_response()
}
+12 -12
View File
@@ -42,7 +42,7 @@ impl FolderHandler {
"create_folder: parent_id is None for user '{}', resolving home folder",
auth_user.username
);
match service.list_folders_for_owner(None, &auth_user.id).await {
match service.list_folders_for_owner(None, auth_user.id).await {
Ok(folders) => {
if let Some(home) = folders.first() {
tracing::info!(
@@ -71,7 +71,7 @@ impl FolderHandler {
if let Some(ref parent_id) = dto.parent_id {
use crate::application::ports::inbound::FolderUseCase;
if service
.get_folder_owned(parent_id, &auth_user.id)
.get_folder_owned(parent_id, auth_user.id)
.await
.is_err()
{
@@ -102,7 +102,7 @@ impl FolderHandler {
Ok(folder) => {
// Access check: folder must belong to the requesting user
if let Some(ref owner) = folder.owner_id
&& owner != &auth_user.id
&& owner != &auth_user.id.to_string()
{
tracing::warn!(
"get_folder: user '{}' attempted to access folder '{}' owned by '{}'",
@@ -155,7 +155,7 @@ impl FolderHandler {
pagination: Query<PaginationRequestDto>,
) -> axum::response::Response {
match service
.list_folders_for_owner_paginated(Some(&id), &auth_user.id, &pagination)
.list_folders_for_owner_paginated(Some(&id), auth_user.id, &pagination)
.await
{
Ok(paginated_result) => (StatusCode::OK, Json(paginated_result)).into_response(),
@@ -172,7 +172,7 @@ impl FolderHandler {
auth_user: &AuthUser,
) -> axum::response::Response {
match service
.list_folders_for_owner(parent_id, &auth_user.id)
.list_folders_for_owner(parent_id, auth_user.id)
.await
{
Ok(folders) => (StatusCode::OK, Json(folders)).into_response(),
@@ -215,8 +215,8 @@ impl FolderHandler {
// Run both queries concurrently — no sequential wait.
let (folders_result, files_result) = tokio::join!(
folder_service.list_folders_for_owner(Some(&id), &auth_user.id),
file_service.list_files_owned(Some(&id), &auth_user.id)
folder_service.list_folders_for_owner(Some(&id), auth_user.id),
file_service.list_files_owned(Some(&id), auth_user.id)
);
match (folders_result, files_result) {
@@ -253,7 +253,7 @@ impl FolderHandler {
Path(id): Path<String>,
Json(dto): Json<RenameFolderDto>,
) -> impl IntoResponse {
match service.rename_folder(&id, dto, &auth_user.id).await {
match service.rename_folder(&id, dto, auth_user.id).await {
Ok(folder) => (StatusCode::OK, Json(folder)).into_response(),
Err(err) => AppError::from(err).into_response(),
}
@@ -266,7 +266,7 @@ impl FolderHandler {
Path(id): Path<String>,
Json(dto): Json<MoveFolderDto>,
) -> impl IntoResponse {
match service.move_folder(&id, dto, &auth_user.id).await {
match service.move_folder(&id, dto, auth_user.id).await {
Ok(folder) => (StatusCode::OK, Json(folder)).into_response(),
Err(err) => AppError::from(err).into_response(),
}
@@ -278,7 +278,7 @@ impl FolderHandler {
auth_user: AuthUser,
Path(id): Path<String>,
) -> impl IntoResponse {
match service.delete_folder(&id, &auth_user.id).await {
match service.delete_folder(&id, auth_user.id).await {
Ok(_) => StatusCode::NO_CONTENT.into_response(),
Err(err) => AppError::from(err).into_response(),
}
@@ -290,7 +290,7 @@ impl FolderHandler {
auth_user: AuthUser,
Path(id): Path<String>,
) -> impl IntoResponse {
let user_id = &auth_user.id;
let user_id = auth_user.id;
// Check if trash service is available
if let Some(trash_service) = &state.trash_service {
tracing::info!("Moving folder to trash: {}", id);
@@ -337,7 +337,7 @@ impl FolderHandler {
match folder_service.get_folder(&id).await {
Ok(folder) => {
// Access check: folder must belong to the requesting user
if folder.owner_id.as_deref() != Some(&auth_user.id) {
if folder.owner_id.as_deref() != Some(&auth_user.id.to_string()) {
tracing::warn!(
"download_folder_zip: user '{}' attempted to download folder '{}' owned by '{:?}'",
auth_user.id,
@@ -31,7 +31,7 @@ pub async fn list_photos(
auth_user: AuthUser,
Query(params): Query<PhotosQueryParams>,
) -> impl IntoResponse {
let user_id = &auth_user.id;
let user_id = auth_user.id;
let limit = params.limit.unwrap_or(200).clamp(1, 500);
let file_read = &state.repositories.file_read_repository;
@@ -25,7 +25,7 @@ pub async fn get_recent_items(
auth_user: AuthUser,
Query(params): Query<GetRecentParams>,
) -> impl IntoResponse {
let user_id = &auth_user.id;
let user_id = auth_user.id;
match recent_service.get_recent_items(user_id, params.limit).await {
Ok(items) => {
@@ -51,7 +51,7 @@ pub async fn record_item_access(
auth_user: AuthUser,
Path((item_type, item_id)): Path<(String, String)>,
) -> impl IntoResponse {
let user_id = &auth_user.id;
let user_id = auth_user.id;
// Validate item type
if item_type != "file" && item_type != "folder" {
@@ -97,7 +97,7 @@ pub async fn remove_from_recent(
auth_user: AuthUser,
Path((item_type, item_id)): Path<(String, String)>,
) -> impl IntoResponse {
let user_id = &auth_user.id;
let user_id = auth_user.id;
match recent_service
.remove_from_recent(user_id, &item_id, &item_type)
@@ -142,7 +142,7 @@ pub async fn clear_recent_items(
State(recent_service): State<Arc<RecentService>>,
auth_user: AuthUser,
) -> impl IntoResponse {
let user_id = &auth_user.id;
let user_id = auth_user.id;
match recent_service.clear_recent_items(user_id).await {
Ok(_) => {
@@ -60,7 +60,7 @@ impl SearchHandler {
sort_by: params.sort_by.unwrap_or_else(|| "relevance".to_string()),
};
match search_service.search(search_criteria, &auth_user.id).await {
match search_service.search(search_criteria, auth_user.id).await {
Ok(results) => {
info!(
"Search completed in {}ms — {} files, {} folders",
@@ -101,7 +101,7 @@ impl SearchHandler {
}
};
match search_service.search(criteria, &auth_user.id).await {
match search_service.search(criteria, auth_user.id).await {
Ok(results) => {
info!(
"Advanced search completed in {}ms — {} files, {} folders",
+18 -5
View File
@@ -1,4 +1,5 @@
use std::sync::Arc;
use uuid::Uuid;
use axum::{
Json,
@@ -40,7 +41,7 @@ pub async fn create_shared_link(
auth_user: AuthUser,
Json(dto): Json<CreateShareDto>,
) -> impl IntoResponse {
match share_use_case.create_shared_link(&auth_user.id, dto).await {
match share_use_case.create_shared_link(auth_user.id, dto).await {
Ok(share) => (StatusCode::CREATED, Json(share)).into_response(),
Err(err) => AppError::from(err).into_response(),
}
@@ -52,7 +53,11 @@ pub async fn get_shared_link(
auth_user: AuthUser,
Path(id): Path<String>,
) -> impl IntoResponse {
match share_use_case.get_shared_link(&id, &auth_user.id).await {
let id = match Uuid::parse_str(&id) {
Ok(id) => id,
Err(_) => return AppError::bad_request("Invalid UUID").into_response(),
};
match share_use_case.get_shared_link(id, auth_user.id).await {
Ok(share) => (StatusCode::OK, Json(share)).into_response(),
Err(err) => AppError::from(err).into_response(),
}
@@ -65,7 +70,7 @@ pub async fn get_user_shares(
auth_user: AuthUser,
Query(query): Query<GetSharesQuery>,
) -> impl IntoResponse {
let user_id = &auth_user.id;
let user_id = auth_user.id;
// If both item_id and item_type are provided, return shares for that specific item
if let (Some(item_id), Some(item_type_str)) = (&query.item_id, &query.item_type) {
@@ -108,8 +113,12 @@ pub async fn update_shared_link(
Path(id): Path<String>,
Json(dto): Json<UpdateShareDto>,
) -> impl IntoResponse {
let id = match Uuid::parse_str(&id) {
Ok(id) => id,
Err(_) => return AppError::bad_request("Invalid UUID").into_response(),
};
match share_use_case
.update_shared_link(&id, &auth_user.id, dto)
.update_shared_link(id, auth_user.id, dto)
.await
{
Ok(share) => (StatusCode::OK, Json(share)).into_response(),
@@ -123,7 +132,11 @@ pub async fn delete_shared_link(
auth_user: AuthUser,
Path(id): Path<String>,
) -> impl IntoResponse {
match share_use_case.delete_shared_link(&id, &auth_user.id).await {
let id = match Uuid::parse_str(&id) {
Ok(id) => id,
Err(_) => return AppError::bad_request("Invalid UUID").into_response(),
};
match share_use_case.delete_shared_link(id, auth_user.id).await {
Ok(_) => StatusCode::NO_CONTENT.into_response(),
Err(err) => AppError::from(err).into_response(),
}
+7 -7
View File
@@ -18,7 +18,7 @@ pub async fn get_trash_items(
// SECURITY: Always use the authenticated user's ID from the JWT token.
// Never allow user ID override via query parameters to prevent
// privilege escalation attacks.
let effective_user = auth_user.id.clone();
let effective_user = auth_user.id;
debug!("Request to list trash items for user {}", effective_user);
@@ -34,7 +34,7 @@ pub async fn get_trash_items(
}
};
let result = trash_service.get_trash_items(&effective_user).await;
let result = trash_service.get_trash_items(effective_user).await;
match result {
Ok(items) => {
@@ -60,7 +60,7 @@ pub async fn move_file_to_trash(
auth_user: AuthUser,
Path(item_id): Path<String>,
) -> (StatusCode, Json<serde_json::Value>) {
let user_id = &auth_user.id;
let user_id = auth_user.id;
debug!(
"Request to move file to trash: id={}, user={}",
item_id, user_id
@@ -111,7 +111,7 @@ pub async fn move_folder_to_trash(
auth_user: AuthUser,
Path(item_id): Path<String>,
) -> (StatusCode, Json<serde_json::Value>) {
let user_id = &auth_user.id;
let user_id = auth_user.id;
debug!(
"Request to move folder to trash: id={}, user={}",
item_id, user_id
@@ -177,7 +177,7 @@ pub async fn restore_from_trash(
);
}
};
let result = trash_service.restore_item(&trash_id, &auth_user.id).await;
let result = trash_service.restore_item(&trash_id, auth_user.id).await;
match result {
Ok(_) => {
@@ -239,7 +239,7 @@ pub async fn delete_permanently(
}
};
let result = trash_service
.delete_permanently(&trash_id, &auth_user.id)
.delete_permanently(&trash_id, auth_user.id)
.await;
match result {
@@ -300,7 +300,7 @@ pub async fn empty_trash(
);
}
};
let result = trash_service.empty_trash(&auth_user.id).await;
let result = trash_service.empty_trash(auth_user.id).await;
match result {
Ok(_) => {
+45 -46
View File
@@ -183,7 +183,7 @@ async fn handle_webdav_methods(
/// If `path` doesn't already start with the user's home folder name, prepend
/// the home folder path so downstream services can find the resource in the DB.
/// Returns `None` when the path already includes the prefix or resolution fails.
async fn resolve_webdav_path(state: &Arc<AppState>, user_id: &str, path: &str) -> Option<String> {
async fn resolve_webdav_path(state: &Arc<AppState>, user_id: Uuid, path: &str) -> Option<String> {
let folder_service = &state.applications.folder_service;
let home_folders = folder_service
.list_folders_for_owner(None, user_id)
@@ -209,9 +209,9 @@ async fn handle_webdav_dispatch(
// prefix when the path doesn't already include it.
// Extract user_id before any async call to keep the future Send.
let path = if !path.is_empty() && method.as_str() != "OPTIONS" {
let user_id = req.extensions().get::<Arc<CurrentUser>>().map(|u| u.id.clone());
let user_id = req.extensions().get::<Arc<CurrentUser>>().map(|u| u.id);
if let Some(uid) = user_id {
resolve_webdav_path(&state, &uid, &path)
resolve_webdav_path(&state, uid, &path)
.await
.unwrap_or(path)
} else {
@@ -371,14 +371,14 @@ async fn handle_propfind(
propfind_request,
folder_service,
file_retrieval_service,
&user.id,
user.id,
)
.await;
}
// Single-query path resolution: folder OR file in one DB round-trip
if let Some(resolver) = &state.path_resolver {
match resolver.resolve_path_for_user(&path, &user.id).await {
match resolver.resolve_path_for_user(&path, user.id).await {
Ok(ResolvedResource::Folder(folder)) => {
let folder_id = folder.id.clone();
return build_streaming_propfind_response(
@@ -389,7 +389,7 @@ async fn handle_propfind(
propfind_request,
folder_service,
file_retrieval_service,
&user.id,
user.id,
)
.await;
}
@@ -420,7 +420,7 @@ async fn handle_propfind(
} else {
// Fallback: legacy double-query path when PathResolver is unavailable
if let Ok(folder) = folder_service.get_folder_by_path(&path).await {
assert_owner(folder.owner_id.as_deref(), &user.id, &path)?;
assert_owner(folder.owner_id.as_deref(), &user.id.to_string(), &path)?;
let folder_id = folder.id.clone();
return build_streaming_propfind_response(
folder,
@@ -430,12 +430,12 @@ async fn handle_propfind(
propfind_request,
folder_service,
file_retrieval_service,
&user.id,
user.id,
)
.await;
}
if let Ok(file) = file_retrieval_service.get_file_by_path(&path).await {
assert_owner(file.owner_id.as_deref(), &user.id, &path)?;
assert_owner(file.owner_id.as_deref(), &user.id.to_string(), &path)?;
let mut buf = Vec::with_capacity(1024);
{
let mut xml_writer = Writer::new(&mut buf);
@@ -477,12 +477,11 @@ async fn build_streaming_propfind_response(
propfind_request: PropFindRequest,
folder_service: std::sync::Arc<FolderService>,
file_retrieval_service: std::sync::Arc<FileRetrievalService>,
user_id: &str,
user_id: Uuid,
) -> Result<Response<Body>, AppError> {
let depth = depth.to_string();
let base_href = base_href.to_string();
let propfind_request = Arc::new(propfind_request);
let user_id = user_id.to_string();
let stream = async_stream::try_stream! {
// ── XML header + <D:multistatus> + folder entry ──────────
@@ -512,7 +511,7 @@ async fn build_streaming_propfind_response(
page_size: pagination.page_size,
};
let result = folder_service
.list_folders_for_owner_paginated(fid_ref, &user_id, &pag)
.list_folders_for_owner_paginated(fid_ref, user_id, &pag)
.await
.map_err(|e| std::io::Error::other(e.to_string()))?;
@@ -542,7 +541,7 @@ async fn build_streaming_propfind_response(
let mut offset: i64 = 0;
loop {
let batch: Vec<FileDto> = file_retrieval_service
.list_files_batch_for_owner(fid_ref, &user_id, offset, PROPFIND_BATCH_SIZE)
.list_files_batch_for_owner(fid_ref, user_id, offset, PROPFIND_BATCH_SIZE)
.await
.map_err(|e| std::io::Error::other(e.to_string()))?;
@@ -675,7 +674,7 @@ async fn handle_get(
// Resolve file — user-scoped when PathResolver is available
let file = if let Some(resolver) = &state.path_resolver {
match resolver.resolve_path_for_user(&path, &user.id).await {
match resolver.resolve_path_for_user(&path, user.id).await {
Ok(ResolvedResource::File(f)) => f,
Ok(ResolvedResource::Folder(_)) => {
return Err(AppError::bad_request("Cannot GET a directory"));
@@ -690,7 +689,7 @@ async fn handle_get(
.get_file_by_path(&path)
.await
.map_err(|_e| AppError::not_found(format!("File not found: {}", path)))?;
assert_owner(f.owner_id.as_deref(), &user.id, &path)?;
assert_owner(f.owner_id.as_deref(), &user.id.to_string(), &path)?;
f
};
@@ -740,7 +739,7 @@ async fn handle_head(
// Single-query path resolution (user-scoped)
if let Some(resolver) = &state.path_resolver {
match resolver.resolve_path_for_user(&path, &user.id).await {
match resolver.resolve_path_for_user(&path, user.id).await {
Ok(ResolvedResource::Folder(folder)) => {
return Ok(Response::builder()
.status(StatusCode::OK)
@@ -771,7 +770,7 @@ async fn handle_head(
// Fallback: legacy double-query path (with ownership check)
if let Ok(folder) = folder_service.get_folder_by_path(&path).await {
assert_owner(folder.owner_id.as_deref(), &user.id, &path)?;
assert_owner(folder.owner_id.as_deref(), &user.id.to_string(), &path)?;
return Ok(Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "httpd/unix-directory")
@@ -786,7 +785,7 @@ async fn handle_head(
.get_file_by_path(&path)
.await
.map_err(|_e| AppError::not_found(format!("Resource not found: {}", path)))?;
assert_owner(file.owner_id.as_deref(), &user.id, &path)?;
assert_owner(file.owner_id.as_deref(), &user.id.to_string(), &path)?;
Ok(Response::builder()
.status(StatusCode::OK)
@@ -840,7 +839,7 @@ async fn handle_put(
// parent folder (create). Without this check a user could
// overwrite another user's file via a crafted PUT path.
if let Some(resolver) = &state.path_resolver {
match resolver.resolve_path_for_user(&path, &user.id).await {
match resolver.resolve_path_for_user(&path, user.id).await {
Ok(ResolvedResource::File(_)) => { /* existing file owned by user — OK */ }
Ok(ResolvedResource::Folder(_)) => {
return Err(AppError::bad_request("Cannot PUT to a directory"));
@@ -854,7 +853,7 @@ async fn handle_put(
};
if !parent_path.is_empty() {
resolver
.resolve_path_for_user(parent_path, &user.id)
.resolve_path_for_user(parent_path, user.id)
.await
.map_err(|_| {
AppError::not_found(format!("Parent folder not found: {}", parent_path))
@@ -1052,10 +1051,10 @@ async fn handle_delete(
// Single-query path resolution (user-scoped)
if let Some(resolver) = &state.path_resolver {
match resolver.resolve_path_for_user(&path, &user.id).await {
match resolver.resolve_path_for_user(&path, user.id).await {
Ok(ResolvedResource::Folder(folder)) => {
folder_service
.delete_folder(&folder.id, &user.id)
.delete_folder(&folder.id, user.id)
.await
.map_err(|e| {
AppError::internal_error(format!("Failed to delete folder: {}", e))
@@ -1076,9 +1075,9 @@ async fn handle_delete(
let folder_result = folder_service.get_folder_by_path(&path).await;
if let Ok(folder) = folder_result {
assert_owner(folder.owner_id.as_deref(), &user.id, &path)?;
assert_owner(folder.owner_id.as_deref(), &user.id.to_string(), &path)?;
folder_service
.delete_folder(&folder.id, &user.id)
.delete_folder(&folder.id, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to delete folder: {}", e)))?;
} else {
@@ -1086,7 +1085,7 @@ async fn handle_delete(
.get_file_by_path(&path)
.await
.map_err(|_e| AppError::not_found(format!("Resource not found: {}", path)))?;
assert_owner(file.owner_id.as_deref(), &user.id, &path)?;
assert_owner(file.owner_id.as_deref(), &user.id.to_string(), &path)?;
file_management_service
.delete_file(&file.id)
@@ -1157,7 +1156,7 @@ async fn handle_move(
if !overwrite {
let dest_exists = if let Some(resolver) = &state.path_resolver {
resolver
.exists_for_user(&destination_path, &user.id)
.exists_for_user(&destination_path, user.id)
.await
.unwrap_or(false)
} else {
@@ -1179,7 +1178,7 @@ async fn handle_move(
// Resolve source: single-query when PathResolver is available (user-scoped)
if let Some(resolver) = &state.path_resolver {
match resolver.resolve_path_for_user(&source_path, &user.id).await {
match resolver.resolve_path_for_user(&source_path, user.id).await {
Ok(ResolvedResource::Folder(folder)) => {
let dest_folder_name = destination_path
.split('/')
@@ -1200,7 +1199,7 @@ async fn handle_move(
// SECURITY: verify destination parent belongs to caller (V-08)
assert_owner(
parent.owner_id.as_deref(),
&user.id,
&user.id.to_string(),
dest_parent_path,
)?;
Some(parent.id)
@@ -1211,7 +1210,7 @@ async fn handle_move(
};
folder_service
.move_folder(&folder.id, move_dto, &user.id)
.move_folder(&folder.id, move_dto, user.id)
.await
.map_err(|e| {
AppError::internal_error(format!("Failed to move folder: {}", e))
@@ -1222,7 +1221,7 @@ async fn handle_move(
name: dest_folder_name.to_string(),
};
folder_service
.rename_folder(&folder.id, rename_dto, &user.id)
.rename_folder(&folder.id, rename_dto, user.id)
.await
.map_err(|e| {
AppError::internal_error(format!("Failed to rename folder: {}", e))
@@ -1251,7 +1250,7 @@ async fn handle_move(
&& let Ok(parent) =
folder_service.get_folder_by_path(dest_parent_path).await
{
assert_owner(parent.owner_id.as_deref(), &user.id, dest_parent_path)?;
assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?;
}
file_management_service
.move_file(&file.id, Some(dest_parent_path.to_string()))
@@ -1281,7 +1280,7 @@ async fn handle_move(
let folder_result = folder_service.get_folder_by_path(&source_path).await;
if let Ok(folder) = folder_result {
assert_owner(folder.owner_id.as_deref(), &user.id, &source_path)?;
assert_owner(folder.owner_id.as_deref(), &user.id.to_string(), &source_path)?;
let dest_folder_name = destination_path
.split('/')
.next_back()
@@ -1299,7 +1298,7 @@ async fn handle_move(
match folder_service.get_folder_by_path(dest_parent_path).await {
Ok(parent) => {
// SECURITY: verify destination parent belongs to caller (V-08)
assert_owner(parent.owner_id.as_deref(), &user.id, dest_parent_path)?;
assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?;
Some(parent.id)
}
Err(_) => None,
@@ -1308,7 +1307,7 @@ async fn handle_move(
};
folder_service
.move_folder(&folder.id, move_dto, &user.id)
.move_folder(&folder.id, move_dto, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to move folder: {}", e)))?;
@@ -1317,7 +1316,7 @@ async fn handle_move(
name: dest_folder_name.to_string(),
};
folder_service
.rename_folder(&folder.id, rename_dto, &user.id)
.rename_folder(&folder.id, rename_dto, user.id)
.await
.map_err(|e| {
AppError::internal_error(format!("Failed to rename folder: {}", e))
@@ -1330,7 +1329,7 @@ async fn handle_move(
.map_err(|_e| {
AppError::not_found(format!("Resource not found: {}", source_path))
})?;
assert_owner(file.owner_id.as_deref(), &user.id, &source_path)?;
assert_owner(file.owner_id.as_deref(), &user.id.to_string(), &source_path)?;
let dest_filename = destination_path
.split('/')
@@ -1352,7 +1351,7 @@ async fn handle_move(
if !dest_parent_path.is_empty()
&& let Ok(parent) = folder_service.get_folder_by_path(dest_parent_path).await
{
assert_owner(parent.owner_id.as_deref(), &user.id, dest_parent_path)?;
assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?;
}
file_management_service
.move_file(&file.id, Some(dest_parent_path.to_string()))
@@ -1438,7 +1437,7 @@ async fn handle_copy(
if !overwrite {
let dest_exists = if let Some(resolver) = &state.path_resolver {
resolver
.exists_for_user(&destination_path, &user.id)
.exists_for_user(&destination_path, user.id)
.await
.unwrap_or(false)
} else {
@@ -1460,7 +1459,7 @@ async fn handle_copy(
// Resolve source: single-query when PathResolver is available (user-scoped)
if let Some(resolver) = &state.path_resolver {
match resolver.resolve_path_for_user(&source_path, &user.id).await {
match resolver.resolve_path_for_user(&source_path, user.id).await {
Ok(ResolvedResource::Folder(folder)) => {
let recursive = depth != "0";
@@ -1480,7 +1479,7 @@ async fn handle_copy(
match folder_service.get_folder_by_path(dest_parent_path).await {
Ok(parent) => {
// SECURITY: verify destination parent belongs to caller (V-08)
assert_owner(parent.owner_id.as_deref(), &user.id, dest_parent_path)?;
assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?;
Some(parent.id)
}
Err(_) => None,
@@ -1528,7 +1527,7 @@ async fn handle_copy(
match folder_service.get_folder_by_path(dest_parent_path).await {
Ok(parent) => {
// SECURITY: verify destination parent belongs to caller (V-08)
assert_owner(parent.owner_id.as_deref(), &user.id, dest_parent_path)?;
assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?;
Some(parent.id)
}
Err(_) => None,
@@ -1553,7 +1552,7 @@ async fn handle_copy(
let folder_result = folder_service.get_folder_by_path(&source_path).await;
if let Ok(folder) = folder_result {
assert_owner(folder.owner_id.as_deref(), &user.id, &source_path)?;
assert_owner(folder.owner_id.as_deref(), &user.id.to_string(), &source_path)?;
let recursive = depth != "0";
let dest_folder_name = destination_path
@@ -1572,7 +1571,7 @@ async fn handle_copy(
match folder_service.get_folder_by_path(dest_parent_path).await {
Ok(parent) => {
// SECURITY: verify destination parent belongs to caller (V-08)
assert_owner(parent.owner_id.as_deref(), &user.id, dest_parent_path)?;
assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?;
Some(parent.id)
}
Err(_) => None,
@@ -1613,7 +1612,7 @@ async fn handle_copy(
.map_err(|_e| {
AppError::not_found(format!("Resource not found: {}", source_path))
})?;
assert_owner(file.owner_id.as_deref(), &user.id, &source_path)?;
assert_owner(file.owner_id.as_deref(), &user.id.to_string(), &source_path)?;
let dest_parent_path = if let Some(idx) = destination_path.rfind('/') {
&destination_path[..idx]
@@ -1627,7 +1626,7 @@ async fn handle_copy(
match folder_service.get_folder_by_path(dest_parent_path).await {
Ok(parent) => {
// SECURITY: verify destination parent belongs to caller (V-08)
assert_owner(parent.owner_id.as_deref(), &user.id, dest_parent_path)?;
assert_owner(parent.owner_id.as_deref(), &user.id.to_string(), dest_parent_path)?;
Some(parent.id)
}
Err(_) => None,
@@ -1737,7 +1736,7 @@ async fn handle_lock(
let token = format!("opaquelocktoken:{}", Uuid::new_v4());
let lock_info = LockInfo {
token,
owner: owner.or(Some(user.id.clone())),
owner: owner.or(Some(user.id.to_string())),
depth: depth.to_string(),
timeout,
scope,
+8 -4
View File
@@ -396,7 +396,7 @@ pub struct EditorUrlResponse {
async fn authorize_wopi_access<S: FileRetrievalUseCase>(
file_retrieval: &S,
file_id: &str,
caller_id: &str,
caller_id: uuid::Uuid,
requested_action: &str,
) -> Result<(crate::application::dtos::file_dto::FileDto, bool), StatusCode> {
let file = file_retrieval
@@ -425,7 +425,7 @@ pub async fn get_editor_url(
let (file, can_write) = match authorize_wopi_access(
state.app_state.applications.file_retrieval_service.as_ref(),
&params.file_id,
&user_id,
user_id,
&params.action,
)
.await
@@ -464,7 +464,7 @@ pub async fn get_editor_url(
let (access_token, access_token_ttl) =
match state
.token_service
.generate_token(&params.file_id, &user_id, &username, can_write)
.generate_token(&params.file_id, &user_id.to_string(), &username, can_write)
{
Ok(t) => t,
Err(e) => {
@@ -503,10 +503,14 @@ async fn host_page(
// Re-verify ownership even though the token was valid — defence in depth.
let requested_action = if claims.can_write { "edit" } else { "view" };
let caller_uuid = match uuid::Uuid::parse_str(&claims.sub) {
Ok(u) => u,
Err(_) => return StatusCode::UNAUTHORIZED.into_response(),
};
let file = match authorize_wopi_access(
state.app_state.applications.file_retrieval_service.as_ref(),
&file_id,
&claims.sub,
caller_uuid,
requested_action,
)
.await
+16 -9
View File
@@ -6,6 +6,7 @@ use axum::{
};
use std::convert::Infallible;
use std::sync::Arc;
use uuid::Uuid;
use crate::common::di::AppState;
@@ -22,7 +23,7 @@ pub struct CookieAuthenticated;
// Structure for use in Axum extractors
#[derive(Clone, Debug)]
pub struct AuthUser {
pub id: String,
pub id: Uuid,
pub username: String,
pub role: String,
}
@@ -35,7 +36,7 @@ pub struct AuthUser {
/// async fn my_handler(CurrentUserId(user_id): CurrentUserId) -> impl IntoResponse { ... }
/// ```
#[derive(Clone, Debug)]
pub struct CurrentUserId(pub String);
pub struct CurrentUserId(pub Uuid);
// Implement FromRequestParts for AuthUser — allows using `auth_user: AuthUser` in handlers
impl<S> FromRequestParts<S> for AuthUser
@@ -49,7 +50,7 @@ where
.extensions
.get::<Arc<CurrentUser>>()
.map(|cu| AuthUser {
id: cu.id.clone(),
id: cu.id,
username: cu.username.clone(),
role: cu.role.clone(),
})
@@ -86,7 +87,7 @@ where
parts
.extensions
.get::<Arc<CurrentUser>>()
.map(|cu| CurrentUserId(cu.id.clone()))
.map(|cu| CurrentUserId(cu.id))
.ok_or(AuthError::UserNotFound)
}
}
@@ -94,7 +95,7 @@ where
/// Optional user ID extractor – never fails.
/// Yields `Some(id)` when auth middleware ran, `None` otherwise.
#[derive(Clone, Debug)]
pub struct OptionalUserId(pub Option<String>);
pub struct OptionalUserId(pub Option<Uuid>);
impl<S> FromRequestParts<S> for OptionalUserId
where
@@ -107,7 +108,7 @@ where
parts
.extensions
.get::<Arc<CurrentUser>>()
.map(|cu| cu.id.clone()),
.map(|cu| cu.id),
))
}
}
@@ -126,7 +127,7 @@ where
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
Ok(OptionalAuthUser(parts.extensions.get::<Arc<CurrentUser>>().map(
|cu| AuthUser {
id: cu.id.clone(),
id: cu.id,
username: cu.username.clone(),
role: cu.role.clone(),
},
@@ -216,8 +217,11 @@ pub async fn auth_middleware(
"Token validated successfully for user: {}",
claims.username
);
let user_id = Uuid::parse_str(&claims.sub).map_err(|_| {
AuthError::InvalidToken("Invalid user ID in token".to_string())
})?;
let current_user = Arc::new(CurrentUser {
id: claims.sub,
id: user_id,
username: claims.username,
email: claims.email,
role: claims.role,
@@ -303,8 +307,11 @@ pub async fn auth_middleware(
match token_service.validate_token(&token_str) {
Ok(claims) => {
tracing::debug!("Cookie token validated for user: {}", claims.username);
let user_id = Uuid::parse_str(&claims.sub).map_err(|_| {
AuthError::InvalidToken("Invalid user ID in token".to_string())
})?;
let current_user = Arc::new(CurrentUser {
id: claims.sub,
id: user_id,
username: claims.username,
email: claims.email,
role: claims.role,
+1 -1
View File
@@ -174,7 +174,7 @@ pub async fn handle_login_submit(
let app_password = match nextcloud
.app_passwords
.create_nc(&current_user.id, "Nextcloud")
.create_nc(current_user.id, "Nextcloud")
.await
{
Ok((_id, password)) => password,
+4 -4
View File
@@ -47,7 +47,7 @@ pub async fn handle_capabilities_v2(State(state): State<Arc<AppState>>) -> Respo
pub async fn handle_user_info(State(state): State<Arc<AppState>>, user: CurrentUser) -> Response {
let quota: (i64, i64) = match state.storage_usage_service.as_ref() {
Some(service) => match service.get_user_storage_info(&user.id).await {
Some(service) => match service.get_user_storage_info(user.id).await {
Ok((used, total)) => (used, total),
Err(_) => (0, 0),
},
@@ -151,7 +151,7 @@ async fn user_provisioning_response(
// Fetch quota from storage usage service
let quota: (i64, i64) = match state.storage_usage_service.as_ref() {
Some(service) => match service.get_user_storage_info(&user_dto.id).await {
Some(service) => match service.get_user_storage_info(uuid::Uuid::parse_str(&user_dto.id).unwrap_or_default()).await {
Ok((used, total)) => (used, total),
Err(_) => (0, 0),
},
@@ -212,7 +212,7 @@ pub async fn handle_revoke_apppassword(
if let Err(e) = nextcloud
.app_passwords
.revoke_by_password(&user.id, &app_password)
.revoke_by_password(user.id, &app_password)
.await
{
tracing::warn!("Failed to revoke app password for {}: {}", user.id, e);
@@ -367,7 +367,7 @@ pub async fn handle_search(
..SearchCriteriaDto::default()
};
let results = match search_service.search(criteria, &user.id).await {
let results = match search_service.search(criteria, user.id).await {
Ok(r) => r,
Err(_) => return empty_search_response().into_response(),
};
+2 -1
View File
@@ -90,7 +90,8 @@ pub async fn handle_preview(
};
// Verify the authenticated user owns this file
if file.owner_id.as_deref() != Some(&user.id) {
let user_id_str = user.id.to_string();
if file.owner_id.as_deref() != Some(user_id_str.as_str()) {
return Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::from("File not found"))
+2 -2
View File
@@ -67,7 +67,7 @@ async fn handle_filter_files(
};
let favorites = fav_svc
.get_favorites(&user.id)
.get_favorites(user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to get favorites: {}", e)))?;
@@ -179,7 +179,7 @@ async fn handle_search(
};
let results = search_svc
.search(criteria, &user.id)
.search(criteria, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Search failed: {}", e)))?;
+4 -4
View File
@@ -75,7 +75,7 @@ async fn handle_propfind(
.ok_or_else(|| AppError::internal_error("Trash service not available"))?;
let items = trash_svc
.get_trash_items(&user.id)
.get_trash_items(user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to list trash: {}", e)))?;
@@ -109,7 +109,7 @@ async fn handle_restore(
.ok_or_else(|| AppError::internal_error("Trash service not available"))?;
trash_svc
.restore_item(&id, &user.id)
.restore_item(&id, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to restore item: {}", e)))?;
@@ -131,7 +131,7 @@ async fn handle_empty_trash(
.ok_or_else(|| AppError::internal_error("Trash service not available"))?;
trash_svc
.empty_trash(&user.id)
.empty_trash(user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to empty trash: {}", e)))?;
@@ -156,7 +156,7 @@ async fn handle_delete_permanent(
.ok_or_else(|| AppError::internal_error("Trash service not available"))?;
trash_svc
.delete_permanently(&id, &user.id)
.delete_permanently(&id, user.id)
.await
.map_err(|e| {
AppError::internal_error(format!("Failed to permanently delete item: {}", e))
+10 -10
View File
@@ -193,7 +193,7 @@ async fn handle_propfind(
items.push((&sf.id, "folder"));
}
fav_svc
.batch_check_favorites(&user.id, &items)
.batch_check_favorites(user.id, &items)
.await
.unwrap_or_default()
} else {
@@ -234,7 +234,7 @@ async fn handle_propfind(
let favorite_ids = if let Some(fav_svc) = state.favorites_service.as_ref() {
let items: Vec<(&str, &str)> = vec![(&file.id, "file")];
fav_svc
.batch_check_favorites(&user.id, &items)
.batch_check_favorites(user.id, &items)
.await
.unwrap_or_default()
} else {
@@ -414,14 +414,14 @@ async fn handle_proppatch(
if let Some(fav_svc) = state.favorites_service.as_ref() {
if value == 1 {
fav_svc
.add_to_favorites(&user.id, &item_id, item_type)
.add_to_favorites(user.id, &item_id, item_type)
.await
.map_err(|e| {
AppError::internal_error(format!("Failed to add favorite: {}", e))
})?;
} else {
fav_svc
.remove_from_favorites(&user.id, &item_id, item_type)
.remove_from_favorites(user.id, &item_id, item_type)
.await
.map_err(|e| {
AppError::internal_error(format!("Failed to remove favorite: {}", e))
@@ -685,7 +685,7 @@ async fn handle_delete(
if let Some(trash_svc) = state.trash_service.as_ref() {
if let Ok(folder) = folder_service.get_folder_by_path(&internal_path).await {
trash_svc
.move_to_trash(&folder.id, "folder", &user.id)
.move_to_trash(&folder.id, "folder", user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to trash folder: {}", e)))?;
return Ok(Response::builder()
@@ -695,7 +695,7 @@ async fn handle_delete(
}
if let Ok(file) = file_service.get_file_by_path(&internal_path).await {
trash_svc
.move_to_trash(&file.id, "file", &user.id)
.move_to_trash(&file.id, "file", user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to trash file: {}", e)))?;
return Ok(Response::builder()
@@ -711,7 +711,7 @@ async fn handle_delete(
if let Ok(folder) = folder_service.get_folder_by_path(&internal_path).await {
folder_service
.delete_folder(&folder.id, &user.id)
.delete_folder(&folder.id, user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to delete folder: {}", e)))?;
@@ -835,7 +835,7 @@ async fn handle_move(
RenameFolderDto {
name: dest_name.to_string(),
},
&user.id,
user.id,
)
.await
.map_err(|e| AppError::internal_error(format!("Rename failed: {}", e)))?;
@@ -853,7 +853,7 @@ async fn handle_move(
MoveFolderDto {
parent_id: Some(dest_parent.id.clone()),
},
&user.id,
user.id,
)
.await
.map_err(|e| AppError::internal_error(format!("Move failed: {}", e)))?;
@@ -867,7 +867,7 @@ async fn handle_move(
RenameFolderDto {
name: dest_name.to_string(),
},
&user.id,
user.id,
)
.await
.map_err(|e| AppError::internal_error(format!("Rename failed: {}", e)))?;