diff --git a/src/application/ports/calendar_ports.rs b/src/application/ports/calendar_ports.rs index 4f8559c6..56799533 100644 --- a/src/application/ports/calendar_ports.rs +++ b/src/application/ports/calendar_ports.rs @@ -87,6 +87,14 @@ pub trait CalendarStoragePort: Send + Sync + 'static { ) -> Result; async fn delete_event(&self, event_id: &str) -> Result<(), DomainError>; async fn get_event(&self, event_id: &str) -> Result; + /// Indexed single-row lookup by iCalendar UID — the CalDAV + /// object-resource paths must use this instead of listing the whole + /// calendar (every row + its `ical_data`) and filtering client-side. + async fn find_event_by_ical_uid( + &self, + calendar_id: &str, + ical_uid: &str, + ) -> Result, DomainError>; async fn list_events_by_calendar( &self, calendar_id: &str, @@ -180,6 +188,15 @@ pub trait CalendarUseCase: Send + Sync + 'static { event_id: &str, user_id: Uuid, ) -> Result; + /// Resolve one event by its iCalendar UID (the identifier CalDAV + /// object resources are addressed by). `Ok(None)` when no event with + /// that UID exists in the calendar. + async fn get_event_by_ical_uid( + &self, + calendar_id: &str, + ical_uid: &str, + user_id: Uuid, + ) -> Result, DomainError>; async fn list_events( &self, calendar_id: &str, diff --git a/src/application/ports/carddav_ports.rs b/src/application/ports/carddav_ports.rs index 94acb1e5..ee49dcec 100644 --- a/src/application/ports/carddav_ports.rs +++ b/src/application/ports/carddav_ports.rs @@ -71,6 +71,17 @@ pub trait ContactUseCase: Send + Sync + 'static { async fn delete_contact(&self, contact_id: &str, user_id: Uuid) -> Result<(), DomainError>; async fn get_contact(&self, contact_id: &str, user_id: Uuid) -> Result; + /// Resolve one contact by its vCard UID (the identifier CardDAV + /// object resources are addressed by) with an indexed single-row + /// lookup — instead of listing the whole address book (every row + /// with its vCard + JSONB columns) and filtering client-side. + /// `Ok(None)` when no contact with that UID exists in the book. + async fn get_contact_by_uid( + &self, + address_book_id: &str, + uid: &str, + user_id: Uuid, + ) -> Result, DomainError>; async fn list_contacts( &self, address_book_id: &str, diff --git a/src/application/services/calendar_service.rs b/src/application/services/calendar_service.rs index 7ce70475..6b51cb8a 100644 --- a/src/application/services/calendar_service.rs +++ b/src/application/services/calendar_service.rs @@ -277,6 +277,29 @@ impl CalendarUseCase for CalendarService { Ok(event) } + async fn get_event_by_ical_uid( + &self, + calendar_id: &str, + ical_uid: &str, + user_id: Uuid, + ) -> Result, DomainError> { + let has_access = self + .calendar_storage + .check_calendar_access(calendar_id, user_id) + .await?; + let calendar = self.calendar_storage.get_calendar(calendar_id).await?; + if !has_access && !calendar.is_public { + return Err(DomainError::new( + ErrorKind::AccessDenied, + "Calendar", + "You don't have permission to view events in this calendar", + )); + } + self.calendar_storage + .find_event_by_ical_uid(calendar_id, ical_uid) + .await + } + async fn list_events( &self, calendar_id: &str, diff --git a/src/application/services/contact_service.rs b/src/application/services/contact_service.rs index f95e9406..9d44b306 100644 --- a/src/application/services/contact_service.rs +++ b/src/application/services/contact_service.rs @@ -780,6 +780,22 @@ impl ContactUseCase for ContactService { Ok(ContactDto::from(contact)) } + async fn get_contact_by_uid( + &self, + address_book_id: &str, + uid: &str, + user_id: Uuid, + ) -> Result, 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?; + + let contact = self.contact_repository.get_contact_by_uid(&id, uid).await?; + Ok(contact.map(ContactDto::from)) + } + async fn list_contacts( &self, address_book_id: &str, diff --git a/src/infrastructure/adapters/calendar_storage_adapter.rs b/src/infrastructure/adapters/calendar_storage_adapter.rs index 05b84c08..e37c3ac5 100644 --- a/src/infrastructure/adapters/calendar_storage_adapter.rs +++ b/src/infrastructure/adapters/calendar_storage_adapter.rs @@ -402,6 +402,26 @@ impl CalendarStoragePort for CalendarStorageAdapter { Ok(CalendarEventDto::from(event)) } + async fn find_event_by_ical_uid( + &self, + calendar_id: &str, + ical_uid: &str, + ) -> Result, DomainError> { + let uuid = Uuid::parse_str(calendar_id).map_err(|_| { + DomainError::new( + ErrorKind::InvalidInput, + "Calendar", + "Invalid calendar ID format", + ) + })?; + + let event = self + .event_repository + .find_event_by_ical_uid(&uuid, ical_uid) + .await?; + Ok(event.map(CalendarEventDto::from)) + } + async fn list_events_by_calendar( &self, calendar_id: &str, diff --git a/src/infrastructure/adapters/contact_storage_adapter.rs b/src/infrastructure/adapters/contact_storage_adapter.rs index d629be2c..1f8a06d6 100644 --- a/src/infrastructure/adapters/contact_storage_adapter.rs +++ b/src/infrastructure/adapters/contact_storage_adapter.rs @@ -712,6 +712,24 @@ impl ContactUseCase for ContactStorageAdapter { Ok(ContactDto::from(contact)) } + async fn get_contact_by_uid( + &self, + address_book_id: &str, + uid: &str, + user_id: Uuid, + ) -> Result, DomainError> { + let uuid = Self::parse_uuid(address_book_id, "AddressBook")?; + + // Check read access + self.check_address_book_access(&uuid, user_id).await?; + + let contact = self + .contact_repository + .get_contact_by_uid(&uuid, uid) + .await?; + Ok(contact.map(ContactDto::from)) + } + async fn list_contacts( &self, address_book_id: &str, diff --git a/src/interfaces/api/handlers/caldav_handler.rs b/src/interfaces/api/handlers/caldav_handler.rs index 99179144..98fded62 100644 --- a/src/interfaces/api/handlers/caldav_handler.rs +++ b/src/interfaces/api/handlers/caldav_handler.rs @@ -608,12 +608,13 @@ async fn handle_put( let ical_uid = extract_uid_from_ical(&ical_data); + // Indexed single-row lookup — listing the whole calendar (every row + // with its ical_data) to find one UID made imports O(N²). let existing = if let Some(ref uid) = ical_uid { - let events = calendar_service - .list_events(calendar_id, None, None, user.id) + calendar_service + .get_event_by_ical_uid(calendar_id, uid, user.id) .await - .unwrap_or_default(); - events.into_iter().find(|e| e.ical_uid == *uid) + .unwrap_or_default() } else { None }; @@ -704,21 +705,17 @@ async fn handle_get( .body(Body::from(ical)) .unwrap()) } else { - // GET on individual event + // GET on individual event — indexed lookup by iCalendar UID. let event_file = parts[1]; let ical_uid = event_file.trim_end_matches(".ics"); - let events = calendar_service - .list_events(calendar_id, None, None, user.id) + let event = calendar_service + .get_event_by_ical_uid(calendar_id, ical_uid, user.id) .await - .map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?; - - let event = events - .iter() - .find(|e| e.ical_uid == ical_uid) + .map_err(|e| AppError::internal_error(format!("Failed to look up event: {}", e)))? .ok_or_else(|| AppError::not_found(format!("Event not found: {}", ical_uid)))?; - let ical = generate_event_ical(event); + let ical = generate_event_ical(&event); Ok(Response::builder() .status(StatusCode::OK) @@ -813,14 +810,11 @@ async fn handle_delete( let event_file = parts[1]; let ical_uid = event_file.trim_end_matches(".ics"); - let events = calendar_service - .list_events(calendar_id, None, None, user.id) + // Indexed lookup by iCalendar UID instead of listing the calendar. + let event = calendar_service + .get_event_by_ical_uid(calendar_id, ical_uid, user.id) .await - .map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?; - - let event = events - .iter() - .find(|e| e.ical_uid == ical_uid) + .map_err(|e| AppError::internal_error(format!("Failed to look up event: {}", e)))? .ok_or_else(|| AppError::not_found(format!("Event not found: {}", ical_uid)))?; calendar_service diff --git a/src/interfaces/api/handlers/carddav_handler.rs b/src/interfaces/api/handlers/carddav_handler.rs index 3a6d273e..5b58c1fd 100644 --- a/src/interfaces/api/handlers/carddav_handler.rs +++ b/src/interfaces/api/handlers/carddav_handler.rs @@ -295,19 +295,14 @@ async fn handle_propfind( .body(Body::from(response_body)) .unwrap()) } else { - // Individual contact .vcf + // Individual contact .vcf — indexed lookup by vCard UID. let contact_file = parts[1]; let contact_uid = contact_file.trim_end_matches(".vcf"); - // Look up by UID across all contacts in this address book - let contacts = contact_svc - .list_contacts(address_book_id, user.id) + let contact = contact_svc + .get_contact_by_uid(address_book_id, contact_uid, user.id) .await - .map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?; - - let contact = contacts - .iter() - .find(|c| c.uid == contact_uid) + .map_err(|e| AppError::internal_error(format!("Failed to look up contact: {}", e)))? .ok_or_else(|| { AppError::not_found(format!("Contact not found: {}", contact_uid)) })?; @@ -322,8 +317,8 @@ async fn handle_propfind( let mut response_body = Vec::new(); CardDavAdapter::generate_contacts_response( &mut response_body, - std::slice::from_ref(contact), - &[(contact.uid.clone(), contact_to_vcard(contact))], + std::slice::from_ref(&contact), + &[(contact.uid.clone(), contact_to_vcard(&contact))], &report, base_href, ) @@ -483,13 +478,13 @@ async fn handle_put( // Extract UID from vCard let vcard_uid = extract_uid_from_vcard(&vcard_data); - // Check if contact already exists + // Check if contact already exists — indexed single-row lookup + // (listing the whole address book made imports O(N²)). let existing = if let Some(ref uid) = vcard_uid { - let contacts = contact_svc - .list_contacts(address_book_id, user.id) + contact_svc + .get_contact_by_uid(address_book_id, uid, user.id) .await - .unwrap_or_default(); - contacts.into_iter().find(|c| c.uid == *uid) + .unwrap_or_default() } else { None }; @@ -579,21 +574,17 @@ async fn handle_get( .body(Body::from(vcf_data)) .unwrap()) } else { - // GET on individual contact + // GET on individual contact — indexed lookup by vCard UID. let contact_file = parts[1]; let contact_uid = contact_file.trim_end_matches(".vcf"); - let contacts = contact_svc - .list_contacts(address_book_id, user.id) + let contact = contact_svc + .get_contact_by_uid(address_book_id, contact_uid, user.id) .await - .map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?; - - let contact = contacts - .iter() - .find(|c| c.uid == contact_uid) + .map_err(|e| AppError::internal_error(format!("Failed to look up contact: {}", e)))? .ok_or_else(|| AppError::not_found(format!("Contact not found: {}", contact_uid)))?; - let vcard = contact_to_vcard(contact); + let vcard = contact_to_vcard(&contact); Ok(Response::builder() .status(StatusCode::OK) @@ -632,18 +623,14 @@ async fn handle_delete( AppError::internal_error(format!("Failed to delete address book: {}", e)) })?; } else { - // Delete contact + // Delete contact — indexed lookup by vCard UID. let contact_file = parts[1]; let contact_uid = contact_file.trim_end_matches(".vcf"); - let contacts = contact_svc - .list_contacts(address_book_id, user.id) + let contact = contact_svc + .get_contact_by_uid(address_book_id, contact_uid, user.id) .await - .map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?; - - let contact = contacts - .iter() - .find(|c| c.uid == contact_uid) + .map_err(|e| AppError::internal_error(format!("Failed to look up contact: {}", e)))? .ok_or_else(|| AppError::not_found(format!("Contact not found: {}", contact_uid)))?; contact_svc