From 0e341758af496d7eeee3ade1172ccde822d0bb2a Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Mon, 6 Jul 2026 08:41:17 +0200 Subject: [PATCH] feat(calendar,addressbook): remove old sharing code --- src/application/dtos/address_book_dto.rs | 13 - src/application/ports/calendar_ports.rs | 48 -- src/application/ports/carddav_ports.rs | 20 +- src/application/ports/storage_ports.rs | 7 - src/application/services/calendar_service.rs | 102 ---- src/application/services/contact_service.rs | 485 +----------------- .../repositories/address_book_repository.rs | 32 +- .../repositories/calendar_repository.rs | 50 +- .../adapters/calendar_storage_adapter.rs | 83 --- .../pg/address_book_pg_repository.rs | 113 ---- .../repositories/pg/calendar_pg_repository.rs | 144 ------ 11 files changed, 29 insertions(+), 1068 deletions(-) diff --git a/src/application/dtos/address_book_dto.rs b/src/application/dtos/address_book_dto.rs index 7523f655..853a1b00 100644 --- a/src/application/dtos/address_book_dto.rs +++ b/src/application/dtos/address_book_dto.rs @@ -61,16 +61,3 @@ pub struct UpdateAddressBookDto { pub is_public: Option, pub user_id: String, // Current user making the update } - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ShareAddressBookDto { - pub address_book_id: String, - pub user_id: String, - pub can_write: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UnshareAddressBookDto { - pub address_book_id: String, - pub user_id: String, -} diff --git a/src/application/ports/calendar_ports.rs b/src/application/ports/calendar_ports.rs index 28728a88..332da7ec 100644 --- a/src/application/ports/calendar_ports.rs +++ b/src/application/ports/calendar_ports.rs @@ -25,38 +25,11 @@ pub trait CalendarStoragePort: Send + Sync + 'static { &self, owner_id: Uuid, ) -> Result, DomainError>; - async fn list_calendars_shared_with_user( - &self, - user_id: Uuid, - ) -> Result, DomainError>; async fn list_public_calendars( &self, limit: i64, offset: i64, ) -> Result, DomainError>; - async fn check_calendar_access( - &self, - calendar_id: &str, - user_id: Uuid, - ) -> Result; - - // Calendar sharing - async fn share_calendar( - &self, - calendar_id: &str, - user_id: Uuid, - access_level: &str, - ) -> Result<(), DomainError>; - async fn remove_calendar_sharing( - &self, - calendar_id: &str, - user_id: Uuid, - ) -> Result<(), DomainError>; - async fn get_calendar_shares( - &self, - calendar_id: &str, - ) -> Result, DomainError>; - // Calendar properties async fn set_calendar_property( &self, @@ -146,33 +119,12 @@ pub trait CalendarUseCase: Send + Sync + 'static { user_id: Uuid, ) -> Result; async fn list_my_calendars(&self, user_id: Uuid) -> Result, DomainError>; - async fn list_shared_calendars(&self, user_id: Uuid) -> Result, DomainError>; async fn list_public_calendars( &self, limit: Option, offset: Option, ) -> Result, DomainError>; - // Calendar sharing - async fn share_calendar( - &self, - calendar_id: &str, - target_user_id: Uuid, - access_level: &str, - caller_user_id: Uuid, - ) -> Result<(), DomainError>; - async fn remove_calendar_sharing( - &self, - calendar_id: &str, - target_user_id: Uuid, - caller_user_id: Uuid, - ) -> Result<(), DomainError>; - async fn get_calendar_shares( - &self, - calendar_id: &str, - user_id: Uuid, - ) -> Result, DomainError>; - // Event operations async fn create_event( &self, diff --git a/src/application/ports/carddav_ports.rs b/src/application/ports/carddav_ports.rs index dd589a7a..3dd420a8 100644 --- a/src/application/ports/carddav_ports.rs +++ b/src/application/ports/carddav_ports.rs @@ -1,6 +1,5 @@ use crate::application::dtos::address_book_dto::{ - AddressBookDto, CreateAddressBookDto, ShareAddressBookDto, UnshareAddressBookDto, - UpdateAddressBookDto, + AddressBookDto, CreateAddressBookDto, UpdateAddressBookDto, }; use crate::application::dtos::contact_dto::{ ContactDto, ContactGroupDto, CreateContactDto, CreateContactGroupDto, CreateContactVCardDto, @@ -127,23 +126,6 @@ pub trait AddressBookUseCase: Send + Sync + 'static { user_id: Uuid, ) -> Result, DomainError>; async fn list_public_address_books(&self) -> Result, DomainError>; - - // Address Book sharing - async fn share_address_book( - &self, - dto: ShareAddressBookDto, - user_id: Uuid, - ) -> Result<(), DomainError>; - async fn unshare_address_book( - &self, - dto: UnshareAddressBookDto, - user_id: Uuid, - ) -> Result<(), DomainError>; - async fn get_address_book_shares( - &self, - address_book_id: &str, - user_id: Uuid, - ) -> Result, DomainError>; } pub trait ContactUseCase: Send + Sync + 'static { diff --git a/src/application/ports/storage_ports.rs b/src/application/ports/storage_ports.rs index 5e4bf4e8..1157f649 100644 --- a/src/application/ports/storage_ports.rs +++ b/src/application/ports/storage_ports.rs @@ -1,6 +1,5 @@ use bytes::Bytes; use futures::Stream; -use serde_json::Value; use std::path::PathBuf; use std::pin::Pin; use uuid::Uuid; @@ -446,9 +445,3 @@ pub trait StorageUsagePort: Send + Sync + 'static { additional_bytes: u64, ) -> Result<(), DomainError>; } - -/// Generic storage service interface for calendar and contact services -pub trait StorageUseCase: Send + Sync + 'static { - /// Handle a request with the specified action and parameters - async fn handle_request(&self, action: &str, params: Value) -> Result; -} diff --git a/src/application/services/calendar_service.rs b/src/application/services/calendar_service.rs index b539325d..fcd7cab0 100644 --- a/src/application/services/calendar_service.rs +++ b/src/application/services/calendar_service.rs @@ -200,15 +200,6 @@ impl CalendarUseCase for CalendarService { Ok(out) } - async fn list_shared_calendars(&self, user_id: Uuid) -> Result, DomainError> { - // Kept for API compatibility (some frontends may still call - // this). Post-Round-3 the concept of "shared vs owned" is a - // client-side filter — the server hands back everything the - // caller has Read on. Callers wanting the strict "shared - // with me, not owned by me" subset filter by `owner_id != caller`. - self.list_my_calendars(user_id).await - } - async fn list_public_calendars( &self, limit: Option, @@ -223,99 +214,6 @@ impl CalendarUseCase for CalendarService { .await } - async fn share_calendar( - &self, - calendar_id: &str, - target_user_id: Uuid, - access_level: &str, - caller_user_id: Uuid, - ) -> Result<(), DomainError> { - let uuid = self - .require_calendar_perm(calendar_id, caller_user_id, Permission::Share) - .await?; - // Map the legacy string-shaped `access_level` onto the ReBAC - // Role enum. `owner` transfers ownership — the storage side - // used to allow this; keep semantics identical here so any - // pending client keeps working. `viewer` / `editor` mirror - // the pre-Round-3 `read` / `write` behaviour. - let role = match access_level { - "read" => Role::Viewer, - "write" => Role::Editor, - "owner" => Role::Owner, - other => { - return Err(DomainError::new( - ErrorKind::InvalidInput, - "Calendar", - format!( - "Invalid access level: {}. Valid values are: read, write, owner", - other - ), - )); - } - }; - self.authz - .set_role( - caller_user_id, - Subject::User(target_user_id), - role, - Resource::Calendar(uuid), - None, - ) - .await?; - Ok(()) - } - - async fn remove_calendar_sharing( - &self, - calendar_id: &str, - target_user_id: Uuid, - caller_user_id: Uuid, - ) -> Result<(), DomainError> { - let uuid = self - .require_calendar_perm(calendar_id, caller_user_id, Permission::Share) - .await?; - self.authz - .clear_role(Subject::User(target_user_id), Resource::Calendar(uuid)) - .await - } - - async fn get_calendar_shares( - &self, - calendar_id: &str, - user_id: Uuid, - ) -> Result, DomainError> { - let uuid = self - .require_calendar_perm(calendar_id, user_id, Permission::Manage) - .await?; - // Translate the engine's `Grant` view into the legacy - // `(user_id, access_level_string)` tuple the handler still - // consumes. `Role → &str` uses the SQL discriminator so a - // client that expects `"read"` / `"write"` / `"owner"` - // keeps working through the transition. - let grants = self - .authz - .list_grants_on_resource(Resource::Calendar(uuid)) - .await?; - Ok(grants - .into_iter() - .filter_map(|g| { - // The legacy shape lists user subjects only. Group / - // token subjects on a calendar didn't exist pre-Round-3; - // the new listing endpoint added in Phase 4 will - // surface them properly. - let Subject::User(user_id) = g.subject else { - return None; - }; - let access = match g.role { - Role::Owner => "owner", - Role::Editor | Role::Contributor => "write", - _ => "read", - }; - Some((user_id.to_string(), access.to_string())) - }) - .collect()) - } - async fn create_event( &self, event: CreateEventDto, diff --git a/src/application/services/contact_service.rs b/src/application/services/contact_service.rs index a14dc07e..45470c4f 100644 --- a/src/application/services/contact_service.rs +++ b/src/application/services/contact_service.rs @@ -3,8 +3,7 @@ use std::sync::Arc; use uuid::Uuid; use crate::application::dtos::address_book_dto::{ - AddressBookDto, CreateAddressBookDto, ShareAddressBookDto, UnshareAddressBookDto, - UpdateAddressBookDto, + AddressBookDto, CreateAddressBookDto, UpdateAddressBookDto, }; use crate::application::dtos::contact_dto::{ ContactDto, ContactGroupDto, CreateContactDto, CreateContactGroupDto, CreateContactVCardDto, @@ -14,7 +13,6 @@ use crate::application::ports::authorization_ports::AuthorizationEngine; use crate::application::ports::carddav_ports::{ AddressBookUseCase, ContactStoragePort, ContactUseCase, }; -use crate::application::ports::storage_ports::StorageUseCase; use crate::common::errors::DomainError; use crate::domain::entities::contact::{Address, AddressBook, Contact, ContactGroup, Email, Phone}; use crate::domain::services::authorization::{Permission, Resource, Role, Subject}; @@ -444,105 +442,6 @@ impl AddressBookUseCase for ContactService { .collect(); Ok(dtos) } - - async fn share_address_book( - &self, - dto: ShareAddressBookDto, - user_id: Uuid, - ) -> Result<(), DomainError> { - let id = Uuid::parse_str(&dto.address_book_id) - .map_err(|_| DomainError::validation_error("Invalid address book ID format"))?; - - // AuthZ: caller must have Share on the address book. Only - // Owner grants include Share today; matches the pre-Round-3 - // owner-only rule. - self.require_address_book_perm(&id, &user_id, Permission::Share) - .await?; - - // Don't allow sharing with yourself. `authz.set_role` would - // silently no-op via `ON CONFLICT UPDATE` but the earlier - // service returned a validation error to help the client - // catch a UX bug — preserve that behaviour. - 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"))?; - let role = if dto.can_write { - Role::Editor - } else { - Role::Viewer - }; - self.authz - .set_role( - user_id, - Subject::User(target_user_id), - role, - Resource::AddressBook(id), - None, - ) - .await?; - Ok(()) - } - - async fn unshare_address_book( - &self, - dto: UnshareAddressBookDto, - user_id: Uuid, - ) -> Result<(), DomainError> { - let id = Uuid::parse_str(&dto.address_book_id) - .map_err(|_| DomainError::validation_error("Invalid address book ID format"))?; - - // AuthZ: caller must have Share on the address book (same - // permission that gates share creation gates removal too). - self.require_address_book_perm(&id, &user_id, Permission::Share) - .await?; - - let target_user_id = Uuid::parse_str(&dto.user_id) - .map_err(|_| DomainError::validation_error("Invalid target user ID format"))?; - self.authz - .clear_role(Subject::User(target_user_id), Resource::AddressBook(id)) - .await?; - Ok(()) - } - - async fn get_address_book_shares( - &self, - address_book_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"))?; - - // AuthZ: caller must have Manage on the address book. Only - // Owner grants include Manage — matches the pre-Round-3 - // owner-only rule for the shares listing. - self.require_address_book_perm(&id, &user_id, Permission::Manage) - .await?; - - let grants = self - .authz - .list_grants_on_resource(Resource::AddressBook(id)) - .await?; - // Translate the engine's `Grant` view into the legacy - // `(user_id_str, can_write_bool)` tuple the handler still - // consumes. Non-user subjects (groups / tokens) are dropped - // from this listing — a phase-4 endpoint will surface them - // properly. - Ok(grants - .into_iter() - .filter_map(|g| { - let Subject::User(uid) = g.subject else { - return None; - }; - let can_write = matches!(g.role, Role::Editor | Role::Contributor | Role::Owner); - Some((uid.to_string(), can_write)) - }) - .collect()) - } } impl ContactUseCase for ContactService { @@ -1171,385 +1070,3 @@ impl ContactUseCase for ContactService { Ok(vcards) } } - -impl StorageUseCase for ContactService { - async fn handle_request( - &self, - action: &str, - params: serde_json::Value, - ) -> Result { - match action { - // Address Book operations - "create_address_book" => { - let dto: CreateAddressBookDto = - serde_json::from_value(params.clone()).map_err(|e| { - DomainError::validation_error(format!("Invalid parameters: {}", e)) - })?; - - let result = self.create_address_book(dto).await?; - Ok(serde_json::to_value(result).unwrap()) - } - "update_address_book" => { - let address_book_id = params["address_book_id"].as_str().ok_or_else(|| { - DomainError::validation_error("Missing address_book_id parameter") - })?; - - let update: UpdateAddressBookDto = - serde_json::from_value(params.clone()).map_err(|e| { - DomainError::validation_error(format!("Invalid parameters: {}", e)) - })?; - - let result = self.update_address_book(address_book_id, update).await?; - Ok(serde_json::to_value(result).unwrap()) - } - "delete_address_book" => { - let address_book_id = params["address_book_id"].as_str().ok_or_else(|| { - DomainError::validation_error("Missing address_book_id parameter") - })?; - - 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) - } - "get_address_book" => { - let address_book_id = params["address_book_id"].as_str().ok_or_else(|| { - DomainError::validation_error("Missing address_book_id parameter") - })?; - - 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()) - } - "list_user_address_books" => { - 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()) - } - "list_public_address_books" => { - let result = self.list_public_address_books().await?; - Ok(serde_json::to_value(result).unwrap()) - } - "share_address_book" => { - let dto: ShareAddressBookDto = - serde_json::from_value(params.clone()).map_err(|e| { - DomainError::validation_error(format!("Invalid parameters: {}", e)) - })?; - - 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) - } - "unshare_address_book" => { - let dto: UnshareAddressBookDto = - serde_json::from_value(params.clone()).map_err(|e| { - DomainError::validation_error(format!("Invalid parameters: {}", e)) - })?; - - 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) - } - "get_address_book_shares" => { - let address_book_id = params["address_book_id"].as_str().ok_or_else(|| { - DomainError::validation_error("Missing address_book_id parameter") - })?; - - 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) - .await?; - Ok(serde_json::to_value(result).unwrap()) - } - - // Contact operations - "create_contact" => { - let dto: CreateContactDto = - serde_json::from_value(params.clone()).map_err(|e| { - DomainError::validation_error(format!("Invalid parameters: {}", e)) - })?; - - let result = self.create_contact(dto).await?; - Ok(serde_json::to_value(result).unwrap()) - } - "create_contact_from_vcard" => { - let dto: CreateContactVCardDto = - serde_json::from_value(params.clone()).map_err(|e| { - DomainError::validation_error(format!("Invalid parameters: {}", e)) - })?; - - let result = self.create_contact_from_vcard(dto).await?; - Ok(serde_json::to_value(result).unwrap()) - } - "update_contact" => { - let contact_id = params["contact_id"] - .as_str() - .ok_or_else(|| DomainError::validation_error("Missing contact_id parameter"))?; - - let update: UpdateContactDto = - serde_json::from_value(params.clone()).map_err(|e| { - DomainError::validation_error(format!("Invalid parameters: {}", e)) - })?; - - let result = self.update_contact(contact_id, update).await?; - Ok(serde_json::to_value(result).unwrap()) - } - "delete_contact" => { - let contact_id = params["contact_id"] - .as_str() - .ok_or_else(|| DomainError::validation_error("Missing contact_id parameter"))?; - - 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) - } - "get_contact" => { - let contact_id = params["contact_id"] - .as_str() - .ok_or_else(|| DomainError::validation_error("Missing contact_id parameter"))?; - - 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()) - } - "list_contacts" => { - let address_book_id = params["address_book_id"].as_str().ok_or_else(|| { - DomainError::validation_error("Missing address_book_id parameter") - })?; - - 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, None, None, user_id) - .await?; - Ok(serde_json::to_value(result).unwrap()) - } - "search_contacts" => { - let address_book_id = params["address_book_id"].as_str().ok_or_else(|| { - DomainError::validation_error("Missing address_book_id parameter") - })?; - - let query = params["query"] - .as_str() - .ok_or_else(|| DomainError::validation_error("Missing query parameter"))?; - - 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) - .await?; - Ok(serde_json::to_value(result).unwrap()) - } - - // Group operations - "create_group" => { - let dto: CreateContactGroupDto = - serde_json::from_value(params.clone()).map_err(|e| { - DomainError::validation_error(format!("Invalid parameters: {}", e)) - })?; - - let result = self.create_group(dto).await?; - Ok(serde_json::to_value(result).unwrap()) - } - "update_group" => { - let group_id = params["group_id"] - .as_str() - .ok_or_else(|| DomainError::validation_error("Missing group_id parameter"))?; - - let update: UpdateContactGroupDto = serde_json::from_value(params.clone()) - .map_err(|e| { - DomainError::validation_error(format!("Invalid parameters: {}", e)) - })?; - - let result = self.update_group(group_id, update).await?; - Ok(serde_json::to_value(result).unwrap()) - } - "delete_group" => { - let group_id = params["group_id"] - .as_str() - .ok_or_else(|| DomainError::validation_error("Missing group_id parameter"))?; - - 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) - } - "get_group" => { - let group_id = params["group_id"] - .as_str() - .ok_or_else(|| DomainError::validation_error("Missing group_id parameter"))?; - - 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()) - } - "list_groups" => { - let address_book_id = params["address_book_id"].as_str().ok_or_else(|| { - DomainError::validation_error("Missing address_book_id parameter") - })?; - - 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()) - } - - // Group membership operations - "add_contact_to_group" => { - let dto: GroupMembershipDto = - serde_json::from_value(params.clone()).map_err(|e| { - DomainError::validation_error(format!("Invalid parameters: {}", e)) - })?; - - 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) - } - "remove_contact_from_group" => { - let dto: GroupMembershipDto = - serde_json::from_value(params.clone()).map_err(|e| { - DomainError::validation_error(format!("Invalid parameters: {}", e)) - })?; - - 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) - } - "list_contacts_in_group" => { - let group_id = params["group_id"] - .as_str() - .ok_or_else(|| DomainError::validation_error("Missing group_id parameter"))?; - - 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()) - } - "list_groups_for_contact" => { - let contact_id = params["contact_id"] - .as_str() - .ok_or_else(|| DomainError::validation_error("Missing contact_id parameter"))?; - - 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()) - } - - // vCard operations - "get_contact_vcard" => { - let contact_id = params["contact_id"] - .as_str() - .ok_or_else(|| DomainError::validation_error("Missing contact_id parameter"))?; - - 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()) - } - "get_contacts_as_vcards" => { - let address_book_id = params["address_book_id"].as_str().ok_or_else(|| { - DomainError::validation_error("Missing address_book_id parameter") - })?; - - 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) - .await?; - Ok(serde_json::to_value(result).unwrap()) - } - - _ => Err(DomainError::validation_error(format!( - "Unknown action: {}", - action - ))), - } - } -} diff --git a/src/domain/repositories/address_book_repository.rs b/src/domain/repositories/address_book_repository.rs index 7adbf6d2..f9841c11 100644 --- a/src/domain/repositories/address_book_repository.rs +++ b/src/domain/repositories/address_book_repository.rs @@ -6,6 +6,15 @@ use crate::domain::entities::contact::AddressBook; pub type AddressBookRepositoryResult = Result; +/// Repository interface for AddressBook entity operations. +/// +/// Post-Round-3, access-control state lives in `storage.role_grants`. +/// The pre-Round-3 methods that read/wrote `carddav.address_book_shares` +/// (`get_shared_address_books`, `share_address_book`, +/// `unshare_address_book`, `get_address_book_shares`) have been removed +/// from this trait. The `carddav.address_book_shares` table still +/// exists for one-release rollback safety; a follow-up migration drops +/// it. pub trait AddressBookRepository: Send + Sync + 'static { async fn create_address_book( &self, @@ -20,28 +29,13 @@ pub trait AddressBookRepository: Send + Sync + 'static { &self, id: &Uuid, ) -> AddressBookRepositoryResult>; + /// Direct owner enumeration — same semantics as the calendar + /// counterpart. The service layer prefers + /// `authz.list_incoming_grants`, but internal maintenance paths + /// keep the owner-only lookup available. async fn get_address_books_by_owner( &self, owner_id: Uuid, ) -> AddressBookRepositoryResult>; - async fn get_shared_address_books( - &self, - user_id: Uuid, - ) -> AddressBookRepositoryResult>; async fn get_public_address_books(&self) -> AddressBookRepositoryResult>; - async fn share_address_book( - &self, - address_book_id: &Uuid, - user_id: Uuid, - can_write: bool, - ) -> AddressBookRepositoryResult<()>; - async fn unshare_address_book( - &self, - address_book_id: &Uuid, - user_id: Uuid, - ) -> AddressBookRepositoryResult<()>; - async fn get_address_book_shares( - &self, - address_book_id: &Uuid, - ) -> AddressBookRepositoryResult>; } diff --git a/src/domain/repositories/calendar_repository.rs b/src/domain/repositories/calendar_repository.rs index 2ba3ead8..1216353e 100644 --- a/src/domain/repositories/calendar_repository.rs +++ b/src/domain/repositories/calendar_repository.rs @@ -4,7 +4,15 @@ use uuid::Uuid; pub type CalendarRepositoryResult = Result; -/// Repository interface for Calendar entity operations +/// Repository interface for Calendar entity operations. +/// +/// Post-Round-3, access-control state lives in `storage.role_grants` — +/// the pre-Round-3 methods that read/wrote `caldav.calendar_shares` +/// (`list_calendars_shared_with_user`, `user_has_calendar_access`, +/// `share_calendar`, `remove_calendar_sharing`, `get_calendar_shares`) +/// have been removed from this trait. The `caldav.calendar_shares` table +/// still exists for one-release rollback safety; a follow-up migration +/// drops it. pub trait CalendarRepository: Send + Sync + 'static { /// Creates a new calendar async fn create_calendar(&self, calendar: Calendar) -> CalendarRepositoryResult; @@ -18,7 +26,11 @@ pub trait CalendarRepository: Send + Sync + 'static { /// Finds a calendar by its ID async fn find_calendar_by_id(&self, id: &Uuid) -> CalendarRepositoryResult; - /// Lists all calendars for a specific user + /// Lists all calendars owned by a specific user. Post-Round-3 the + /// service layer prefers `authz.list_incoming_grants` (surfaces + /// owned + shared in one union), but this direct lookup remains + /// available for internal maintenance / migration paths that need + /// owner-only enumeration without going through the engine. async fn list_calendars_by_owner( &self, owner_id: Uuid, @@ -31,12 +43,6 @@ pub trait CalendarRepository: Send + Sync + 'static { owner_id: Uuid, ) -> CalendarRepositoryResult; - /// Lists calendars shared with a specific user - async fn list_calendars_shared_with_user( - &self, - user_id: Uuid, - ) -> CalendarRepositoryResult>; - /// List public calendars async fn list_public_calendars( &self, @@ -44,13 +50,6 @@ pub trait CalendarRepository: Send + Sync + 'static { offset: i64, ) -> CalendarRepositoryResult>; - /// Checks if a user has access to a calendar - async fn user_has_calendar_access( - &self, - calendar_id: &Uuid, - user_id: Uuid, - ) -> CalendarRepositoryResult; - /// Gets a custom property for a calendar async fn get_calendar_property( &self, @@ -78,25 +77,4 @@ pub trait CalendarRepository: Send + Sync + 'static { &self, calendar_id: &Uuid, ) -> CalendarRepositoryResult>; - - /// Share calendar with another user - async fn share_calendar( - &self, - calendar_id: &Uuid, - user_id: Uuid, - access_level: &str, - ) -> CalendarRepositoryResult<()>; - - /// Remove calendar sharing for a user - async fn remove_calendar_sharing( - &self, - calendar_id: &Uuid, - user_id: Uuid, - ) -> CalendarRepositoryResult<()>; - - /// Get calendar sharing information (who has access to this calendar) - async fn get_calendar_shares( - &self, - calendar_id: &Uuid, - ) -> CalendarRepositoryResult>; } diff --git a/src/infrastructure/adapters/calendar_storage_adapter.rs b/src/infrastructure/adapters/calendar_storage_adapter.rs index 346ba487..c8a45941 100644 --- a/src/infrastructure/adapters/calendar_storage_adapter.rs +++ b/src/infrastructure/adapters/calendar_storage_adapter.rs @@ -126,17 +126,6 @@ impl CalendarStoragePort for CalendarStorageAdapter { Ok(calendars.into_iter().map(CalendarDto::from).collect()) } - async fn list_calendars_shared_with_user( - &self, - user_id: Uuid, - ) -> Result, DomainError> { - let calendars = self - .calendar_repository - .list_calendars_shared_with_user(user_id) - .await?; - Ok(calendars.into_iter().map(CalendarDto::from).collect()) - } - async fn list_public_calendars( &self, limit: i64, @@ -149,78 +138,6 @@ impl CalendarStoragePort for CalendarStorageAdapter { Ok(calendars.into_iter().map(CalendarDto::from).collect()) } - async fn check_calendar_access( - &self, - calendar_id: &str, - user_id: Uuid, - ) -> Result { - let uuid = Uuid::parse_str(calendar_id).map_err(|_| { - DomainError::new( - ErrorKind::InvalidInput, - "Calendar", - "Invalid calendar ID format", - ) - })?; - - self.calendar_repository - .user_has_calendar_access(&uuid, user_id) - .await - } - - // Calendar sharing - - async fn share_calendar( - &self, - calendar_id: &str, - user_id: Uuid, - access_level: &str, - ) -> Result<(), DomainError> { - let uuid = Uuid::parse_str(calendar_id).map_err(|_| { - DomainError::new( - ErrorKind::InvalidInput, - "Calendar", - "Invalid calendar ID format", - ) - })?; - - self.calendar_repository - .share_calendar(&uuid, user_id, access_level) - .await - } - - async fn remove_calendar_sharing( - &self, - calendar_id: &str, - user_id: Uuid, - ) -> Result<(), DomainError> { - let uuid = Uuid::parse_str(calendar_id).map_err(|_| { - DomainError::new( - ErrorKind::InvalidInput, - "Calendar", - "Invalid calendar ID format", - ) - })?; - - self.calendar_repository - .remove_calendar_sharing(&uuid, user_id) - .await - } - - async fn get_calendar_shares( - &self, - calendar_id: &str, - ) -> Result, DomainError> { - let uuid = Uuid::parse_str(calendar_id).map_err(|_| { - DomainError::new( - ErrorKind::InvalidInput, - "Calendar", - "Invalid calendar ID format", - ) - })?; - - self.calendar_repository.get_calendar_shares(&uuid).await - } - // Calendar properties async fn set_calendar_property( diff --git a/src/infrastructure/repositories/pg/address_book_pg_repository.rs b/src/infrastructure/repositories/pg/address_book_pg_repository.rs index 0d2191f7..8ea91ca4 100644 --- a/src/infrastructure/repositories/pg/address_book_pg_repository.rs +++ b/src/infrastructure/repositories/pg/address_book_pg_repository.rs @@ -184,44 +184,6 @@ impl AddressBookRepository for AddressBookPgRepository { Ok(result) } - async fn get_shared_address_books( - &self, - user_id: Uuid, - ) -> AddressBookRepositoryResult> { - let rows = sqlx::query( - r#" - SELECT a.id, a.name, a.owner_id, a.description, a.color, a.is_public, a.created_at, a.updated_at - FROM carddav.address_books a - INNER JOIN carddav.address_book_shares s ON a.id = s.address_book_id - WHERE s.user_id = $1 - ORDER BY a.name - "# - ) - .bind(user_id) - .fetch_all(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to get shared address books: {}", e)))?; - - let result = rows - .into_iter() - .map(|row| { - let owner_id: Uuid = row.get("owner_id"); - AddressBook::from_raw( - row.get("id"), - row.get("name"), - owner_id.to_string(), - row.get("description"), - row.get("color"), - row.get("is_public"), - row.get("created_at"), - row.get("updated_at"), - ) - }) - .collect(); - - Ok(result) - } - async fn get_public_address_books(&self) -> AddressBookRepositoryResult> { let rows = sqlx::query( r#" @@ -256,79 +218,4 @@ impl AddressBookRepository for AddressBookPgRepository { Ok(result) } - - async fn share_address_book( - &self, - address_book_id: &Uuid, - user_id: Uuid, - can_write: bool, - ) -> AddressBookRepositoryResult<()> { - sqlx::query( - r#" - INSERT INTO carddav.address_book_shares (address_book_id, user_id, can_write) - VALUES ($1, $2, $3) - ON CONFLICT (address_book_id, user_id) DO UPDATE SET can_write = $3 - "#, - ) - .bind(address_book_id) - .bind(user_id) - .bind(can_write) - .execute(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to share address book: {}", e)))?; - - Ok(()) - } - - async fn unshare_address_book( - &self, - address_book_id: &Uuid, - user_id: Uuid, - ) -> AddressBookRepositoryResult<()> { - sqlx::query( - r#" - DELETE FROM carddav.address_book_shares - WHERE address_book_id = $1 AND user_id = $2 - "#, - ) - .bind(address_book_id) - .bind(user_id) - .execute(&*self.pool) - .await - .map_err(|e| { - DomainError::database_error(format!("Failed to unshare address book: {}", e)) - })?; - - Ok(()) - } - - async fn get_address_book_shares( - &self, - address_book_id: &Uuid, - ) -> AddressBookRepositoryResult> { - let rows = sqlx::query( - r#" - SELECT user_id, can_write - FROM carddav.address_book_shares - WHERE address_book_id = $1 - ORDER BY user_id - "#, - ) - .bind(address_book_id) - .fetch_all(&*self.pool) - .await - .map_err(|e| { - DomainError::database_error(format!("Failed to get address book shares: {}", e)) - })?; - - let result = rows - .into_iter() - .map(|row| { - let user_id: Uuid = row.get("user_id"); - (user_id.to_string(), row.get("can_write")) - }) - .collect(); - - Ok(result) - } } diff --git a/src/infrastructure/repositories/pg/calendar_pg_repository.rs b/src/infrastructure/repositories/pg/calendar_pg_repository.rs index 10bb3cc3..8eae0ce1 100644 --- a/src/infrastructure/repositories/pg/calendar_pg_repository.rs +++ b/src/infrastructure/repositories/pg/calendar_pg_repository.rs @@ -216,44 +216,6 @@ impl CalendarRepository for CalendarPgRepository { Ok(calendar) } - async fn list_calendars_shared_with_user( - &self, - user_id: Uuid, - ) -> CalendarRepositoryResult> { - let rows = sqlx::query( - r#" - SELECT c.id, c.name, c.owner_id, c.description, c.color, c.is_public, c.created_at, c.updated_at - FROM caldav.calendars c - INNER JOIN caldav.calendar_shares s ON c.id = s.calendar_id - WHERE s.user_id = $1 - ORDER BY c.name - "# - ) - .bind(user_id) - .fetch_all(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to get shared calendars: {}", e)))?; - - let mut calendars = Vec::new(); - for row in rows { - let calendar = Calendar::with_id( - row.get("id"), - row.get("name"), - row.get("owner_id"), - row.get("description"), - row.get("color"), - row.get("created_at"), - row.get("updated_at"), - ) - .map_err(|e| { - DomainError::database_error(format!("Failed to create calendar object: {}", e)) - })?; - calendars.push(calendar); - } - - Ok(calendars) - } - async fn list_public_calendars( &self, limit: i64, @@ -296,112 +258,6 @@ impl CalendarRepository for CalendarPgRepository { Ok(calendars) } - async fn user_has_calendar_access( - &self, - calendar_id: &Uuid, - user_id: Uuid, - ) -> CalendarRepositoryResult { - // Check if the user is the owner of the calendar or has a share - let row = sqlx::query( - r#" - SELECT EXISTS ( - SELECT 1 FROM caldav.calendars c - WHERE c.id = $1 AND (c.owner_id = $2 OR c.is_public = true) - UNION - SELECT 1 FROM caldav.calendar_shares s - WHERE s.calendar_id = $1 AND s.user_id = $2 - ) as has_access - "#, - ) - .bind(calendar_id) - .bind(user_id) - .fetch_one(&*self.pool) - .await - .map_err(|e| { - DomainError::database_error(format!("Failed to check calendar access: {}", e)) - })?; - - Ok(row.get::("has_access")) - } - - async fn share_calendar( - &self, - calendar_id: &Uuid, - user_id: Uuid, - access_level: &str, - ) -> CalendarRepositoryResult<()> { - // Validate access level - if !["read", "write", "owner"].contains(&access_level) { - return Err(DomainError::validation_error(format!( - "Invalid access level: '{}'. Must be 'read', 'write', or 'owner'", - access_level - ))); - } - - sqlx::query( - r#" - INSERT INTO caldav.calendar_shares (calendar_id, user_id, access_level) - VALUES ($1, $2, $3) - ON CONFLICT (calendar_id, user_id) DO UPDATE SET access_level = $3 - "#, - ) - .bind(calendar_id) - .bind(user_id) - .bind(access_level) - .execute(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to share calendar: {}", e)))?; - - Ok(()) - } - - async fn remove_calendar_sharing( - &self, - calendar_id: &Uuid, - user_id: Uuid, - ) -> CalendarRepositoryResult<()> { - sqlx::query( - r#" - DELETE FROM caldav.calendar_shares - WHERE calendar_id = $1 AND user_id = $2 - "#, - ) - .bind(calendar_id) - .bind(user_id) - .execute(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to unshare calendar: {}", e)))?; - - Ok(()) - } - - async fn get_calendar_shares( - &self, - calendar_id: &Uuid, - ) -> CalendarRepositoryResult> { - let rows = sqlx::query( - r#" - SELECT user_id, access_level - FROM caldav.calendar_shares - WHERE calendar_id = $1 - ORDER BY user_id - "#, - ) - .bind(calendar_id) - .fetch_all(&*self.pool) - .await - .map_err(|e| { - DomainError::database_error(format!("Failed to get calendar shares: {}", e)) - })?; - - let mut shares = Vec::new(); - for row in rows { - shares.push((row.get("user_id"), row.get("access_level"))); - } - - Ok(shares) - } - async fn get_calendar_property( &self, calendar_id: &Uuid,