feat(calendar,addressbook): remove old sharing code
This commit is contained in:
@@ -61,16 +61,3 @@ pub struct UpdateAddressBookDto {
|
|||||||
pub is_public: Option<bool>,
|
pub is_public: Option<bool>,
|
||||||
pub user_id: String, // Current user making the update
|
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,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -25,38 +25,11 @@ pub trait CalendarStoragePort: Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
owner_id: Uuid,
|
owner_id: Uuid,
|
||||||
) -> Result<Vec<CalendarDto>, DomainError>;
|
) -> Result<Vec<CalendarDto>, DomainError>;
|
||||||
async fn list_calendars_shared_with_user(
|
|
||||||
&self,
|
|
||||||
user_id: Uuid,
|
|
||||||
) -> Result<Vec<CalendarDto>, DomainError>;
|
|
||||||
async fn list_public_calendars(
|
async fn list_public_calendars(
|
||||||
&self,
|
&self,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
offset: i64,
|
offset: i64,
|
||||||
) -> Result<Vec<CalendarDto>, DomainError>;
|
) -> 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
|
// Calendar properties
|
||||||
async fn set_calendar_property(
|
async fn set_calendar_property(
|
||||||
&self,
|
&self,
|
||||||
@@ -146,33 +119,12 @@ pub trait CalendarUseCase: Send + Sync + 'static {
|
|||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
) -> Result<CalendarDto, DomainError>;
|
) -> Result<CalendarDto, DomainError>;
|
||||||
async fn list_my_calendars(&self, user_id: Uuid) -> Result<Vec<CalendarDto>, DomainError>;
|
async fn list_my_calendars(&self, user_id: Uuid) -> Result<Vec<CalendarDto>, DomainError>;
|
||||||
async fn list_shared_calendars(&self, user_id: Uuid) -> Result<Vec<CalendarDto>, DomainError>;
|
|
||||||
async fn list_public_calendars(
|
async fn list_public_calendars(
|
||||||
&self,
|
&self,
|
||||||
limit: Option<i64>,
|
limit: Option<i64>,
|
||||||
offset: Option<i64>,
|
offset: Option<i64>,
|
||||||
) -> Result<Vec<CalendarDto>, DomainError>;
|
) -> 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
|
// Event operations
|
||||||
async fn create_event(
|
async fn create_event(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::application::dtos::address_book_dto::{
|
use crate::application::dtos::address_book_dto::{
|
||||||
AddressBookDto, CreateAddressBookDto, ShareAddressBookDto, UnshareAddressBookDto,
|
AddressBookDto, CreateAddressBookDto, UpdateAddressBookDto,
|
||||||
UpdateAddressBookDto,
|
|
||||||
};
|
};
|
||||||
use crate::application::dtos::contact_dto::{
|
use crate::application::dtos::contact_dto::{
|
||||||
ContactDto, ContactGroupDto, CreateContactDto, CreateContactGroupDto, CreateContactVCardDto,
|
ContactDto, ContactGroupDto, CreateContactDto, CreateContactGroupDto, CreateContactVCardDto,
|
||||||
@@ -127,23 +126,6 @@ pub trait AddressBookUseCase: Send + Sync + 'static {
|
|||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<AddressBookDto>, DomainError>;
|
) -> Result<Vec<AddressBookDto>, DomainError>;
|
||||||
async fn list_public_address_books(&self) -> 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 {
|
pub trait ContactUseCase: Send + Sync + 'static {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
use serde_json::Value;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -446,9 +445,3 @@ pub trait StorageUsagePort: Send + Sync + 'static {
|
|||||||
additional_bytes: u64,
|
additional_bytes: u64,
|
||||||
) -> Result<(), DomainError>;
|
) -> 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)
|
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(
|
async fn list_public_calendars(
|
||||||
&self,
|
&self,
|
||||||
limit: Option<i64>,
|
limit: Option<i64>,
|
||||||
@@ -223,99 +214,6 @@ impl CalendarUseCase for CalendarService {
|
|||||||
.await
|
.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(
|
async fn create_event(
|
||||||
&self,
|
&self,
|
||||||
event: CreateEventDto,
|
event: CreateEventDto,
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ use std::sync::Arc;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::dtos::address_book_dto::{
|
use crate::application::dtos::address_book_dto::{
|
||||||
AddressBookDto, CreateAddressBookDto, ShareAddressBookDto, UnshareAddressBookDto,
|
AddressBookDto, CreateAddressBookDto, UpdateAddressBookDto,
|
||||||
UpdateAddressBookDto,
|
|
||||||
};
|
};
|
||||||
use crate::application::dtos::contact_dto::{
|
use crate::application::dtos::contact_dto::{
|
||||||
ContactDto, ContactGroupDto, CreateContactDto, CreateContactGroupDto, CreateContactVCardDto,
|
ContactDto, ContactGroupDto, CreateContactDto, CreateContactGroupDto, CreateContactVCardDto,
|
||||||
@@ -14,7 +13,6 @@ use crate::application::ports::authorization_ports::AuthorizationEngine;
|
|||||||
use crate::application::ports::carddav_ports::{
|
use crate::application::ports::carddav_ports::{
|
||||||
AddressBookUseCase, ContactStoragePort, ContactUseCase,
|
AddressBookUseCase, ContactStoragePort, ContactUseCase,
|
||||||
};
|
};
|
||||||
use crate::application::ports::storage_ports::StorageUseCase;
|
|
||||||
use crate::common::errors::DomainError;
|
use crate::common::errors::DomainError;
|
||||||
use crate::domain::entities::contact::{Address, AddressBook, Contact, ContactGroup, Email, Phone};
|
use crate::domain::entities::contact::{Address, AddressBook, Contact, ContactGroup, Email, Phone};
|
||||||
use crate::domain::services::authorization::{Permission, Resource, Role, Subject};
|
use crate::domain::services::authorization::{Permission, Resource, Role, Subject};
|
||||||
@@ -444,105 +442,6 @@ impl AddressBookUseCase for ContactService {
|
|||||||
.collect();
|
.collect();
|
||||||
Ok(dtos)
|
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 {
|
impl ContactUseCase for ContactService {
|
||||||
@@ -1171,385 +1070,3 @@ impl ContactUseCase for ContactService {
|
|||||||
Ok(vcards)
|
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
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,6 +6,15 @@ use crate::domain::entities::contact::AddressBook;
|
|||||||
|
|
||||||
pub type AddressBookRepositoryResult<T> = Result<T, DomainError>;
|
pub type AddressBookRepositoryResult<T> = Result<T, DomainError>;
|
||||||
|
|
||||||
|
/// 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 {
|
pub trait AddressBookRepository: Send + Sync + 'static {
|
||||||
async fn create_address_book(
|
async fn create_address_book(
|
||||||
&self,
|
&self,
|
||||||
@@ -20,28 +29,13 @@ pub trait AddressBookRepository: Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
id: &Uuid,
|
id: &Uuid,
|
||||||
) -> AddressBookRepositoryResult<Option<AddressBook>>;
|
) -> AddressBookRepositoryResult<Option<AddressBook>>;
|
||||||
|
/// 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(
|
async fn get_address_books_by_owner(
|
||||||
&self,
|
&self,
|
||||||
owner_id: Uuid,
|
owner_id: Uuid,
|
||||||
) -> AddressBookRepositoryResult<Vec<AddressBook>>;
|
) -> AddressBookRepositoryResult<Vec<AddressBook>>;
|
||||||
async fn get_shared_address_books(
|
|
||||||
&self,
|
|
||||||
user_id: Uuid,
|
|
||||||
) -> AddressBookRepositoryResult<Vec<AddressBook>>;
|
|
||||||
async fn get_public_address_books(&self) -> AddressBookRepositoryResult<Vec<AddressBook>>;
|
async fn get_public_address_books(&self) -> AddressBookRepositoryResult<Vec<AddressBook>>;
|
||||||
async fn share_address_book(
|
|
||||||
&self,
|
|
||||||
address_book_id: &Uuid,
|
|
||||||
user_id: 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<Vec<(String, bool)>>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,15 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
pub type CalendarRepositoryResult<T> = Result<T, DomainError>;
|
pub type CalendarRepositoryResult<T> = Result<T, DomainError>;
|
||||||
|
|
||||||
/// 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 {
|
pub trait CalendarRepository: Send + Sync + 'static {
|
||||||
/// Creates a new calendar
|
/// Creates a new calendar
|
||||||
async fn create_calendar(&self, calendar: Calendar) -> CalendarRepositoryResult<Calendar>;
|
async fn create_calendar(&self, calendar: Calendar) -> CalendarRepositoryResult<Calendar>;
|
||||||
@@ -18,7 +26,11 @@ pub trait CalendarRepository: Send + Sync + 'static {
|
|||||||
/// Finds a calendar by its ID
|
/// Finds a calendar by its ID
|
||||||
async fn find_calendar_by_id(&self, id: &Uuid) -> CalendarRepositoryResult<Calendar>;
|
async fn find_calendar_by_id(&self, id: &Uuid) -> CalendarRepositoryResult<Calendar>;
|
||||||
|
|
||||||
/// 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(
|
async fn list_calendars_by_owner(
|
||||||
&self,
|
&self,
|
||||||
owner_id: Uuid,
|
owner_id: Uuid,
|
||||||
@@ -31,12 +43,6 @@ pub trait CalendarRepository: Send + Sync + 'static {
|
|||||||
owner_id: Uuid,
|
owner_id: Uuid,
|
||||||
) -> CalendarRepositoryResult<Calendar>;
|
) -> CalendarRepositoryResult<Calendar>;
|
||||||
|
|
||||||
/// Lists calendars shared with a specific user
|
|
||||||
async fn list_calendars_shared_with_user(
|
|
||||||
&self,
|
|
||||||
user_id: Uuid,
|
|
||||||
) -> CalendarRepositoryResult<Vec<Calendar>>;
|
|
||||||
|
|
||||||
/// List public calendars
|
/// List public calendars
|
||||||
async fn list_public_calendars(
|
async fn list_public_calendars(
|
||||||
&self,
|
&self,
|
||||||
@@ -44,13 +50,6 @@ pub trait CalendarRepository: Send + Sync + 'static {
|
|||||||
offset: i64,
|
offset: i64,
|
||||||
) -> CalendarRepositoryResult<Vec<Calendar>>;
|
) -> CalendarRepositoryResult<Vec<Calendar>>;
|
||||||
|
|
||||||
/// Checks if a user has access to a calendar
|
|
||||||
async fn user_has_calendar_access(
|
|
||||||
&self,
|
|
||||||
calendar_id: &Uuid,
|
|
||||||
user_id: Uuid,
|
|
||||||
) -> CalendarRepositoryResult<bool>;
|
|
||||||
|
|
||||||
/// Gets a custom property for a calendar
|
/// Gets a custom property for a calendar
|
||||||
async fn get_calendar_property(
|
async fn get_calendar_property(
|
||||||
&self,
|
&self,
|
||||||
@@ -78,25 +77,4 @@ pub trait CalendarRepository: Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
calendar_id: &Uuid,
|
calendar_id: &Uuid,
|
||||||
) -> CalendarRepositoryResult<std::collections::HashMap<String, String>>;
|
) -> CalendarRepositoryResult<std::collections::HashMap<String, String>>;
|
||||||
|
|
||||||
/// 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<Vec<(String, String)>>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,17 +126,6 @@ impl CalendarStoragePort for CalendarStorageAdapter {
|
|||||||
Ok(calendars.into_iter().map(CalendarDto::from).collect())
|
Ok(calendars.into_iter().map(CalendarDto::from).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_calendars_shared_with_user(
|
|
||||||
&self,
|
|
||||||
user_id: Uuid,
|
|
||||||
) -> Result<Vec<CalendarDto>, 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(
|
async fn list_public_calendars(
|
||||||
&self,
|
&self,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
@@ -149,78 +138,6 @@ impl CalendarStoragePort for CalendarStorageAdapter {
|
|||||||
Ok(calendars.into_iter().map(CalendarDto::from).collect())
|
Ok(calendars.into_iter().map(CalendarDto::from).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_calendar_access(
|
|
||||||
&self,
|
|
||||||
calendar_id: &str,
|
|
||||||
user_id: Uuid,
|
|
||||||
) -> Result<bool, DomainError> {
|
|
||||||
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<Vec<(String, String)>, 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
|
// Calendar properties
|
||||||
|
|
||||||
async fn set_calendar_property(
|
async fn set_calendar_property(
|
||||||
|
|||||||
@@ -184,44 +184,6 @@ impl AddressBookRepository for AddressBookPgRepository {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_shared_address_books(
|
|
||||||
&self,
|
|
||||||
user_id: Uuid,
|
|
||||||
) -> AddressBookRepositoryResult<Vec<AddressBook>> {
|
|
||||||
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<Vec<AddressBook>> {
|
async fn get_public_address_books(&self) -> AddressBookRepositoryResult<Vec<AddressBook>> {
|
||||||
let rows = sqlx::query(
|
let rows = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -256,79 +218,4 @@ impl AddressBookRepository for AddressBookPgRepository {
|
|||||||
|
|
||||||
Ok(result)
|
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<Vec<(String, bool)>> {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,44 +216,6 @@ impl CalendarRepository for CalendarPgRepository {
|
|||||||
Ok(calendar)
|
Ok(calendar)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_calendars_shared_with_user(
|
|
||||||
&self,
|
|
||||||
user_id: Uuid,
|
|
||||||
) -> CalendarRepositoryResult<Vec<Calendar>> {
|
|
||||||
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(
|
async fn list_public_calendars(
|
||||||
&self,
|
&self,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
@@ -296,112 +258,6 @@ impl CalendarRepository for CalendarPgRepository {
|
|||||||
Ok(calendars)
|
Ok(calendars)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn user_has_calendar_access(
|
|
||||||
&self,
|
|
||||||
calendar_id: &Uuid,
|
|
||||||
user_id: Uuid,
|
|
||||||
) -> CalendarRepositoryResult<bool> {
|
|
||||||
// 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::<bool, _>("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<Vec<(String, String)>> {
|
|
||||||
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(
|
async fn get_calendar_property(
|
||||||
&self,
|
&self,
|
||||||
calendar_id: &Uuid,
|
calendar_id: &Uuid,
|
||||||
|
|||||||
Reference in New Issue
Block a user