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:
@@ -1,4 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::application::dtos::settings_dto::{
|
||||
OidcSettingsDto, OidcTestResultDto, SaveOidcSettingsDto, TestOidcConnectionDto,
|
||||
@@ -181,7 +182,7 @@ impl AdminSettingsService {
|
||||
pub async fn save_oidc_settings(
|
||||
&self,
|
||||
dto: SaveOidcSettingsDto,
|
||||
updated_by: &str,
|
||||
updated_by: Uuid,
|
||||
) -> Result<(), DomainError> {
|
||||
let cat = "oidc";
|
||||
let by = Some(updated_by);
|
||||
@@ -366,7 +367,7 @@ impl AdminSettingsService {
|
||||
}
|
||||
|
||||
/// Mark the system as initialized after the first admin is created.
|
||||
pub async fn mark_system_initialized(&self, admin_user_id: &str) -> Result<(), DomainError> {
|
||||
pub async fn mark_system_initialized(&self, admin_user_id: Uuid) -> Result<(), DomainError> {
|
||||
self.settings_repo
|
||||
.set(
|
||||
"system_initialized",
|
||||
@@ -384,7 +385,7 @@ impl AdminSettingsService {
|
||||
/// initialized (the caller "won" the race), or `Ok(false)` if another
|
||||
/// request already did it. This eliminates the race-condition window
|
||||
/// between `is_system_initialized()` and `mark_system_initialized()`.
|
||||
pub async fn try_claim_initialization(&self, admin_user_id: &str) -> Result<bool, DomainError> {
|
||||
pub async fn try_claim_initialization(&self, admin_user_id: Uuid) -> Result<bool, DomainError> {
|
||||
self.settings_repo
|
||||
.try_claim_initialization(admin_user_id)
|
||||
.await
|
||||
@@ -412,7 +413,7 @@ impl AdminSettingsService {
|
||||
pub async fn set_registration_enabled(
|
||||
&self,
|
||||
enabled: bool,
|
||||
updated_by: &str,
|
||||
updated_by: Uuid,
|
||||
) -> Result<(), DomainError> {
|
||||
self.settings_repo
|
||||
.set(
|
||||
|
||||
@@ -17,6 +17,7 @@ use moka::future::Cache;
|
||||
use rand_core::RngCore;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration as StdDuration;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// App password token length (32 random alphanumeric chars after prefix).
|
||||
const TOKEN_LENGTH: usize = 32;
|
||||
@@ -41,7 +42,7 @@ const BASIC_AUTH_CACHE_MAX_ENTRIES: u64 = 10_000;
|
||||
/// Cached identity returned after a successful Basic Auth verification.
|
||||
#[derive(Clone)]
|
||||
struct CachedBasicAuthResult {
|
||||
user_id: String,
|
||||
user_id: Uuid,
|
||||
username: String,
|
||||
email: String,
|
||||
role: String,
|
||||
@@ -118,7 +119,7 @@ impl AppPasswordService {
|
||||
/// Returns the response DTO that includes the plain-text password (shown only once).
|
||||
pub async fn create(
|
||||
&self,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
request: CreateAppPasswordRequestDto,
|
||||
) -> Result<AppPasswordCreatedResponseDto, DomainError> {
|
||||
// Validate label
|
||||
@@ -147,7 +148,7 @@ impl AppPasswordService {
|
||||
|
||||
// Create entity
|
||||
let app_password = AppPassword::new(
|
||||
user_id.to_string(),
|
||||
user_id,
|
||||
label.clone(),
|
||||
password_hash,
|
||||
prefix.clone(),
|
||||
@@ -165,7 +166,7 @@ impl AppPasswordService {
|
||||
);
|
||||
|
||||
Ok(AppPasswordCreatedResponseDto {
|
||||
id: saved.id,
|
||||
id: saved.id.to_string(),
|
||||
label,
|
||||
password: plain_token,
|
||||
username: username.clone(),
|
||||
@@ -200,7 +201,7 @@ impl AppPasswordService {
|
||||
}
|
||||
|
||||
/// List all app passwords for a user (excludes plain-text passwords).
|
||||
pub async fn list(&self, user_id: &str) -> Result<AppPasswordListResponseDto, DomainError> {
|
||||
pub async fn list(&self, user_id: Uuid) -> Result<AppPasswordListResponseDto, DomainError> {
|
||||
let passwords = self.repo.list_by_user(user_id).await?;
|
||||
let total = passwords.len();
|
||||
|
||||
@@ -209,7 +210,7 @@ impl AppPasswordService {
|
||||
.map(|ap| {
|
||||
let is_active = ap.active && !ap.is_expired();
|
||||
AppPasswordSummaryDto {
|
||||
id: ap.id,
|
||||
id: ap.id.to_string(),
|
||||
label: ap.label,
|
||||
prefix: format!("{}...", ap.prefix),
|
||||
scopes: ap.scopes,
|
||||
@@ -234,8 +235,8 @@ impl AppPasswordService {
|
||||
/// up to `BASIC_AUTH_CACHE_TTL_SECS`).
|
||||
pub async fn revoke(
|
||||
&self,
|
||||
user_id: &str,
|
||||
id: &str,
|
||||
user_id: Uuid,
|
||||
id: Uuid,
|
||||
) -> Result<AppPasswordRevokeResponseDto, DomainError> {
|
||||
// Ownership enforced at SQL level (WHERE user_id = $2).
|
||||
// The get_by_id pre-check gives a clear error message when
|
||||
@@ -250,7 +251,7 @@ impl AppPasswordService {
|
||||
|
||||
// Invalidate all cached auth entries for this user so the
|
||||
// revocation is effective immediately.
|
||||
let uid = user_id.to_string();
|
||||
let uid = user_id;
|
||||
self.auth_cache
|
||||
.invalidate_entries_if(move |_key, val| val.user_id == uid)
|
||||
.ok();
|
||||
@@ -282,7 +283,7 @@ impl AppPasswordService {
|
||||
&self,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<(String, String, String, String), DomainError> {
|
||||
) -> Result<(Uuid, String, String, String), DomainError> {
|
||||
// ── 1. Compute cache key = blake3("username:password") ────────
|
||||
let cache_key: [u8; 32] =
|
||||
blake3::hash(format!("{}:{}", username, password).as_bytes()).into();
|
||||
@@ -344,10 +345,10 @@ impl AppPasswordService {
|
||||
.verify_password(&verify_password, &ap.password_hash)
|
||||
.await
|
||||
{
|
||||
let _ = self.repo.touch_last_used(&ap.id).await;
|
||||
let _ = self.repo.touch_last_used(ap.id).await;
|
||||
|
||||
let result = CachedBasicAuthResult {
|
||||
user_id: user.id().to_string(),
|
||||
user_id: user.id(),
|
||||
username: user.username().to_string(),
|
||||
email: user.email().to_string(),
|
||||
role: user.role().to_string(),
|
||||
@@ -372,16 +373,16 @@ impl AppPasswordService {
|
||||
/// Returns `(id, plain_password)`.
|
||||
pub async fn create_nc(
|
||||
&self,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
label: &str,
|
||||
) -> Result<(String, String), DomainError> {
|
||||
) -> Result<(Uuid, String), DomainError> {
|
||||
let password = generate_nc_app_password();
|
||||
let normalized = nc_normalize_password(&password);
|
||||
let prefix = nc_token_prefix(&normalized)?;
|
||||
let hash = self.hasher.hash_password(&normalized).await?;
|
||||
|
||||
let ap = AppPassword::new(
|
||||
user_id.to_string(),
|
||||
user_id,
|
||||
label.to_string(),
|
||||
hash,
|
||||
prefix,
|
||||
@@ -397,7 +398,7 @@ impl AppPasswordService {
|
||||
/// Scoped to the authenticated user (fixes I3 — no global prefix search).
|
||||
pub async fn revoke_by_password(
|
||||
&self,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
password: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
let normalized = nc_normalize_password(password);
|
||||
@@ -417,10 +418,10 @@ impl AppPasswordService {
|
||||
.verify_password(&normalized, &ap.password_hash)
|
||||
.await
|
||||
{
|
||||
self.repo.revoke(&ap.id, user_id).await?;
|
||||
self.repo.revoke(ap.id, user_id).await?;
|
||||
|
||||
// Invalidate cache for this user
|
||||
let uid = user_id.to_string();
|
||||
let uid = user_id;
|
||||
self.auth_cache
|
||||
.invalidate_entries_if(move |_key, val| val.user_id == uid)
|
||||
.ok();
|
||||
@@ -432,12 +433,12 @@ impl AppPasswordService {
|
||||
}
|
||||
|
||||
/// List app passwords for a user (simple summary for NC UI).
|
||||
pub async fn list_nc(&self, user_id: &str) -> Result<Vec<AppPassword>, DomainError> {
|
||||
pub async fn list_nc(&self, user_id: Uuid) -> Result<Vec<AppPassword>, DomainError> {
|
||||
self.repo.list_by_user(user_id).await
|
||||
}
|
||||
|
||||
/// Delete an app password by ID, scoped to the owning user.
|
||||
pub async fn delete_by_user(&self, id: &str, user_id: &str) -> Result<(), DomainError> {
|
||||
pub async fn delete_by_user(&self, id: Uuid, user_id: Uuid) -> Result<(), DomainError> {
|
||||
let deleted = self.repo.delete_by_user_and_id(id, user_id).await?;
|
||||
if !deleted {
|
||||
return Err(DomainError::new(
|
||||
|
||||
@@ -17,6 +17,7 @@ use crate::infrastructure::services::jwt_service::JwtTokenService;
|
||||
use crate::infrastructure::services::oidc_service::OidcService;
|
||||
use crate::infrastructure::services::password_hasher::Argon2PasswordHasher;
|
||||
use moka::sync::Cache;
|
||||
use uuid::Uuid;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
@@ -33,7 +34,7 @@ pub enum OidcCallbackResult {
|
||||
/// app password and complete the NC login flow.
|
||||
NextcloudLogin {
|
||||
nc_flow_token: String,
|
||||
user_id: String,
|
||||
user_id: Uuid,
|
||||
username: String,
|
||||
},
|
||||
}
|
||||
@@ -411,7 +412,7 @@ impl AuthApplicationService {
|
||||
|
||||
// Save session
|
||||
let session = Session::new(
|
||||
user.id().to_string(),
|
||||
user.id(),
|
||||
refresh_token.clone(),
|
||||
None, // IP (can be added from the HTTP layer)
|
||||
None, // User-Agent (can be added from the HTTP layer)
|
||||
@@ -466,7 +467,7 @@ impl AuthApplicationService {
|
||||
}
|
||||
|
||||
Ok(crate::application::dtos::user_dto::CurrentUser {
|
||||
id: user.id().to_string(),
|
||||
id: user.id(),
|
||||
username: user.username().to_string(),
|
||||
email: user.email().to_string(),
|
||||
role: user.role().to_string(),
|
||||
@@ -514,7 +515,7 @@ impl AuthApplicationService {
|
||||
|
||||
// Create new session
|
||||
let new_session = Session::new(
|
||||
user.id().to_string(),
|
||||
user.id(),
|
||||
new_refresh_token.clone(),
|
||||
None,
|
||||
None,
|
||||
@@ -532,7 +533,7 @@ impl AuthApplicationService {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn logout(&self, user_id: &str, refresh_token: &str) -> Result<(), DomainError> {
|
||||
pub async fn logout(&self, user_id: Uuid, refresh_token: &str) -> Result<(), DomainError> {
|
||||
// Get session
|
||||
let session = match self
|
||||
.session_storage
|
||||
@@ -559,7 +560,7 @@ impl AuthApplicationService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn logout_all(&self, user_id: &str) -> Result<u64, DomainError> {
|
||||
pub async fn logout_all(&self, user_id: Uuid) -> Result<u64, DomainError> {
|
||||
// Revoke all user sessions
|
||||
let revoked_count = self
|
||||
.session_storage
|
||||
@@ -571,7 +572,7 @@ impl AuthApplicationService {
|
||||
|
||||
pub async fn change_password(
|
||||
&self,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
dto: ChangePasswordDto,
|
||||
) -> Result<(), DomainError> {
|
||||
// Get user
|
||||
@@ -627,13 +628,13 @@ impl AuthApplicationService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_user(&self, user_id: &str) -> Result<UserDto, DomainError> {
|
||||
pub async fn get_user(&self, user_id: Uuid) -> Result<UserDto, DomainError> {
|
||||
let user = self.user_storage.get_user_by_id(user_id).await?;
|
||||
Ok(UserDto::from(user))
|
||||
}
|
||||
|
||||
// Alias for consistency with handler method
|
||||
pub async fn get_user_by_id(&self, user_id: &str) -> Result<UserDto, DomainError> {
|
||||
pub async fn get_user_by_id(&self, user_id: Uuid) -> Result<UserDto, DomainError> {
|
||||
self.get_user(user_id).await
|
||||
}
|
||||
|
||||
@@ -778,7 +779,7 @@ impl AuthApplicationService {
|
||||
/// Admin-only: reset a user's password.
|
||||
pub async fn admin_reset_password(
|
||||
&self,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
new_password: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
// Block password reset for OIDC-provisioned users
|
||||
@@ -812,13 +813,13 @@ impl AuthApplicationService {
|
||||
}
|
||||
|
||||
/// Get a single user by ID (for admin panel)
|
||||
pub async fn get_user_admin(&self, user_id: &str) -> Result<UserDto, DomainError> {
|
||||
pub async fn get_user_admin(&self, user_id: Uuid) -> Result<UserDto, DomainError> {
|
||||
let user = self.user_storage.get_user_by_id(user_id).await?;
|
||||
Ok(UserDto::from(user))
|
||||
}
|
||||
|
||||
/// Delete a user by ID (admin only)
|
||||
pub async fn delete_user_admin(&self, user_id: &str) -> Result<(), DomainError> {
|
||||
pub async fn delete_user_admin(&self, user_id: Uuid) -> Result<(), DomainError> {
|
||||
// Prevent deleting yourself
|
||||
let user = self.user_storage.get_user_by_id(user_id).await?;
|
||||
tracing::info!("Admin deleting user: {} ({})", user.username(), user_id);
|
||||
@@ -826,14 +827,14 @@ impl AuthApplicationService {
|
||||
}
|
||||
|
||||
/// Activate or deactivate a user (admin only)
|
||||
pub async fn set_user_active(&self, user_id: &str, active: bool) -> Result<(), DomainError> {
|
||||
pub async fn set_user_active(&self, user_id: Uuid, active: bool) -> Result<(), DomainError> {
|
||||
self.user_storage
|
||||
.set_user_active_status(user_id, active)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Change user role (admin only)
|
||||
pub async fn change_user_role(&self, user_id: &str, role: &str) -> Result<(), DomainError> {
|
||||
pub async fn change_user_role(&self, user_id: Uuid, role: &str) -> Result<(), DomainError> {
|
||||
if role != "admin" && role != "user" {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
@@ -847,7 +848,7 @@ impl AuthApplicationService {
|
||||
/// Update user's storage quota (admin only)
|
||||
pub async fn update_user_quota(
|
||||
&self,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
quota_bytes: i64,
|
||||
) -> Result<(), DomainError> {
|
||||
if quota_bytes < 0 {
|
||||
@@ -865,7 +866,7 @@ impl AuthApplicationService {
|
||||
/// Check if a user has enough quota for an upload of the given size
|
||||
pub async fn check_quota(
|
||||
&self,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
additional_bytes: i64,
|
||||
) -> Result<bool, DomainError> {
|
||||
let user = self.user_storage.get_user_by_id(user_id).await?;
|
||||
@@ -1209,7 +1210,7 @@ impl AuthApplicationService {
|
||||
);
|
||||
return Ok(OidcCallbackResult::NextcloudLogin {
|
||||
nc_flow_token: nc_token,
|
||||
user_id: user.id().to_string(),
|
||||
user_id: user.id(),
|
||||
username: user.username().to_string(),
|
||||
});
|
||||
}
|
||||
@@ -1219,7 +1220,7 @@ impl AuthApplicationService {
|
||||
let refresh_token = self.token_service.generate_refresh_token();
|
||||
|
||||
let session = Session::new(
|
||||
user.id().to_string(),
|
||||
user.id(),
|
||||
refresh_token.clone(),
|
||||
None,
|
||||
None,
|
||||
@@ -1282,7 +1283,7 @@ impl AuthApplicationService {
|
||||
}
|
||||
|
||||
/// Helper to create a personal folder for a new user
|
||||
async fn create_personal_folder(&self, username: &str, user_id: &str) {
|
||||
async fn create_personal_folder(&self, username: &str, user_id: Uuid) {
|
||||
if let Some(folder_service) = &self.folder_service {
|
||||
let folder_name = format!("My Folder - {}", username);
|
||||
match folder_service
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::application::services::folder_service::FolderService;
|
||||
use crate::application::services::trash_service::TrashService;
|
||||
use crate::common::config::AppConfig;
|
||||
use crate::common::errors::DomainError;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Specific errors for batch operations
|
||||
#[derive(Debug, Error)]
|
||||
@@ -117,7 +118,7 @@ impl BatchOperationService {
|
||||
&self,
|
||||
file_ids: Vec<String>,
|
||||
target_folder_id: Option<String>,
|
||||
caller_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<BatchResult<FileDto>, BatchOperationError> {
|
||||
info!("Starting batch copy of {} files", file_ids.len());
|
||||
let start_time = std::time::Instant::now();
|
||||
@@ -135,17 +136,15 @@ impl BatchOperationService {
|
||||
|
||||
// Arc<str> avoids N heap-clones of the same string
|
||||
let target_folder: Option<Arc<str>> = target_folder_id.map(|s| Arc::from(s.as_str()));
|
||||
let caller: Arc<str> = Arc::from(caller_id);
|
||||
|
||||
// buffer_unordered materialises only max_concurrent futures at a time
|
||||
let mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| {
|
||||
let mgmt = self.file_management.clone();
|
||||
let target_folder = target_folder.clone();
|
||||
let caller = caller.clone();
|
||||
|
||||
async move {
|
||||
let copy_result = mgmt
|
||||
.copy_file_owned(&file_id, &caller, target_folder.map(|s| s.to_string()))
|
||||
.copy_file_owned(&file_id, user_id, target_folder.map(|s| s.to_string()))
|
||||
.await;
|
||||
(file_id, copy_result)
|
||||
}
|
||||
@@ -187,7 +186,7 @@ impl BatchOperationService {
|
||||
&self,
|
||||
file_ids: Vec<String>,
|
||||
target_folder_id: Option<String>,
|
||||
caller_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<BatchResult<FileDto>, BatchOperationError> {
|
||||
info!("Starting batch move of {} files", file_ids.len());
|
||||
let start_time = std::time::Instant::now();
|
||||
@@ -204,16 +203,14 @@ impl BatchOperationService {
|
||||
};
|
||||
|
||||
let target_folder: Option<Arc<str>> = target_folder_id.map(|s| Arc::from(s.as_str()));
|
||||
let caller: Arc<str> = Arc::from(caller_id);
|
||||
|
||||
let mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| {
|
||||
let mgmt = self.file_management.clone();
|
||||
let target_folder = target_folder.clone();
|
||||
let caller = caller.clone();
|
||||
|
||||
async move {
|
||||
let move_result = mgmt
|
||||
.move_file_owned(&file_id, &caller, target_folder.map(|s| s.to_string()))
|
||||
.move_file_owned(&file_id, user_id, target_folder.map(|s| s.to_string()))
|
||||
.await;
|
||||
(file_id, move_result)
|
||||
}
|
||||
@@ -253,7 +250,7 @@ impl BatchOperationService {
|
||||
pub async fn delete_files(
|
||||
&self,
|
||||
file_ids: Vec<String>,
|
||||
caller_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<BatchResult<String>, BatchOperationError> {
|
||||
info!("Starting batch deletion of {} files", file_ids.len());
|
||||
let start_time = std::time::Instant::now();
|
||||
@@ -268,15 +265,11 @@ impl BatchOperationService {
|
||||
},
|
||||
};
|
||||
|
||||
// Define the operation to perform for each file
|
||||
let caller: Arc<str> = Arc::from(caller_id);
|
||||
|
||||
let mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| {
|
||||
let mgmt = self.file_management.clone();
|
||||
let caller = caller.clone();
|
||||
|
||||
async move {
|
||||
let delete_result = mgmt.delete_file_owned(&file_id, &caller).await;
|
||||
let delete_result = mgmt.delete_file_owned(&file_id, user_id).await;
|
||||
let id_for_result = file_id.clone();
|
||||
(file_id, delete_result.map(|_| id_for_result))
|
||||
}
|
||||
@@ -317,7 +310,7 @@ impl BatchOperationService {
|
||||
pub async fn get_multiple_files(
|
||||
&self,
|
||||
file_ids: Vec<String>,
|
||||
caller_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<BatchResult<FileDto>, BatchOperationError> {
|
||||
info!("Starting batch load of {} files", file_ids.len());
|
||||
let start_time = std::time::Instant::now();
|
||||
@@ -332,15 +325,11 @@ impl BatchOperationService {
|
||||
},
|
||||
};
|
||||
|
||||
// Define the operation to perform for each file
|
||||
let caller: Arc<str> = Arc::from(caller_id);
|
||||
|
||||
let mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| {
|
||||
let retrieval = self.file_retrieval.clone();
|
||||
let caller = caller.clone();
|
||||
|
||||
async move {
|
||||
let get_result = retrieval.get_file_owned(&file_id, &caller).await;
|
||||
let get_result = retrieval.get_file_owned(&file_id, user_id).await;
|
||||
(file_id, get_result)
|
||||
}
|
||||
}))
|
||||
@@ -381,7 +370,7 @@ impl BatchOperationService {
|
||||
&self,
|
||||
folder_ids: Vec<String>,
|
||||
_recursive: bool,
|
||||
caller_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<BatchResult<String>, BatchOperationError> {
|
||||
info!("Starting batch deletion of {} folders", folder_ids.len());
|
||||
let start_time = std::time::Instant::now();
|
||||
@@ -396,16 +385,11 @@ impl BatchOperationService {
|
||||
},
|
||||
};
|
||||
|
||||
// Define the operation to perform for each folder
|
||||
// Arc<str> avoids N heap-clones of the caller string
|
||||
let caller: Arc<str> = Arc::from(caller_id);
|
||||
|
||||
let mut operation_stream = stream::iter(folder_ids.into_iter().map(|folder_id| {
|
||||
let folder_service = self.folder_service.clone();
|
||||
let caller = caller.clone();
|
||||
|
||||
async move {
|
||||
let delete_result = folder_service.delete_folder(&folder_id, &caller).await;
|
||||
let delete_result = folder_service.delete_folder(&folder_id, user_id).await;
|
||||
let id_for_result = folder_id.clone();
|
||||
(folder_id, delete_result.map(|_| id_for_result))
|
||||
}
|
||||
@@ -445,7 +429,7 @@ impl BatchOperationService {
|
||||
pub async fn trash_files(
|
||||
&self,
|
||||
file_ids: Vec<String>,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<BatchResult<String>, BatchOperationError> {
|
||||
let trash_service = self
|
||||
.trash_service
|
||||
@@ -464,14 +448,14 @@ impl BatchOperationService {
|
||||
},
|
||||
};
|
||||
|
||||
let uid: Arc<str> = Arc::from(user_id);
|
||||
let uid = user_id;
|
||||
|
||||
let mut operation_stream = stream::iter(file_ids.into_iter().map(|file_id| {
|
||||
let trash = trash_service.clone();
|
||||
let uid = uid.clone();
|
||||
let uid = uid;
|
||||
|
||||
async move {
|
||||
let trash_result = trash.move_to_trash(&file_id, "file", &uid).await;
|
||||
let trash_result = trash.move_to_trash(&file_id, "file", uid).await;
|
||||
let id_for_result = file_id.clone();
|
||||
(file_id, trash_result.map(|_| id_for_result))
|
||||
}
|
||||
@@ -510,7 +494,7 @@ impl BatchOperationService {
|
||||
pub async fn trash_folders(
|
||||
&self,
|
||||
folder_ids: Vec<String>,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<BatchResult<String>, BatchOperationError> {
|
||||
let trash_service = self
|
||||
.trash_service
|
||||
@@ -529,14 +513,14 @@ impl BatchOperationService {
|
||||
},
|
||||
};
|
||||
|
||||
let uid: Arc<str> = Arc::from(user_id);
|
||||
let uid = user_id;
|
||||
|
||||
let mut operation_stream = stream::iter(folder_ids.into_iter().map(|folder_id| {
|
||||
let trash = trash_service.clone();
|
||||
let uid = uid.clone();
|
||||
let uid = uid;
|
||||
|
||||
async move {
|
||||
let trash_result = trash.move_to_trash(&folder_id, "folder", &uid).await;
|
||||
let trash_result = trash.move_to_trash(&folder_id, "folder", uid).await;
|
||||
let id_for_result = folder_id.clone();
|
||||
(folder_id, trash_result.map(|_| id_for_result))
|
||||
}
|
||||
@@ -576,7 +560,7 @@ impl BatchOperationService {
|
||||
&self,
|
||||
folder_ids: Vec<String>,
|
||||
target_folder_id: Option<String>,
|
||||
caller_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<BatchResult<FolderDto>, BatchOperationError> {
|
||||
info!("Starting batch move of {} folders", folder_ids.len());
|
||||
let start_time = std::time::Instant::now();
|
||||
@@ -591,18 +575,16 @@ impl BatchOperationService {
|
||||
};
|
||||
|
||||
let target: Option<Arc<str>> = target_folder_id.map(|s| Arc::from(s.as_str()));
|
||||
let caller: Arc<str> = Arc::from(caller_id);
|
||||
|
||||
let mut operation_stream = stream::iter(folder_ids.into_iter().map(|folder_id| {
|
||||
let folder_service = self.folder_service.clone();
|
||||
let target = target.clone();
|
||||
let caller = caller.clone();
|
||||
|
||||
async move {
|
||||
let dto = MoveFolderDto {
|
||||
parent_id: target.map(|s| s.to_string()),
|
||||
};
|
||||
let move_result = folder_service.move_folder(&folder_id, dto, &caller).await;
|
||||
let move_result = folder_service.move_folder(&folder_id, dto, user_id).await;
|
||||
(folder_id, move_result)
|
||||
}
|
||||
}))
|
||||
@@ -645,7 +627,7 @@ impl BatchOperationService {
|
||||
&self,
|
||||
file_ids: Vec<String>,
|
||||
folder_ids: Vec<String>,
|
||||
caller_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<NamedTempFile, BatchOperationError> {
|
||||
info!(
|
||||
"Starting batch download: {} files, {} folders",
|
||||
@@ -665,10 +647,10 @@ impl BatchOperationService {
|
||||
|
||||
// ── Add individual files at the root of the ZIP ──────────────────
|
||||
for file_id in &file_ids {
|
||||
match self.file_retrieval.get_file_owned(file_id, caller_id).await {
|
||||
match self.file_retrieval.get_file_owned(file_id, user_id).await {
|
||||
Ok(file_dto) => {
|
||||
if let Err(e) = self
|
||||
.add_file_entry_streamed(&mut zip, file_id, &file_dto.name, caller_id)
|
||||
.add_file_entry_streamed(&mut zip, file_id, &file_dto.name, user_id)
|
||||
.await
|
||||
{
|
||||
info!("Could not add file {} to ZIP: {}", file_dto.name, e);
|
||||
@@ -684,12 +666,12 @@ impl BatchOperationService {
|
||||
for folder_id in &folder_ids {
|
||||
match self
|
||||
.folder_service
|
||||
.get_folder_owned(folder_id, caller_id)
|
||||
.get_folder_owned(folder_id, user_id)
|
||||
.await
|
||||
{
|
||||
Ok(root_folder) => {
|
||||
if let Err(e) = self
|
||||
.add_folder_subtree_to_zip(&mut zip, folder_id, &root_folder, caller_id)
|
||||
.add_folder_subtree_to_zip(&mut zip, folder_id, &root_folder, user_id)
|
||||
.await
|
||||
{
|
||||
info!("Could not add folder {} to ZIP: {}", root_folder.name, e);
|
||||
@@ -728,7 +710,7 @@ impl BatchOperationService {
|
||||
zip: &mut ZipFileWriter<tokio_util::compat::Compat<BufWriter<tokio::fs::File>>>,
|
||||
file_id: &str,
|
||||
entry_name: &str,
|
||||
caller_id: &str,
|
||||
caller_id: Uuid,
|
||||
) -> Result<(), BatchOperationError> {
|
||||
let entry = ZipEntryBuilder::new(entry_name.to_string().into(), Compression::Deflate);
|
||||
let mut writer = zip
|
||||
@@ -769,7 +751,7 @@ impl BatchOperationService {
|
||||
zip: &mut ZipFileWriter<tokio_util::compat::Compat<BufWriter<tokio::fs::File>>>,
|
||||
folder_id: &str,
|
||||
root_folder: &FolderDto,
|
||||
caller_id: &str,
|
||||
caller_id: Uuid,
|
||||
) -> Result<(), BatchOperationError> {
|
||||
// Bulk-fetch folder tree (small — one entry per folder)
|
||||
let all_folders = self
|
||||
@@ -908,7 +890,7 @@ impl BatchOperationService {
|
||||
pub async fn create_folders(
|
||||
&self,
|
||||
folders: Vec<(String, Option<String>)>, // (name, parent_id)
|
||||
caller_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<BatchResult<FolderDto>, BatchOperationError> {
|
||||
info!("Starting batch creation of {} folders", folders.len());
|
||||
let start_time = std::time::Instant::now();
|
||||
@@ -923,17 +905,13 @@ impl BatchOperationService {
|
||||
},
|
||||
};
|
||||
|
||||
// Define the operation for each folder
|
||||
let caller: Arc<str> = Arc::from(caller_id);
|
||||
|
||||
let mut operation_stream = stream::iter(folders.into_iter().map(|(name, parent_id)| {
|
||||
let folder_service = self.folder_service.clone();
|
||||
let caller = caller.clone();
|
||||
|
||||
async move {
|
||||
// If a parent is specified, verify the caller owns it
|
||||
if let Some(ref pid) = parent_id
|
||||
&& let Err(e) = folder_service.get_folder_owned(pid, &caller).await
|
||||
&& let Err(e) = folder_service.get_folder_owned(pid, user_id).await
|
||||
{
|
||||
let id = format!("{}:{}", name, pid);
|
||||
return (id, Err(e));
|
||||
@@ -983,7 +961,7 @@ impl BatchOperationService {
|
||||
pub async fn get_multiple_folders(
|
||||
&self,
|
||||
folder_ids: Vec<String>,
|
||||
caller_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<BatchResult<FolderDto>, BatchOperationError> {
|
||||
info!("Starting batch load of {} folders", folder_ids.len());
|
||||
let start_time = std::time::Instant::now();
|
||||
@@ -998,15 +976,11 @@ impl BatchOperationService {
|
||||
},
|
||||
};
|
||||
|
||||
// Define the operation for each folder
|
||||
let caller: Arc<str> = Arc::from(caller_id);
|
||||
|
||||
let mut operation_stream = stream::iter(folder_ids.into_iter().map(|folder_id| {
|
||||
let folder_service = self.folder_service.clone();
|
||||
let caller = caller.clone();
|
||||
|
||||
async move {
|
||||
let get_result = folder_service.get_folder_owned(&folder_id, &caller).await;
|
||||
let get_result = folder_service.get_folder_owned(&folder_id, user_id).await;
|
||||
(folder_id, get_result)
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::application::dtos::calendar_dto::{
|
||||
CalendarDto, CalendarEventDto, CreateCalendarDto, CreateEventDto, CreateEventICalDto,
|
||||
@@ -23,7 +24,7 @@ impl CalendarUseCase for CalendarService {
|
||||
async fn create_calendar(
|
||||
&self,
|
||||
calendar: CreateCalendarDto,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<CalendarDto, DomainError> {
|
||||
self.calendar_storage
|
||||
.create_calendar(calendar, user_id)
|
||||
@@ -34,7 +35,7 @@ impl CalendarUseCase for CalendarService {
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
update: UpdateCalendarDto,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<CalendarDto, DomainError> {
|
||||
let has_access = self
|
||||
.calendar_storage
|
||||
@@ -52,7 +53,7 @@ impl CalendarUseCase for CalendarService {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn delete_calendar(&self, calendar_id: &str, user_id: &str) -> Result<(), DomainError> {
|
||||
async fn delete_calendar(&self, calendar_id: &str, user_id: Uuid) -> Result<(), DomainError> {
|
||||
let has_access = self
|
||||
.calendar_storage
|
||||
.check_calendar_access(calendar_id, user_id)
|
||||
@@ -70,7 +71,7 @@ impl CalendarUseCase for CalendarService {
|
||||
async fn get_calendar(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<CalendarDto, DomainError> {
|
||||
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
|
||||
let has_access = self
|
||||
@@ -87,11 +88,11 @@ impl CalendarUseCase for CalendarService {
|
||||
Ok(calendar)
|
||||
}
|
||||
|
||||
async fn list_my_calendars(&self, user_id: &str) -> Result<Vec<CalendarDto>, DomainError> {
|
||||
async fn list_my_calendars(&self, user_id: Uuid) -> Result<Vec<CalendarDto>, DomainError> {
|
||||
self.calendar_storage.list_calendars_by_owner(user_id).await
|
||||
}
|
||||
|
||||
async fn list_shared_calendars(&self, user_id: &str) -> Result<Vec<CalendarDto>, DomainError> {
|
||||
async fn list_shared_calendars(&self, user_id: Uuid) -> Result<Vec<CalendarDto>, DomainError> {
|
||||
self.calendar_storage
|
||||
.list_calendars_shared_with_user(user_id)
|
||||
.await
|
||||
@@ -112,12 +113,12 @@ impl CalendarUseCase for CalendarService {
|
||||
async fn share_calendar(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
target_user_id: &str,
|
||||
target_user_id: Uuid,
|
||||
access_level: &str,
|
||||
caller_user_id: &str,
|
||||
caller_user_id: Uuid,
|
||||
) -> Result<(), DomainError> {
|
||||
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
|
||||
if calendar.owner_id != caller_user_id {
|
||||
if calendar.owner_id != caller_user_id.to_string() {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
@@ -145,11 +146,11 @@ impl CalendarUseCase for CalendarService {
|
||||
async fn remove_calendar_sharing(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
target_user_id: &str,
|
||||
caller_user_id: &str,
|
||||
target_user_id: Uuid,
|
||||
caller_user_id: Uuid,
|
||||
) -> Result<(), DomainError> {
|
||||
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
|
||||
if calendar.owner_id != caller_user_id {
|
||||
if calendar.owner_id != caller_user_id.to_string() {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
@@ -164,10 +165,10 @@ impl CalendarUseCase for CalendarService {
|
||||
async fn get_calendar_shares(
|
||||
&self,
|
||||
calendar_id: &str,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<(String, String)>, DomainError> {
|
||||
let calendar = self.calendar_storage.get_calendar(calendar_id).await?;
|
||||
if calendar.owner_id != user_id {
|
||||
if calendar.owner_id != user_id.to_string() {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Calendar",
|
||||
@@ -180,7 +181,7 @@ impl CalendarUseCase for CalendarService {
|
||||
async fn create_event(
|
||||
&self,
|
||||
event: CreateEventDto,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<CalendarEventDto, DomainError> {
|
||||
let has_access = self
|
||||
.calendar_storage
|
||||
@@ -199,7 +200,7 @@ impl CalendarUseCase for CalendarService {
|
||||
async fn create_event_from_ical(
|
||||
&self,
|
||||
event: CreateEventICalDto,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<CalendarEventDto, DomainError> {
|
||||
let has_access = self
|
||||
.calendar_storage
|
||||
@@ -219,7 +220,7 @@ impl CalendarUseCase for CalendarService {
|
||||
&self,
|
||||
event_id: &str,
|
||||
update: UpdateEventDto,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<CalendarEventDto, DomainError> {
|
||||
let event = self.calendar_storage.get_event(event_id).await?;
|
||||
let has_access = self
|
||||
@@ -236,7 +237,7 @@ impl CalendarUseCase for CalendarService {
|
||||
self.calendar_storage.update_event(event_id, update).await
|
||||
}
|
||||
|
||||
async fn delete_event(&self, event_id: &str, user_id: &str) -> Result<(), DomainError> {
|
||||
async fn delete_event(&self, event_id: &str, user_id: Uuid) -> Result<(), DomainError> {
|
||||
let event = self.calendar_storage.get_event(event_id).await?;
|
||||
let has_access = self
|
||||
.calendar_storage
|
||||
@@ -255,7 +256,7 @@ impl CalendarUseCase for CalendarService {
|
||||
async fn get_event(
|
||||
&self,
|
||||
event_id: &str,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<CalendarEventDto, DomainError> {
|
||||
let event = self.calendar_storage.get_event(event_id).await?;
|
||||
let has_access = self
|
||||
@@ -281,7 +282,7 @@ impl CalendarUseCase for CalendarService {
|
||||
calendar_id: &str,
|
||||
limit: Option<i64>,
|
||||
offset: Option<i64>,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<CalendarEventDto>, DomainError> {
|
||||
let has_access = self
|
||||
.calendar_storage
|
||||
@@ -313,7 +314,7 @@ impl CalendarUseCase for CalendarService {
|
||||
calendar_id: &str,
|
||||
start: DateTime<Utc>,
|
||||
end: DateTime<Utc>,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<CalendarEventDto>, DomainError> {
|
||||
let has_access = self
|
||||
.calendar_storage
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use chrono::Utc;
|
||||
use sqlx::types::Uuid;
|
||||
use uuid::Uuid;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::application::dtos::address_book_dto::{
|
||||
@@ -43,7 +43,7 @@ impl ContactService {
|
||||
async fn check_address_book_access(
|
||||
&self,
|
||||
address_book_id: &Uuid,
|
||||
user_id: &str,
|
||||
user_id: &Uuid,
|
||||
) -> Result<AddressBook, DomainError> {
|
||||
let address_book = self
|
||||
.address_book_repository
|
||||
@@ -52,7 +52,7 @@ impl ContactService {
|
||||
.ok_or_else(|| DomainError::not_found("Address book", "not found"))?;
|
||||
|
||||
// Check if user is owner
|
||||
if address_book.owner_id() == user_id {
|
||||
if address_book.owner_id() == user_id.to_string() {
|
||||
return Ok(address_book);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ impl ContactService {
|
||||
.address_book_repository
|
||||
.get_address_book_shares(address_book_id)
|
||||
.await?;
|
||||
if shares.iter().any(|(id, _)| id == user_id) {
|
||||
if shares.iter().any(|(id, _)| id == &user_id.to_string()) {
|
||||
return Ok(address_book);
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ impl ContactService {
|
||||
async fn check_address_book_write_access(
|
||||
&self,
|
||||
address_book_id: &Uuid,
|
||||
user_id: &str,
|
||||
user_id: &Uuid,
|
||||
) -> Result<AddressBook, DomainError> {
|
||||
let address_book = self
|
||||
.address_book_repository
|
||||
@@ -87,7 +87,7 @@ impl ContactService {
|
||||
.ok_or_else(|| DomainError::not_found("Address book", "not found"))?;
|
||||
|
||||
// Check if user is owner
|
||||
if address_book.owner_id() == user_id {
|
||||
if address_book.owner_id() == user_id.to_string() {
|
||||
return Ok(address_book);
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ impl ContactService {
|
||||
.await?;
|
||||
if shares
|
||||
.iter()
|
||||
.any(|(id, can_write)| id == user_id && *can_write)
|
||||
.any(|(id, can_write)| id == &user_id.to_string() && *can_write)
|
||||
{
|
||||
return Ok(address_book);
|
||||
}
|
||||
@@ -296,7 +296,8 @@ impl AddressBookUseCase for ContactService {
|
||||
|
||||
// Check if user has write access to the address book
|
||||
let address_book = self
|
||||
.check_address_book_write_access(&id, &update.user_id)
|
||||
.check_address_book_write_access(&id, &Uuid::parse_str(&update.user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?)
|
||||
.await?;
|
||||
|
||||
// Apply updates
|
||||
@@ -327,7 +328,7 @@ impl AddressBookUseCase for ContactService {
|
||||
async fn delete_address_book(
|
||||
&self,
|
||||
address_book_id: &str,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<(), DomainError> {
|
||||
let id = Uuid::parse_str(address_book_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||
@@ -339,7 +340,7 @@ impl AddressBookUseCase for ContactService {
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::not_found("Address book", "not found"))?;
|
||||
|
||||
if address_book.owner_id() != user_id {
|
||||
if address_book.owner_id() != user_id.to_string() {
|
||||
return Err(DomainError::unauthorized(
|
||||
"Only the owner can delete an address book",
|
||||
));
|
||||
@@ -354,18 +355,18 @@ impl AddressBookUseCase for ContactService {
|
||||
async fn get_address_book(
|
||||
&self,
|
||||
address_book_id: &str,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<AddressBookDto, DomainError> {
|
||||
let id = Uuid::parse_str(address_book_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||
|
||||
let address_book = self.check_address_book_access(&id, user_id).await?;
|
||||
let address_book = self.check_address_book_access(&id, &user_id).await?;
|
||||
Ok(AddressBookDto::from(address_book))
|
||||
}
|
||||
|
||||
async fn list_user_address_books(
|
||||
&self,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<AddressBookDto>, DomainError> {
|
||||
// Get address books owned by the user
|
||||
let owned_address_books = self
|
||||
@@ -397,7 +398,7 @@ impl AddressBookUseCase for ContactService {
|
||||
}
|
||||
|
||||
for address_book in public_address_books {
|
||||
if address_book.owner_id() != user_id
|
||||
if address_book.owner_id() != user_id.to_string()
|
||||
&& !address_book_map.contains_key(address_book.id())
|
||||
{
|
||||
address_book_map.insert(*address_book.id(), address_book);
|
||||
@@ -428,7 +429,7 @@ impl AddressBookUseCase for ContactService {
|
||||
async fn share_address_book(
|
||||
&self,
|
||||
dto: ShareAddressBookDto,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<(), DomainError> {
|
||||
let id = Uuid::parse_str(&dto.address_book_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||
@@ -440,21 +441,23 @@ impl AddressBookUseCase for ContactService {
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::not_found("Address book", "not found"))?;
|
||||
|
||||
if address_book.owner_id() != user_id {
|
||||
if address_book.owner_id() != user_id.to_string() {
|
||||
return Err(DomainError::unauthorized(
|
||||
"Only the owner can share an address book",
|
||||
));
|
||||
}
|
||||
|
||||
// Don't allow sharing with yourself
|
||||
if dto.user_id == user_id {
|
||||
if dto.user_id == user_id.to_string() {
|
||||
return Err(DomainError::validation_error(
|
||||
"Cannot share an address book with yourself",
|
||||
));
|
||||
}
|
||||
|
||||
let target_user_id = Uuid::parse_str(&dto.user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid target user ID format"))?;
|
||||
self.address_book_repository
|
||||
.share_address_book(&id, &dto.user_id, dto.can_write)
|
||||
.share_address_book(&id, target_user_id, dto.can_write)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -462,7 +465,7 @@ impl AddressBookUseCase for ContactService {
|
||||
async fn unshare_address_book(
|
||||
&self,
|
||||
dto: UnshareAddressBookDto,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<(), DomainError> {
|
||||
let id = Uuid::parse_str(&dto.address_book_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||
@@ -474,14 +477,16 @@ impl AddressBookUseCase for ContactService {
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::not_found("Address book", "not found"))?;
|
||||
|
||||
if address_book.owner_id() != user_id {
|
||||
if address_book.owner_id() != user_id.to_string() {
|
||||
return Err(DomainError::unauthorized(
|
||||
"Only the owner can unshare an address book",
|
||||
));
|
||||
}
|
||||
|
||||
let target_user_id = Uuid::parse_str(&dto.user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid target user ID format"))?;
|
||||
self.address_book_repository
|
||||
.unshare_address_book(&id, &dto.user_id)
|
||||
.unshare_address_book(&id, target_user_id)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -489,7 +494,7 @@ impl AddressBookUseCase for ContactService {
|
||||
async fn get_address_book_shares(
|
||||
&self,
|
||||
address_book_id: &str,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<(String, bool)>, DomainError> {
|
||||
let id = Uuid::parse_str(address_book_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||
@@ -501,7 +506,7 @@ impl AddressBookUseCase for ContactService {
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::not_found("Address book", "not found"))?;
|
||||
|
||||
if address_book.owner_id() != user_id {
|
||||
if address_book.owner_id() != user_id.to_string() {
|
||||
return Err(DomainError::unauthorized(
|
||||
"Only the owner can view address book shares",
|
||||
));
|
||||
@@ -521,7 +526,8 @@ impl ContactUseCase for ContactService {
|
||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||
|
||||
// Check if user has write access to the address book
|
||||
self.check_address_book_write_access(&address_book_id, &dto.user_id)
|
||||
self.check_address_book_write_access(&address_book_id, &Uuid::parse_str(&dto.user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?)
|
||||
.await?;
|
||||
|
||||
// Convert DTOs to domain entities
|
||||
@@ -598,7 +604,8 @@ impl ContactUseCase for ContactService {
|
||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||
|
||||
// Check if user has write access to the address book
|
||||
self.check_address_book_write_access(&address_book_id, &dto.user_id)
|
||||
self.check_address_book_write_access(&address_book_id, &Uuid::parse_str(&dto.user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?)
|
||||
.await?;
|
||||
|
||||
// Parse vCard data
|
||||
@@ -633,7 +640,9 @@ impl ContactUseCase for ContactService {
|
||||
.ok_or_else(|| DomainError::not_found("Contact", "not found"))?;
|
||||
|
||||
// Check if user has write access to the address book
|
||||
self.check_address_book_write_access(contact.address_book_id(), &update.user_id)
|
||||
let update_user_id = Uuid::parse_str(&update.user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?;
|
||||
self.check_address_book_write_access(contact.address_book_id(), &update_user_id)
|
||||
.await?;
|
||||
|
||||
// Destructure contact into owned parts for updates
|
||||
@@ -720,7 +729,7 @@ impl ContactUseCase for ContactService {
|
||||
Ok(ContactDto::from(result))
|
||||
}
|
||||
|
||||
async fn delete_contact(&self, contact_id: &str, user_id: &str) -> Result<(), DomainError> {
|
||||
async fn delete_contact(&self, contact_id: &str, user_id: Uuid) -> Result<(), DomainError> {
|
||||
let id = Uuid::parse_str(contact_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid contact ID format"))?;
|
||||
|
||||
@@ -732,7 +741,7 @@ impl ContactUseCase for ContactService {
|
||||
.ok_or_else(|| DomainError::not_found("Contact", "not found"))?;
|
||||
|
||||
// Check if user has write access to the address book
|
||||
self.check_address_book_write_access(contact.address_book_id(), user_id)
|
||||
self.check_address_book_write_access(contact.address_book_id(), &user_id)
|
||||
.await?;
|
||||
|
||||
// Delete the contact
|
||||
@@ -743,7 +752,7 @@ impl ContactUseCase for ContactService {
|
||||
async fn get_contact(
|
||||
&self,
|
||||
contact_id: &str,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<ContactDto, DomainError> {
|
||||
let id = Uuid::parse_str(contact_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid contact ID format"))?;
|
||||
@@ -756,7 +765,7 @@ impl ContactUseCase for ContactService {
|
||||
.ok_or_else(|| DomainError::not_found("Contact", "not found"))?;
|
||||
|
||||
// Check if user has access to the address book
|
||||
self.check_address_book_access(contact.address_book_id(), user_id)
|
||||
self.check_address_book_access(contact.address_book_id(), &user_id)
|
||||
.await?;
|
||||
|
||||
Ok(ContactDto::from(contact))
|
||||
@@ -765,13 +774,13 @@ impl ContactUseCase for ContactService {
|
||||
async fn list_contacts(
|
||||
&self,
|
||||
address_book_id: &str,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<ContactDto>, DomainError> {
|
||||
let id = Uuid::parse_str(address_book_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||
|
||||
// Check if user has access to the address book
|
||||
self.check_address_book_access(&id, user_id).await?;
|
||||
self.check_address_book_access(&id, &user_id).await?;
|
||||
|
||||
// Get contacts
|
||||
let contacts = self
|
||||
@@ -787,13 +796,13 @@ impl ContactUseCase for ContactService {
|
||||
&self,
|
||||
address_book_id: &str,
|
||||
query: &str,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<ContactDto>, DomainError> {
|
||||
let id = Uuid::parse_str(address_book_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||
|
||||
// Check if user has access to the address book
|
||||
self.check_address_book_access(&id, user_id).await?;
|
||||
self.check_address_book_access(&id, &user_id).await?;
|
||||
|
||||
// Search contacts
|
||||
let contacts = self.contact_repository.search_contacts(&id, query).await?;
|
||||
@@ -810,7 +819,8 @@ impl ContactUseCase for ContactService {
|
||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||
|
||||
// Check if user has write access to the address book
|
||||
self.check_address_book_write_access(&address_book_id, &dto.user_id)
|
||||
self.check_address_book_write_access(&address_book_id, &Uuid::parse_str(&dto.user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?)
|
||||
.await?;
|
||||
|
||||
let group = ContactGroup::new(address_book_id, dto.name);
|
||||
@@ -835,7 +845,8 @@ impl ContactUseCase for ContactService {
|
||||
.ok_or_else(|| DomainError::not_found("Contact group", "not found"))?;
|
||||
|
||||
// Check if user has write access to the address book
|
||||
self.check_address_book_write_access(group.address_book_id(), &update.user_id)
|
||||
self.check_address_book_write_access(group.address_book_id(), &Uuid::parse_str(&update.user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user ID format"))?)
|
||||
.await?;
|
||||
|
||||
// Update the group
|
||||
@@ -854,7 +865,7 @@ impl ContactUseCase for ContactService {
|
||||
Ok(ContactGroupDto::from(result))
|
||||
}
|
||||
|
||||
async fn delete_group(&self, group_id: &str, user_id: &str) -> Result<(), DomainError> {
|
||||
async fn delete_group(&self, group_id: &str, user_id: Uuid) -> Result<(), DomainError> {
|
||||
let id = Uuid::parse_str(group_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid group ID format"))?;
|
||||
|
||||
@@ -866,7 +877,7 @@ impl ContactUseCase for ContactService {
|
||||
.ok_or_else(|| DomainError::not_found("Contact group", "not found"))?;
|
||||
|
||||
// Check if user has write access to the address book
|
||||
self.check_address_book_write_access(group.address_book_id(), user_id)
|
||||
self.check_address_book_write_access(group.address_book_id(), &user_id)
|
||||
.await?;
|
||||
|
||||
// Delete the group
|
||||
@@ -877,7 +888,7 @@ impl ContactUseCase for ContactService {
|
||||
async fn get_group(
|
||||
&self,
|
||||
group_id: &str,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<ContactGroupDto, DomainError> {
|
||||
let id = Uuid::parse_str(group_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid group ID format"))?;
|
||||
@@ -890,7 +901,7 @@ impl ContactUseCase for ContactService {
|
||||
.ok_or_else(|| DomainError::not_found("Contact group", "not found"))?;
|
||||
|
||||
// Check if user has access to the address book
|
||||
self.check_address_book_access(group.address_book_id(), user_id)
|
||||
self.check_address_book_access(group.address_book_id(), &user_id)
|
||||
.await?;
|
||||
|
||||
// Get the number of contacts in the group
|
||||
@@ -908,13 +919,13 @@ impl ContactUseCase for ContactService {
|
||||
async fn list_groups(
|
||||
&self,
|
||||
address_book_id: &str,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<ContactGroupDto>, DomainError> {
|
||||
let id = Uuid::parse_str(address_book_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||
|
||||
// Check if user has access to the address book
|
||||
self.check_address_book_access(&id, user_id).await?;
|
||||
self.check_address_book_access(&id, &user_id).await?;
|
||||
|
||||
// Get groups
|
||||
let groups = self
|
||||
@@ -929,7 +940,7 @@ impl ContactUseCase for ContactService {
|
||||
async fn add_contact_to_group(
|
||||
&self,
|
||||
dto: GroupMembershipDto,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<(), DomainError> {
|
||||
let group_id = Uuid::parse_str(&dto.group_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid group ID format"))?;
|
||||
@@ -945,7 +956,7 @@ impl ContactUseCase for ContactService {
|
||||
.ok_or_else(|| DomainError::not_found("Contact group", "not found"))?;
|
||||
|
||||
// Check if user has write access to the address book
|
||||
self.check_address_book_write_access(group.address_book_id(), user_id)
|
||||
self.check_address_book_write_access(group.address_book_id(), &user_id)
|
||||
.await?;
|
||||
|
||||
// Add contact to group
|
||||
@@ -958,7 +969,7 @@ impl ContactUseCase for ContactService {
|
||||
async fn remove_contact_from_group(
|
||||
&self,
|
||||
dto: GroupMembershipDto,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<(), DomainError> {
|
||||
let group_id = Uuid::parse_str(&dto.group_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid group ID format"))?;
|
||||
@@ -974,7 +985,7 @@ impl ContactUseCase for ContactService {
|
||||
.ok_or_else(|| DomainError::not_found("Contact group", "not found"))?;
|
||||
|
||||
// Check if user has write access to the address book
|
||||
self.check_address_book_write_access(group.address_book_id(), user_id)
|
||||
self.check_address_book_write_access(group.address_book_id(), &user_id)
|
||||
.await?;
|
||||
|
||||
// Remove contact from group
|
||||
@@ -987,7 +998,7 @@ impl ContactUseCase for ContactService {
|
||||
async fn list_contacts_in_group(
|
||||
&self,
|
||||
group_id: &str,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<ContactDto>, DomainError> {
|
||||
let id = Uuid::parse_str(group_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid group ID format"))?;
|
||||
@@ -1000,7 +1011,7 @@ impl ContactUseCase for ContactService {
|
||||
.ok_or_else(|| DomainError::not_found("Contact group", "not found"))?;
|
||||
|
||||
// Check if user has access to the address book
|
||||
self.check_address_book_access(group.address_book_id(), user_id)
|
||||
self.check_address_book_access(group.address_book_id(), &user_id)
|
||||
.await?;
|
||||
|
||||
// Get contacts in group
|
||||
@@ -1016,7 +1027,7 @@ impl ContactUseCase for ContactService {
|
||||
async fn list_groups_for_contact(
|
||||
&self,
|
||||
contact_id: &str,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<ContactGroupDto>, DomainError> {
|
||||
let id = Uuid::parse_str(contact_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid contact ID format"))?;
|
||||
@@ -1029,7 +1040,7 @@ impl ContactUseCase for ContactService {
|
||||
.ok_or_else(|| DomainError::not_found("Contact", "not found"))?;
|
||||
|
||||
// Check if user has access to the address book
|
||||
self.check_address_book_access(contact.address_book_id(), user_id)
|
||||
self.check_address_book_access(contact.address_book_id(), &user_id)
|
||||
.await?;
|
||||
|
||||
// Get groups for contact
|
||||
@@ -1045,7 +1056,7 @@ impl ContactUseCase for ContactService {
|
||||
async fn get_contact_vcard(
|
||||
&self,
|
||||
contact_id: &str,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<String, DomainError> {
|
||||
let id = Uuid::parse_str(contact_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid contact ID format"))?;
|
||||
@@ -1058,7 +1069,7 @@ impl ContactUseCase for ContactService {
|
||||
.ok_or_else(|| DomainError::not_found("Contact", "not found"))?;
|
||||
|
||||
// Check if user has access to the address book
|
||||
self.check_address_book_access(contact.address_book_id(), user_id)
|
||||
self.check_address_book_access(contact.address_book_id(), &user_id)
|
||||
.await?;
|
||||
|
||||
// Return the vCard data
|
||||
@@ -1068,13 +1079,13 @@ impl ContactUseCase for ContactService {
|
||||
async fn get_contacts_as_vcards(
|
||||
&self,
|
||||
address_book_id: &str,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<(String, String)>, DomainError> {
|
||||
let id = Uuid::parse_str(address_book_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid address book ID format"))?;
|
||||
|
||||
// Check if user has access to the address book
|
||||
self.check_address_book_access(&id, user_id).await?;
|
||||
self.check_address_book_access(&id, &user_id).await?;
|
||||
|
||||
// Get all contacts in the address book
|
||||
let contacts = self
|
||||
@@ -1130,6 +1141,8 @@ impl StorageUseCase for ContactService {
|
||||
let user_id = params["user_id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
||||
let user_id = Uuid::parse_str(user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
|
||||
|
||||
self.delete_address_book(address_book_id, user_id).await?;
|
||||
Ok(serde_json::Value::Null)
|
||||
@@ -1142,6 +1155,8 @@ impl StorageUseCase for ContactService {
|
||||
let user_id = params["user_id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
||||
let user_id = Uuid::parse_str(user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
|
||||
|
||||
let result = self.get_address_book(address_book_id, user_id).await?;
|
||||
Ok(serde_json::to_value(result).unwrap())
|
||||
@@ -1150,6 +1165,8 @@ impl StorageUseCase for ContactService {
|
||||
let user_id = params["user_id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
||||
let user_id = Uuid::parse_str(user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
|
||||
|
||||
let result = self.list_user_address_books(user_id).await?;
|
||||
Ok(serde_json::to_value(result).unwrap())
|
||||
@@ -1167,6 +1184,8 @@ impl StorageUseCase for ContactService {
|
||||
let user_id = params["user_id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
||||
let user_id = Uuid::parse_str(user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
|
||||
|
||||
self.share_address_book(dto, user_id).await?;
|
||||
Ok(serde_json::Value::Null)
|
||||
@@ -1180,6 +1199,8 @@ impl StorageUseCase for ContactService {
|
||||
let user_id = params["user_id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
||||
let user_id = Uuid::parse_str(user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
|
||||
|
||||
self.unshare_address_book(dto, user_id).await?;
|
||||
Ok(serde_json::Value::Null)
|
||||
@@ -1192,6 +1213,8 @@ impl StorageUseCase for ContactService {
|
||||
let user_id = params["user_id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
||||
let user_id = Uuid::parse_str(user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
|
||||
|
||||
let result = self
|
||||
.get_address_book_shares(address_book_id, user_id)
|
||||
@@ -1239,6 +1262,8 @@ impl StorageUseCase for ContactService {
|
||||
let user_id = params["user_id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
||||
let user_id = Uuid::parse_str(user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
|
||||
|
||||
self.delete_contact(contact_id, user_id).await?;
|
||||
Ok(serde_json::Value::Null)
|
||||
@@ -1251,6 +1276,8 @@ impl StorageUseCase for ContactService {
|
||||
let user_id = params["user_id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
||||
let user_id = Uuid::parse_str(user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
|
||||
|
||||
let result = self.get_contact(contact_id, user_id).await?;
|
||||
Ok(serde_json::to_value(result).unwrap())
|
||||
@@ -1263,6 +1290,8 @@ impl StorageUseCase for ContactService {
|
||||
let user_id = params["user_id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
||||
let user_id = Uuid::parse_str(user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
|
||||
|
||||
let result = self.list_contacts(address_book_id, user_id).await?;
|
||||
Ok(serde_json::to_value(result).unwrap())
|
||||
@@ -1279,6 +1308,8 @@ impl StorageUseCase for ContactService {
|
||||
let user_id = params["user_id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
||||
let user_id = Uuid::parse_str(user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
|
||||
|
||||
let result = self
|
||||
.search_contacts(address_book_id, query, user_id)
|
||||
@@ -1317,6 +1348,8 @@ impl StorageUseCase for ContactService {
|
||||
let user_id = params["user_id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
||||
let user_id = Uuid::parse_str(user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
|
||||
|
||||
self.delete_group(group_id, user_id).await?;
|
||||
Ok(serde_json::Value::Null)
|
||||
@@ -1329,6 +1362,8 @@ impl StorageUseCase for ContactService {
|
||||
let user_id = params["user_id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
||||
let user_id = Uuid::parse_str(user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
|
||||
|
||||
let result = self.get_group(group_id, user_id).await?;
|
||||
Ok(serde_json::to_value(result).unwrap())
|
||||
@@ -1341,6 +1376,8 @@ impl StorageUseCase for ContactService {
|
||||
let user_id = params["user_id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
||||
let user_id = Uuid::parse_str(user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
|
||||
|
||||
let result = self.list_groups(address_book_id, user_id).await?;
|
||||
Ok(serde_json::to_value(result).unwrap())
|
||||
@@ -1356,6 +1393,8 @@ impl StorageUseCase for ContactService {
|
||||
let user_id = params["user_id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
||||
let user_id = Uuid::parse_str(user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
|
||||
|
||||
self.add_contact_to_group(dto, user_id).await?;
|
||||
Ok(serde_json::Value::Null)
|
||||
@@ -1369,6 +1408,8 @@ impl StorageUseCase for ContactService {
|
||||
let user_id = params["user_id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
||||
let user_id = Uuid::parse_str(user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
|
||||
|
||||
self.remove_contact_from_group(dto, user_id).await?;
|
||||
Ok(serde_json::Value::Null)
|
||||
@@ -1381,6 +1422,8 @@ impl StorageUseCase for ContactService {
|
||||
let user_id = params["user_id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
||||
let user_id = Uuid::parse_str(user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
|
||||
|
||||
let result = self.list_contacts_in_group(group_id, user_id).await?;
|
||||
Ok(serde_json::to_value(result).unwrap())
|
||||
@@ -1393,6 +1436,8 @@ impl StorageUseCase for ContactService {
|
||||
let user_id = params["user_id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
||||
let user_id = Uuid::parse_str(user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
|
||||
|
||||
let result = self.list_groups_for_contact(contact_id, user_id).await?;
|
||||
Ok(serde_json::to_value(result).unwrap())
|
||||
@@ -1407,6 +1452,8 @@ impl StorageUseCase for ContactService {
|
||||
let user_id = params["user_id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
||||
let user_id = Uuid::parse_str(user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
|
||||
|
||||
let result = self.get_contact_vcard(contact_id, user_id).await?;
|
||||
Ok(serde_json::to_value(result).unwrap())
|
||||
@@ -1419,6 +1466,8 @@ impl StorageUseCase for ContactService {
|
||||
let user_id = params["user_id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| DomainError::validation_error("Missing user_id parameter"))?;
|
||||
let user_id = Uuid::parse_str(user_id)
|
||||
.map_err(|_| DomainError::validation_error("Invalid user_id format"))?;
|
||||
|
||||
let result = self
|
||||
.get_contacts_as_vcards(address_book_id, user_id)
|
||||
|
||||
@@ -22,6 +22,7 @@ use crate::infrastructure::repositories::pg::DeviceCodePgRepository;
|
||||
use crate::infrastructure::repositories::pg::SessionPgRepository;
|
||||
use crate::infrastructure::repositories::pg::UserPgRepository;
|
||||
use crate::infrastructure::services::jwt_service::JwtTokenService;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Default device code lifetime: 15 minutes (RFC 8628 recommends 5-30 min).
|
||||
const DEVICE_CODE_LIFETIME_SECS: i64 = 900;
|
||||
@@ -155,7 +156,7 @@ impl DeviceAuthService {
|
||||
///
|
||||
/// * `user_code` — the code from the verification page
|
||||
/// * `user_id` — the authenticated user's ID (from session/JWT)
|
||||
pub async fn approve(&self, user_code: &str, user_id: &str) -> Result<(), DomainError> {
|
||||
pub async fn approve(&self, user_code: &str, user_id: Uuid) -> Result<(), DomainError> {
|
||||
let normalized = user_code.trim().to_uppercase().replace(' ', "");
|
||||
|
||||
let mut dc = self
|
||||
@@ -180,7 +181,7 @@ impl DeviceAuthService {
|
||||
|
||||
// Persist refresh token as a session
|
||||
let session = Session::new(
|
||||
user_id.to_string(),
|
||||
user_id,
|
||||
refresh_token.clone(),
|
||||
None, // ip_address
|
||||
Some(format!("device:{}", dc.client_name())), // user_agent
|
||||
@@ -189,7 +190,7 @@ impl DeviceAuthService {
|
||||
self.session_storage.create_session(session).await?;
|
||||
|
||||
// Store tokens on the device code entity
|
||||
dc.authorize(user_id.to_string(), access_token, refresh_token);
|
||||
dc.authorize(user_id, access_token, refresh_token);
|
||||
self.device_code_storage.update_device_code(dc).await?;
|
||||
|
||||
tracing::info!(
|
||||
@@ -297,7 +298,7 @@ impl DeviceAuthService {
|
||||
|
||||
pub async fn list_user_devices(
|
||||
&self,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<DeviceInfoDto>, DomainError> {
|
||||
let codes = self.device_code_storage.list_by_user(user_id).await?;
|
||||
Ok(codes
|
||||
@@ -318,7 +319,7 @@ impl DeviceAuthService {
|
||||
// 8. Revoke — user revokes a device authorization
|
||||
// ========================================================================
|
||||
|
||||
pub async fn revoke_device(&self, device_id: &str, user_id: &str) -> Result<(), DomainError> {
|
||||
pub async fn revoke_device(&self, device_id: Uuid, user_id: Uuid) -> Result<(), DomainError> {
|
||||
// Verify ownership before deleting
|
||||
let devices = self.device_code_storage.list_by_user(user_id).await?;
|
||||
let found = devices.iter().any(|d| d.id() == device_id);
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use tracing::info;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::application::dtos::favorites_dto::{
|
||||
BatchFavoritesResult, BatchFavoritesStats, FavoriteItemDto,
|
||||
@@ -27,7 +28,7 @@ impl FavoritesService {
|
||||
|
||||
impl FavoritesUseCase for FavoritesService {
|
||||
/// Get all favorites for a user
|
||||
async fn get_favorites(&self, user_id: &str) -> Result<Vec<FavoriteItemDto>> {
|
||||
async fn get_favorites(&self, user_id: Uuid) -> Result<Vec<FavoriteItemDto>> {
|
||||
info!("Getting favorites for user: {}", user_id);
|
||||
let favorites = self.repo.get_favorites(user_id).await?;
|
||||
info!(
|
||||
@@ -39,7 +40,7 @@ impl FavoritesUseCase for FavoritesService {
|
||||
}
|
||||
|
||||
/// Add an item to user's favorites
|
||||
async fn add_to_favorites(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()> {
|
||||
async fn add_to_favorites(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<()> {
|
||||
info!(
|
||||
"Adding {} '{}' to favorites for user {}",
|
||||
item_type, item_id, user_id
|
||||
@@ -64,7 +65,7 @@ impl FavoritesUseCase for FavoritesService {
|
||||
/// Remove an item from user's favorites
|
||||
async fn remove_from_favorites(
|
||||
&self,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
item_id: &str,
|
||||
item_type: &str,
|
||||
) -> Result<bool> {
|
||||
@@ -91,7 +92,7 @@ impl FavoritesUseCase for FavoritesService {
|
||||
}
|
||||
|
||||
/// Check if an item is in user's favorites
|
||||
async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool> {
|
||||
async fn is_favorite(&self, user_id: Uuid, item_id: &str, item_type: &str) -> Result<bool> {
|
||||
info!(
|
||||
"Checking if {} '{}' is favorite for user {}",
|
||||
item_type, item_id, user_id
|
||||
@@ -101,7 +102,7 @@ impl FavoritesUseCase for FavoritesService {
|
||||
|
||||
async fn batch_add_to_favorites(
|
||||
&self,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
items: &[(String, String)],
|
||||
) -> Result<BatchFavoritesResult> {
|
||||
info!(
|
||||
@@ -148,7 +149,7 @@ impl FavoritesUseCase for FavoritesService {
|
||||
|
||||
async fn batch_check_favorites(
|
||||
&self,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
item_ids: &[(&str, &str)],
|
||||
) -> Result<HashSet<String>> {
|
||||
self.repo.batch_check_favorites(user_id, item_ids).await
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::common::errors::DomainError;
|
||||
use crate::infrastructure::repositories::pg::file_blob_read_repository::FileBlobReadRepository;
|
||||
use crate::infrastructure::repositories::pg::file_blob_write_repository::FileBlobWriteRepository;
|
||||
use tracing::{error, info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Service for file management operations (move, delete).
|
||||
///
|
||||
@@ -46,7 +47,7 @@ impl FileManagementService {
|
||||
}
|
||||
|
||||
/// Verifies ownership via the read repository.
|
||||
async fn verify_owner(&self, file_id: &str, caller_id: &str) -> Result<(), DomainError> {
|
||||
async fn verify_owner(&self, file_id: &str, caller_id: Uuid) -> Result<(), DomainError> {
|
||||
if let Some(read) = &self.file_read {
|
||||
read.verify_file_owner(file_id, caller_id).await
|
||||
} else {
|
||||
@@ -92,7 +93,7 @@ impl FileManagementUseCase for FileManagementService {
|
||||
async fn move_file_owned(
|
||||
&self,
|
||||
file_id: &str,
|
||||
caller_id: &str,
|
||||
caller_id: Uuid,
|
||||
folder_id: Option<String>,
|
||||
) -> Result<FileDto, DomainError> {
|
||||
self.verify_owner(file_id, caller_id).await?;
|
||||
@@ -131,7 +132,7 @@ impl FileManagementUseCase for FileManagementService {
|
||||
async fn copy_file_owned(
|
||||
&self,
|
||||
file_id: &str,
|
||||
caller_id: &str,
|
||||
caller_id: Uuid,
|
||||
target_folder_id: Option<String>,
|
||||
) -> Result<FileDto, DomainError> {
|
||||
self.verify_owner(file_id, caller_id).await?;
|
||||
@@ -162,7 +163,7 @@ impl FileManagementUseCase for FileManagementService {
|
||||
async fn rename_file_owned(
|
||||
&self,
|
||||
file_id: &str,
|
||||
caller_id: &str,
|
||||
caller_id: Uuid,
|
||||
new_name: &str,
|
||||
) -> Result<FileDto, DomainError> {
|
||||
self.verify_owner(file_id, caller_id).await?;
|
||||
@@ -173,7 +174,7 @@ impl FileManagementUseCase for FileManagementService {
|
||||
self.file_repository.delete_file(id).await
|
||||
}
|
||||
|
||||
async fn delete_file_owned(&self, id: &str, caller_id: &str) -> Result<(), DomainError> {
|
||||
async fn delete_file_owned(&self, id: &str, caller_id: Uuid) -> Result<(), DomainError> {
|
||||
self.verify_owner(id, caller_id).await?;
|
||||
self.delete_file(id).await
|
||||
}
|
||||
@@ -184,7 +185,7 @@ impl FileManagementUseCase for FileManagementService {
|
||||
/// `trg_files_decrement_blob_ref` which fires on DELETE FROM storage.files.
|
||||
/// We do NOT decrement here — trashing is a soft-delete (UPDATE, not DELETE)
|
||||
/// so the blob must remain referenced until the file is permanently deleted.
|
||||
async fn delete_with_cleanup(&self, id: &str, user_id: &str) -> Result<bool, DomainError> {
|
||||
async fn delete_with_cleanup(&self, id: &str, user_id: Uuid) -> Result<bool, DomainError> {
|
||||
// Step 1: Try trash (soft delete — file row stays, blob stays referenced)
|
||||
if let Some(trash) = &self.trash_service {
|
||||
info!("Moving file to trash: {}", id);
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::infrastructure::services::image_transcode_service::{
|
||||
ImageTranscodeService, OutputFormat,
|
||||
};
|
||||
use tracing::{debug, info};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Threshold below which files are served from RAM cache (10 MB).
|
||||
const CACHE_THRESHOLD: u64 = 10 * 1024 * 1024;
|
||||
@@ -201,7 +202,7 @@ impl FileRetrievalUseCase for FileRetrievalService {
|
||||
Ok(FileDto::from(file))
|
||||
}
|
||||
|
||||
async fn get_file_owned(&self, id: &str, caller_id: &str) -> Result<FileDto, DomainError> {
|
||||
async fn get_file_owned(&self, id: &str, caller_id: Uuid) -> Result<FileDto, DomainError> {
|
||||
let file = self.file_read.get_file_for_owner(id, caller_id).await?;
|
||||
Ok(FileDto::from(file))
|
||||
}
|
||||
@@ -226,7 +227,7 @@ impl FileRetrievalUseCase for FileRetrievalService {
|
||||
async fn list_files_owned(
|
||||
&self,
|
||||
folder_id: Option<&str>,
|
||||
owner_id: &str,
|
||||
owner_id: Uuid,
|
||||
) -> Result<Vec<FileDto>, DomainError> {
|
||||
let files = self
|
||||
.file_read
|
||||
@@ -245,7 +246,7 @@ impl FileRetrievalUseCase for FileRetrievalService {
|
||||
async fn get_file_stream_owned(
|
||||
&self,
|
||||
id: &str,
|
||||
caller_id: &str,
|
||||
caller_id: Uuid,
|
||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
|
||||
self.file_read.verify_file_owner(id, caller_id).await?;
|
||||
self.file_read.get_file_stream(id).await
|
||||
@@ -267,7 +268,7 @@ impl FileRetrievalUseCase for FileRetrievalService {
|
||||
async fn get_file_optimized_owned(
|
||||
&self,
|
||||
id: &str,
|
||||
caller_id: &str,
|
||||
caller_id: Uuid,
|
||||
accept_webp: bool,
|
||||
prefer_original: bool,
|
||||
) -> Result<(FileDto, OptimizedFileContent), DomainError> {
|
||||
@@ -302,7 +303,7 @@ impl FileRetrievalUseCase for FileRetrievalService {
|
||||
async fn get_file_range_stream_owned(
|
||||
&self,
|
||||
id: &str,
|
||||
caller_id: &str,
|
||||
caller_id: Uuid,
|
||||
start: u64,
|
||||
end: Option<u64>,
|
||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError> {
|
||||
@@ -336,7 +337,7 @@ impl FileRetrievalUseCase for FileRetrievalService {
|
||||
async fn list_files_batch_for_owner(
|
||||
&self,
|
||||
folder_id: Option<&str>,
|
||||
owner_id: &str,
|
||||
owner_id: Uuid,
|
||||
offset: i64,
|
||||
limit: i64,
|
||||
) -> Result<Vec<FileDto>, DomainError> {
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::domain::repositories::folder_repository::FolderRepository;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::infrastructure::repositories::pg::folder_db_repository::FolderDbRepository;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Implementation of the use case for folder operations
|
||||
pub struct FolderService {
|
||||
@@ -35,7 +36,7 @@ impl FolderService {
|
||||
async fn get_folder_owned(
|
||||
&self,
|
||||
_id: &str,
|
||||
_caller_id: &str,
|
||||
_caller_id: Uuid,
|
||||
) -> Result<FolderDto, DomainError> {
|
||||
Ok(FolderDto::empty())
|
||||
}
|
||||
@@ -54,7 +55,7 @@ impl FolderService {
|
||||
async fn list_folders_for_owner(
|
||||
&self,
|
||||
_parent_id: Option<&str>,
|
||||
_owner_id: &str,
|
||||
_owner_id: Uuid,
|
||||
) -> Result<Vec<FolderDto>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
@@ -80,7 +81,7 @@ impl FolderService {
|
||||
async fn list_folders_for_owner_paginated(
|
||||
&self,
|
||||
_parent_id: Option<&str>,
|
||||
_owner_id: &str,
|
||||
_owner_id: Uuid,
|
||||
_pagination: &crate::application::dtos::pagination::PaginationRequestDto,
|
||||
) -> Result<
|
||||
crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>,
|
||||
@@ -100,7 +101,7 @@ impl FolderService {
|
||||
&self,
|
||||
_id: &str,
|
||||
_dto: RenameFolderDto,
|
||||
_caller_id: &str,
|
||||
_caller_id: Uuid,
|
||||
) -> Result<FolderDto, DomainError> {
|
||||
Ok(FolderDto::empty())
|
||||
}
|
||||
@@ -109,18 +110,18 @@ impl FolderService {
|
||||
&self,
|
||||
_id: &str,
|
||||
_dto: MoveFolderDto,
|
||||
_caller_id: &str,
|
||||
_caller_id: Uuid,
|
||||
) -> Result<FolderDto, DomainError> {
|
||||
Ok(FolderDto::empty())
|
||||
}
|
||||
|
||||
async fn delete_folder(&self, _id: &str, _caller_id: &str) -> Result<(), DomainError> {
|
||||
async fn delete_folder(&self, _id: &str, _caller_id: Uuid) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_home_folder(
|
||||
&self,
|
||||
_user_id: &str,
|
||||
_user_id: Uuid,
|
||||
_name: String,
|
||||
) -> Result<FolderDto, DomainError> {
|
||||
Ok(FolderDto::empty())
|
||||
@@ -170,12 +171,13 @@ impl FolderUseCase for FolderService {
|
||||
/// Creates a root-level home folder for a user during registration.
|
||||
async fn create_home_folder(
|
||||
&self,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
name: String,
|
||||
) -> Result<FolderDto, DomainError> {
|
||||
let user_id_str = user_id.to_string();
|
||||
let folder = self
|
||||
.folder_storage
|
||||
.create_home_folder(user_id, name)
|
||||
.create_home_folder(&user_id_str, name)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::internal_error(
|
||||
@@ -205,9 +207,10 @@ impl FolderUseCase for FolderService {
|
||||
}
|
||||
|
||||
/// Gets a folder by its ID, enforcing that `caller_id` is the owner.
|
||||
async fn get_folder_owned(&self, id: &str, caller_id: &str) -> Result<FolderDto, DomainError> {
|
||||
async fn get_folder_owned(&self, id: &str, caller_id: Uuid) -> Result<FolderDto, DomainError> {
|
||||
let caller_id_str = caller_id.to_string();
|
||||
let folder_dto = self.get_folder(id).await?;
|
||||
if folder_dto.owner_id.as_deref() != Some(caller_id) {
|
||||
if folder_dto.owner_id.as_deref() != Some(caller_id_str.as_str()) {
|
||||
tracing::warn!(
|
||||
"get_folder_owned: user '{}' attempted to access folder '{}' owned by '{:?}'",
|
||||
caller_id,
|
||||
@@ -260,11 +263,12 @@ impl FolderUseCase for FolderService {
|
||||
async fn list_folders_for_owner(
|
||||
&self,
|
||||
parent_id: Option<&str>,
|
||||
owner_id: &str,
|
||||
owner_id: Uuid,
|
||||
) -> Result<Vec<FolderDto>, DomainError> {
|
||||
let owner_id_str = owner_id.to_string();
|
||||
let folders = self
|
||||
.folder_storage
|
||||
.list_folders_by_owner(parent_id, owner_id)
|
||||
.list_folders_by_owner(parent_id, &owner_id_str)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::internal_error(
|
||||
@@ -283,10 +287,10 @@ impl FolderUseCase for FolderService {
|
||||
"No root folders found for user {}, creating home folder automatically",
|
||||
owner_id
|
||||
);
|
||||
let folder_name = format!("My Folder - {}", &owner_id[..8.min(owner_id.len())]);
|
||||
let folder_name = format!("My Folder - {}", &owner_id_str[..8.min(owner_id_str.len())]);
|
||||
match self
|
||||
.folder_storage
|
||||
.create_home_folder(owner_id, folder_name.clone())
|
||||
.create_home_folder(&owner_id_str, folder_name.clone())
|
||||
.await
|
||||
{
|
||||
Ok(home_folder) => {
|
||||
@@ -346,17 +350,18 @@ impl FolderUseCase for FolderService {
|
||||
async fn list_folders_for_owner_paginated(
|
||||
&self,
|
||||
parent_id: Option<&str>,
|
||||
owner_id: &str,
|
||||
owner_id: Uuid,
|
||||
pagination: &crate::application::dtos::pagination::PaginationRequestDto,
|
||||
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>
|
||||
{
|
||||
let owner_id_str = owner_id.to_string();
|
||||
let pagination = pagination.validate_and_adjust();
|
||||
|
||||
let (folders, total_items) = self
|
||||
.folder_storage
|
||||
.list_folders_by_owner_paginated(
|
||||
parent_id,
|
||||
owner_id,
|
||||
&owner_id_str,
|
||||
pagination.offset(),
|
||||
pagination.limit(),
|
||||
true,
|
||||
@@ -389,8 +394,9 @@ impl FolderUseCase for FolderService {
|
||||
&self,
|
||||
id: &str,
|
||||
dto: RenameFolderDto,
|
||||
caller_id: &str,
|
||||
caller_id: Uuid,
|
||||
) -> Result<FolderDto, DomainError> {
|
||||
let caller_id_str = caller_id.to_string();
|
||||
// Input validation
|
||||
if dto.name.is_empty() {
|
||||
return Err(DomainError::new(
|
||||
@@ -408,7 +414,7 @@ impl FolderUseCase for FolderService {
|
||||
)
|
||||
})?;
|
||||
|
||||
if existing_folder.owner_id() != Some(caller_id) {
|
||||
if existing_folder.owner_id() != Some(caller_id_str.as_str()) {
|
||||
tracing::warn!(
|
||||
"rename_folder: user '{}' attempted to rename folder '{}' owned by '{:?}'",
|
||||
caller_id,
|
||||
@@ -438,8 +444,9 @@ impl FolderUseCase for FolderService {
|
||||
&self,
|
||||
id: &str,
|
||||
dto: MoveFolderDto,
|
||||
caller_id: &str,
|
||||
caller_id: Uuid,
|
||||
) -> Result<FolderDto, DomainError> {
|
||||
let caller_id_str = caller_id.to_string();
|
||||
// Verify the source folder exists and belongs to the caller
|
||||
let source_folder = self.folder_storage.get_folder(id).await.map_err(|e| {
|
||||
DomainError::internal_error(
|
||||
@@ -448,7 +455,7 @@ impl FolderUseCase for FolderService {
|
||||
)
|
||||
})?;
|
||||
|
||||
if source_folder.owner_id() != Some(caller_id) {
|
||||
if source_folder.owner_id() != Some(caller_id_str.as_str()) {
|
||||
tracing::warn!(
|
||||
"move_folder: user '{}' attempted to move folder '{}' owned by '{:?}'",
|
||||
caller_id,
|
||||
@@ -495,7 +502,8 @@ impl FolderUseCase for FolderService {
|
||||
}
|
||||
|
||||
/// Deletes a folder after verifying ownership.
|
||||
async fn delete_folder(&self, id: &str, caller_id: &str) -> Result<(), DomainError> {
|
||||
async fn delete_folder(&self, id: &str, caller_id: Uuid) -> Result<(), DomainError> {
|
||||
let caller_id_str = caller_id.to_string();
|
||||
// Verify the folder exists and belongs to the caller
|
||||
let folder = self.folder_storage.get_folder(id).await.map_err(|e| {
|
||||
DomainError::internal_error(
|
||||
@@ -504,7 +512,7 @@ impl FolderUseCase for FolderService {
|
||||
)
|
||||
})?;
|
||||
|
||||
if folder.owner_id() != Some(caller_id) {
|
||||
if folder.owner_id() != Some(caller_id_str.as_str()) {
|
||||
tracing::warn!(
|
||||
"delete_folder: user '{}' attempted to delete folder '{}' owned by '{:?}'",
|
||||
caller_id,
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::common::errors::{DomainError, ErrorKind, Result};
|
||||
use crate::infrastructure::repositories::pg::RecentItemsPgRepository;
|
||||
use std::sync::Arc;
|
||||
use tracing::info;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Implementation of the use case for managing recent items.
|
||||
///
|
||||
@@ -28,7 +29,7 @@ impl RecentItemsUseCase for RecentService {
|
||||
/// Get recent items for a user
|
||||
async fn get_recent_items(
|
||||
&self,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
limit: Option<i32>,
|
||||
) -> Result<Vec<RecentItemDto>> {
|
||||
info!("Getting recent items for user: {}", user_id);
|
||||
@@ -47,7 +48,7 @@ impl RecentItemsUseCase for RecentService {
|
||||
/// Record access to an item
|
||||
async fn record_item_access(
|
||||
&self,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
item_id: &str,
|
||||
item_type: &str,
|
||||
) -> Result<()> {
|
||||
@@ -77,7 +78,7 @@ impl RecentItemsUseCase for RecentService {
|
||||
/// Remove an item from recent
|
||||
async fn remove_from_recent(
|
||||
&self,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
item_id: &str,
|
||||
item_type: &str,
|
||||
) -> Result<bool> {
|
||||
@@ -101,7 +102,7 @@ impl RecentItemsUseCase for RecentService {
|
||||
}
|
||||
|
||||
/// Clear all recent items
|
||||
async fn clear_recent_items(&self, user_id: &str) -> Result<()> {
|
||||
async fn clear_recent_items(&self, user_id: Uuid) -> Result<()> {
|
||||
info!("Clearing all recent items for user {}", user_id);
|
||||
self.repo.clear_all(user_id).await?;
|
||||
info!("Cleared all recent items for user {}", user_id);
|
||||
|
||||
@@ -19,6 +19,7 @@ use crate::domain::repositories::folder_repository::FolderRepository;
|
||||
use crate::infrastructure::repositories::pg::file_blob_read_repository::FileBlobReadRepository;
|
||||
use crate::infrastructure::repositories::pg::folder_db_repository::FolderDbRepository;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use uuid::Uuid;
|
||||
|
||||
/**
|
||||
* High-performance search service implementation for files and folders.
|
||||
@@ -285,12 +286,13 @@ impl SearchUseCase for SearchService {
|
||||
async fn search(
|
||||
&self,
|
||||
criteria: SearchCriteriaDto,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<Arc<SearchResultsDto>> {
|
||||
let start = Instant::now();
|
||||
let user_id_str = user_id.to_string();
|
||||
|
||||
// Try to get from cache
|
||||
let cache_key = Self::create_cache_key(&criteria, user_id);
|
||||
let cache_key = Self::create_cache_key(&criteria, &user_id_str);
|
||||
if let Some(cached_results) = self.get_from_cache(cache_key).await {
|
||||
return Ok(cached_results);
|
||||
}
|
||||
@@ -321,7 +323,7 @@ impl SearchUseCase for SearchService {
|
||||
.search_folders(
|
||||
criteria.folder_id.as_deref(),
|
||||
criteria.name_contains.as_deref(),
|
||||
user_id,
|
||||
&user_id_str,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -403,7 +405,7 @@ impl SearchUseCase for SearchService {
|
||||
.search_folders(
|
||||
criteria.folder_id.as_deref(),
|
||||
criteria.name_contains.as_deref(),
|
||||
user_id,
|
||||
&user_id_str,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
@@ -504,7 +506,7 @@ impl SearchService {
|
||||
async fn search(
|
||||
&self,
|
||||
_criteria: SearchCriteriaDto,
|
||||
_user_id: &str,
|
||||
_user_id: Uuid,
|
||||
) -> Result<Arc<SearchResultsDto>> {
|
||||
Ok(Arc::new(SearchResultsDto::empty()))
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use thiserror::Error;
|
||||
use tokio::sync::Semaphore;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::domain::repositories::folder_repository::FolderRepository;
|
||||
use crate::infrastructure::repositories::pg::SharePgRepository;
|
||||
@@ -150,7 +151,7 @@ impl ShareService {
|
||||
/// but belongs to a different user — this prevents share-ID enumeration
|
||||
/// attacks where an attacker probes IDs and uses 403-vs-404 to learn
|
||||
/// which ones are valid.
|
||||
async fn fetch_owned_share(&self, id: &str, requester_id: &str) -> Result<Share, DomainError> {
|
||||
async fn fetch_owned_share(&self, id: Uuid, requester_id: Uuid) -> Result<Share, DomainError> {
|
||||
let share = self
|
||||
.share_repository
|
||||
.find_share_by_id_for_user(id, requester_id)
|
||||
@@ -163,7 +164,7 @@ impl ShareService {
|
||||
impl ShareUseCase for ShareService {
|
||||
async fn create_shared_link(
|
||||
&self,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
dto: CreateShareDto,
|
||||
) -> Result<ShareDto, DomainError> {
|
||||
// Convert the item type
|
||||
@@ -187,7 +188,7 @@ impl ShareUseCase for ShareService {
|
||||
dto.item_id.clone(),
|
||||
dto.item_name.clone(),
|
||||
item_type,
|
||||
user_id.to_string(),
|
||||
user_id,
|
||||
permissions,
|
||||
password_hash,
|
||||
dto.expires_at,
|
||||
@@ -205,7 +206,7 @@ impl ShareUseCase for ShareService {
|
||||
Ok(ShareDto::from_entity(&saved_share, &self.config.base_url()))
|
||||
}
|
||||
|
||||
async fn get_shared_link(&self, id: &str, requester_id: &str) -> Result<ShareDto, DomainError> {
|
||||
async fn get_shared_link(&self, id: Uuid, requester_id: Uuid) -> Result<ShareDto, DomainError> {
|
||||
// SECURITY: ownership-verified lookup — returns 404 if the share
|
||||
// doesn't exist OR belongs to another user.
|
||||
let share = self.fetch_owned_share(id, requester_id).await?;
|
||||
@@ -253,7 +254,7 @@ impl ShareUseCase for ShareService {
|
||||
&self,
|
||||
item_id: &str,
|
||||
item_type: &ShareItemType,
|
||||
requester_id: &str,
|
||||
requester_id: Uuid,
|
||||
) -> Result<Vec<ShareDto>, DomainError> {
|
||||
// SECURITY: only return shares created by the requester
|
||||
let shares = self
|
||||
@@ -276,8 +277,8 @@ impl ShareUseCase for ShareService {
|
||||
|
||||
async fn update_shared_link(
|
||||
&self,
|
||||
id: &str,
|
||||
requester_id: &str,
|
||||
id: Uuid,
|
||||
requester_id: Uuid,
|
||||
dto: UpdateShareDto,
|
||||
) -> Result<ShareDto, DomainError> {
|
||||
// SECURITY: ownership-verified lookup — prevents IDOR
|
||||
@@ -322,7 +323,7 @@ impl ShareUseCase for ShareService {
|
||||
))
|
||||
}
|
||||
|
||||
async fn delete_shared_link(&self, id: &str, requester_id: &str) -> Result<(), DomainError> {
|
||||
async fn delete_shared_link(&self, id: Uuid, requester_id: Uuid) -> Result<(), DomainError> {
|
||||
// SECURITY: ownership-verified delete — only the creator can remove
|
||||
self.share_repository
|
||||
.delete_share_for_user(id, requester_id)
|
||||
@@ -333,7 +334,7 @@ impl ShareUseCase for ShareService {
|
||||
|
||||
async fn get_user_shared_links(
|
||||
&self,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
page: usize,
|
||||
per_page: usize,
|
||||
) -> Result<PaginatedResponseDto<ShareDto>, DomainError> {
|
||||
@@ -519,7 +520,7 @@ mod tests {
|
||||
{
|
||||
async fn create_shared_link(
|
||||
&self,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
dto: CreateShareDto,
|
||||
) -> Result<ShareDto, DomainError> {
|
||||
let item_type = ShareItemType::try_from(dto.item_type.as_str())
|
||||
@@ -534,7 +535,7 @@ mod tests {
|
||||
dto.item_id.clone(),
|
||||
dto.item_name.clone(),
|
||||
item_type,
|
||||
user_id.to_string(),
|
||||
user_id,
|
||||
permissions,
|
||||
password_hash,
|
||||
dto.expires_at,
|
||||
@@ -550,8 +551,8 @@ mod tests {
|
||||
|
||||
async fn get_shared_link(
|
||||
&self,
|
||||
id: &str,
|
||||
requester_id: &str,
|
||||
id: Uuid,
|
||||
requester_id: Uuid,
|
||||
) -> Result<ShareDto, DomainError> {
|
||||
let share = self
|
||||
.share_repository
|
||||
@@ -584,7 +585,7 @@ mod tests {
|
||||
&self,
|
||||
item_id: &str,
|
||||
item_type: &ShareItemType,
|
||||
requester_id: &str,
|
||||
requester_id: Uuid,
|
||||
) -> Result<Vec<ShareDto>, DomainError> {
|
||||
let shares = self
|
||||
.share_repository
|
||||
@@ -600,8 +601,8 @@ mod tests {
|
||||
|
||||
async fn update_shared_link(
|
||||
&self,
|
||||
id: &str,
|
||||
requester_id: &str,
|
||||
id: Uuid,
|
||||
requester_id: Uuid,
|
||||
dto: UpdateShareDto,
|
||||
) -> Result<ShareDto, DomainError> {
|
||||
let mut share = self
|
||||
@@ -635,8 +636,8 @@ mod tests {
|
||||
|
||||
async fn delete_shared_link(
|
||||
&self,
|
||||
id: &str,
|
||||
requester_id: &str,
|
||||
id: Uuid,
|
||||
requester_id: Uuid,
|
||||
) -> Result<(), DomainError> {
|
||||
self.share_repository
|
||||
.delete_share_for_user(id, requester_id)
|
||||
@@ -647,7 +648,7 @@ mod tests {
|
||||
|
||||
async fn get_user_shared_links(
|
||||
&self,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
page: usize,
|
||||
per_page: usize,
|
||||
) -> Result<PaginatedResponseDto<ShareDto>, DomainError> {
|
||||
@@ -1018,28 +1019,30 @@ mod tests {
|
||||
|
||||
async fn find_share_by_id_for_user(
|
||||
&self,
|
||||
id: &str,
|
||||
user_id: &str,
|
||||
id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> Result<Share, DomainError> {
|
||||
let shares = self.shares.lock().unwrap();
|
||||
let id_str = id.to_string();
|
||||
shares
|
||||
.get(id)
|
||||
.get(&id_str)
|
||||
.filter(|s| s.created_by() == user_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| DomainError::not_found("Share", id))
|
||||
.ok_or_else(|| DomainError::not_found("Share", &id_str))
|
||||
}
|
||||
|
||||
async fn delete_share_for_user(&self, id: &str, user_id: &str) -> Result<(), DomainError> {
|
||||
async fn delete_share_for_user(&self, id: Uuid, user_id: Uuid) -> Result<(), DomainError> {
|
||||
let mut shares = self.shares.lock().unwrap();
|
||||
let mut tokens = self.tokens.lock().unwrap();
|
||||
let id_str = id.to_string();
|
||||
|
||||
let share = shares
|
||||
.get(id)
|
||||
.get(&id_str)
|
||||
.filter(|s| s.created_by() == user_id)
|
||||
.ok_or_else(|| DomainError::not_found("Share", id))?;
|
||||
.ok_or_else(|| DomainError::not_found("Share", &id_str))?;
|
||||
|
||||
tokens.remove(share.token());
|
||||
shares.remove(id);
|
||||
shares.remove(&id_str);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1047,7 +1050,7 @@ mod tests {
|
||||
&self,
|
||||
item_id: &str,
|
||||
item_type: &ShareItemType,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<Share>, DomainError> {
|
||||
let shares = self.shares.lock().unwrap();
|
||||
let type_str = item_type.to_string();
|
||||
@@ -1078,7 +1081,7 @@ mod tests {
|
||||
|
||||
async fn find_shares_by_user(
|
||||
&self,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
) -> Result<(Vec<Share>, usize), DomainError> {
|
||||
@@ -1125,7 +1128,7 @@ mod tests {
|
||||
}),
|
||||
};
|
||||
|
||||
let result = service.create_shared_link("user123", dto).await;
|
||||
let result = service.create_shared_link(Uuid::new_v4(), dto).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let share_dto = result.unwrap();
|
||||
|
||||
@@ -6,6 +6,7 @@ use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
use tokio::task;
|
||||
use tracing::{debug, error, info};
|
||||
use uuid::Uuid;
|
||||
|
||||
/**
|
||||
* Service for managing and updating user storage usage statistics.
|
||||
@@ -31,7 +32,7 @@ impl StorageUsageService {
|
||||
}
|
||||
|
||||
/// Calculates and updates storage usage for a specific user
|
||||
pub async fn update_user_storage_usage(&self, user_id: &str) -> Result<i64, DomainError> {
|
||||
pub async fn update_user_storage_usage(&self, user_id: Uuid) -> Result<i64, DomainError> {
|
||||
info!("Updating storage usage for user: {}", user_id);
|
||||
|
||||
// Calculate storage usage directly from database
|
||||
@@ -52,12 +53,11 @@ impl StorageUsageService {
|
||||
|
||||
/// Calculates a user's storage usage by summing all their file sizes.
|
||||
/// Uses a direct SQL query for O(1) performance.
|
||||
async fn calculate_user_storage_usage(&self, user_id: &str) -> Result<i64, DomainError> {
|
||||
async fn calculate_user_storage_usage(&self, user_id: Uuid) -> Result<i64, DomainError> {
|
||||
debug!("Calculating storage for user: {}", user_id);
|
||||
|
||||
// Direct SQL query to sum all file sizes for this user
|
||||
// This is much more efficient than recursively walking folders
|
||||
// Note: user_id is stored as varchar, not uuid, so we bind it directly as text
|
||||
let total_size: i64 = sqlx::query_scalar(
|
||||
r#"
|
||||
SELECT COALESCE(SUM(size), 0)::bigint
|
||||
@@ -88,14 +88,14 @@ impl StorageUsageService {
|
||||
info!("Updating storage usage for username: {}", username);
|
||||
|
||||
let user = self.user_repository.get_user_by_username(username).await?;
|
||||
let user_id = user.id().to_string();
|
||||
let user_id = user.id();
|
||||
|
||||
// Reuse the existing calculation logic
|
||||
let total_usage = self.calculate_user_storage_usage(&user_id).await?;
|
||||
let total_usage = self.calculate_user_storage_usage(user_id).await?;
|
||||
|
||||
// Update the user's storage usage in the database
|
||||
self.user_repository
|
||||
.update_storage_usage(&user_id, total_usage)
|
||||
.update_storage_usage(user_id, total_usage)
|
||||
.await?;
|
||||
|
||||
info!(
|
||||
@@ -112,7 +112,7 @@ impl StorageUsageService {
|
||||
* to the application layer.
|
||||
*/
|
||||
impl StorageUsagePort for StorageUsageService {
|
||||
async fn update_user_storage_usage(&self, user_id: &str) -> Result<i64, DomainError> {
|
||||
async fn update_user_storage_usage(&self, user_id: Uuid) -> Result<i64, DomainError> {
|
||||
StorageUsageService::update_user_storage_usage(self, user_id).await
|
||||
}
|
||||
|
||||
@@ -133,12 +133,12 @@ impl StorageUsagePort for StorageUsageService {
|
||||
|
||||
// Process users in parallel
|
||||
for user in users {
|
||||
let user_id = user.id().to_string();
|
||||
let user_id = user.id();
|
||||
let service_clone = self.clone();
|
||||
|
||||
// Spawn a background task for each user
|
||||
let task = task::spawn(async move {
|
||||
match service_clone.update_user_storage_usage(&user_id).await {
|
||||
match service_clone.update_user_storage_usage(user_id).await {
|
||||
Ok(usage) => {
|
||||
debug!(
|
||||
"Updated storage usage for user {}: {} bytes",
|
||||
@@ -168,7 +168,7 @@ impl StorageUsagePort for StorageUsageService {
|
||||
|
||||
async fn check_storage_quota(
|
||||
&self,
|
||||
user_id: &str,
|
||||
user_id: Uuid,
|
||||
additional_bytes: u64,
|
||||
) -> Result<(), DomainError> {
|
||||
let user = self.user_repository.get_user_by_id(user_id).await?;
|
||||
@@ -206,7 +206,7 @@ impl StorageUsagePort for StorageUsageService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_user_storage_info(&self, user_id: &str) -> Result<(i64, i64), DomainError> {
|
||||
async fn get_user_storage_info(&self, user_id: Uuid) -> Result<(i64, i64), DomainError> {
|
||||
let user = self.user_repository.get_user_by_id(user_id).await?;
|
||||
Ok((user.storage_used_bytes(), user.storage_quota_bytes()))
|
||||
}
|
||||
|
||||
@@ -123,13 +123,10 @@ impl TrashService {
|
||||
|
||||
impl TrashUseCase for TrashService {
|
||||
#[instrument(skip(self))]
|
||||
async fn get_trash_items(&self, user_id: &str) -> Result<Vec<TrashedItemDto>> {
|
||||
async fn get_trash_items(&self, user_id: Uuid) -> Result<Vec<TrashedItemDto>> {
|
||||
debug!("Getting trash items for user: {}", user_id);
|
||||
|
||||
let user_uuid = Uuid::parse_str(user_id)
|
||||
.map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?;
|
||||
|
||||
let items = self.trash_repository.get_trash_items(&user_uuid).await?;
|
||||
let items = self.trash_repository.get_trash_items(&user_id).await?;
|
||||
|
||||
let dtos = items.into_iter().map(|item| self.to_dto(item)).collect();
|
||||
|
||||
@@ -137,7 +134,7 @@ impl TrashUseCase for TrashService {
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: &str) -> Result<()> {
|
||||
async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: Uuid) -> Result<()> {
|
||||
info!(
|
||||
"Moving to trash: type={}, id={}, user={}",
|
||||
item_type, item_id, user_id
|
||||
@@ -163,20 +160,7 @@ impl TrashUseCase for TrashService {
|
||||
}
|
||||
};
|
||||
|
||||
debug!("Validating user UUID: {}", user_id);
|
||||
let user_uuid = match Uuid::parse_str(user_id) {
|
||||
Ok(uuid) => {
|
||||
debug!("Valid user UUID: {}", uuid);
|
||||
uuid
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Invalid user UUID: {} - Error: {}", user_id, e);
|
||||
return Err(DomainError::validation_error(format!(
|
||||
"Invalid user ID: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
};
|
||||
let user_uuid = user_id;
|
||||
|
||||
match item_type {
|
||||
"file" => {
|
||||
@@ -276,7 +260,7 @@ impl TrashUseCase for TrashService {
|
||||
|
||||
// Ownership check — return NotFound (not Forbidden) to
|
||||
// prevent leaking whether the folder exists.
|
||||
if folder.owner_id().is_none_or(|o| o != user_id) {
|
||||
if folder.owner_id().is_none_or(|o| o != user_id.to_string()) {
|
||||
return Err(DomainError::not_found(
|
||||
"Folder",
|
||||
format!("Folder not found: {}", item_id),
|
||||
@@ -331,7 +315,7 @@ impl TrashUseCase for TrashService {
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn restore_item(&self, trash_id: &str, user_id: &str) -> Result<()> {
|
||||
async fn restore_item(&self, trash_id: &str, user_id: Uuid) -> Result<()> {
|
||||
info!("Restoring item {} for user {}", trash_id, user_id);
|
||||
|
||||
let trash_uuid = match Uuid::parse_str(trash_id) {
|
||||
@@ -348,19 +332,7 @@ impl TrashUseCase for TrashService {
|
||||
}
|
||||
};
|
||||
|
||||
let user_uuid = match Uuid::parse_str(user_id) {
|
||||
Ok(id) => {
|
||||
info!("User UUID parsed successfully: {}", id);
|
||||
id
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Invalid user ID format: {} - {}", user_id, e);
|
||||
return Err(DomainError::validation_error(format!(
|
||||
"Invalid user ID: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
};
|
||||
let user_uuid = user_id;
|
||||
|
||||
// Get the trash item
|
||||
info!("Retrieving trash item from repository: ID={}", trash_id);
|
||||
@@ -514,7 +486,7 @@ impl TrashUseCase for TrashService {
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn delete_permanently(&self, trash_id: &str, user_id: &str) -> Result<()> {
|
||||
async fn delete_permanently(&self, trash_id: &str, user_id: Uuid) -> Result<()> {
|
||||
info!(
|
||||
"Permanently deleting item {} for user {}",
|
||||
trash_id, user_id
|
||||
@@ -534,19 +506,7 @@ impl TrashUseCase for TrashService {
|
||||
}
|
||||
};
|
||||
|
||||
let user_uuid = match Uuid::parse_str(user_id) {
|
||||
Ok(id) => {
|
||||
info!("User UUID parsed successfully: {}", id);
|
||||
id
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Invalid user ID format: {} - {}", user_id, e);
|
||||
return Err(DomainError::validation_error(format!(
|
||||
"Invalid user ID: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
};
|
||||
let user_uuid = user_id;
|
||||
|
||||
// Get the trash item
|
||||
info!("Retrieving trash item from repository: ID={}", trash_id);
|
||||
@@ -684,12 +644,9 @@ impl TrashUseCase for TrashService {
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
async fn empty_trash(&self, user_id: &str) -> Result<()> {
|
||||
async fn empty_trash(&self, user_id: Uuid) -> Result<()> {
|
||||
info!("Emptying trash for user {}", user_id);
|
||||
|
||||
let user_uuid = Uuid::parse_str(user_id)
|
||||
.map_err(|e| DomainError::validation_error(format!("Invalid user ID: {}", e)))?;
|
||||
|
||||
// clear_trash() already performs bulk SQL DELETEs in 2 queries:
|
||||
// 1. DELETE FROM storage.files WHERE user_id = $1 AND is_trashed = TRUE
|
||||
// 2. DELETE FROM storage.folders WHERE user_id = $1 AND is_trashed = TRUE
|
||||
@@ -700,7 +657,7 @@ impl TrashUseCase for TrashService {
|
||||
// remove_reference() call is needed.
|
||||
//
|
||||
// Finally it clears the trash_items index for the user.
|
||||
self.trash_repository.clear_trash(&user_uuid).await?;
|
||||
self.trash_repository.clear_trash(&user_id).await?;
|
||||
|
||||
info!("Trash emptied for user {}", user_id);
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user