feat(calendar,addressbook): remove old sharing code

This commit is contained in:
Edouard Vanbelle
2026-07-06 08:41:17 +02:00
parent a2ad7757c3
commit 0e341758af
11 changed files with 29 additions and 1068 deletions
-13
View File
@@ -61,16 +61,3 @@ pub struct UpdateAddressBookDto {
pub is_public: Option<bool>,
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,
}
-48
View File
@@ -25,38 +25,11 @@ pub trait CalendarStoragePort: Send + Sync + 'static {
&self,
owner_id: Uuid,
) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_calendars_shared_with_user(
&self,
user_id: Uuid,
) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_public_calendars(
&self,
limit: i64,
offset: i64,
) -> Result<Vec<CalendarDto>, DomainError>;
async fn check_calendar_access(
&self,
calendar_id: &str,
user_id: Uuid,
) -> Result<bool, DomainError>;
// 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<Vec<(String, String)>, DomainError>;
// Calendar properties
async fn set_calendar_property(
&self,
@@ -146,33 +119,12 @@ pub trait CalendarUseCase: Send + Sync + 'static {
user_id: Uuid,
) -> Result<CalendarDto, DomainError>;
async fn list_my_calendars(&self, user_id: Uuid) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_shared_calendars(&self, user_id: Uuid) -> Result<Vec<CalendarDto>, DomainError>;
async fn list_public_calendars(
&self,
limit: Option<i64>,
offset: Option<i64>,
) -> Result<Vec<CalendarDto>, 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<Vec<(String, String)>, DomainError>;
// Event operations
async fn create_event(
&self,
+1 -19
View File
@@ -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<Vec<AddressBookDto>, DomainError>;
async fn list_public_address_books(&self) -> Result<Vec<AddressBookDto>, 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<Vec<(String, bool)>, DomainError>;
}
pub trait ContactUseCase: Send + Sync + 'static {
-7
View File
@@ -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<Value, DomainError>;
}
@@ -200,15 +200,6 @@ impl CalendarUseCase for CalendarService {
Ok(out)
}
async fn list_shared_calendars(&self, user_id: Uuid) -> Result<Vec<CalendarDto>, 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<i64>,
@@ -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<Vec<(String, String)>, 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,
+1 -484
View File
@@ -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<Vec<(String, bool)>, 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<serde_json::Value, DomainError> {
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
))),
}
}
}