diff --git a/crates/caldav/src/calendar/service.rs b/crates/caldav/src/calendar/service.rs index 5721206..a82c96a 100644 --- a/crates/caldav/src/calendar/service.rs +++ b/crates/caldav/src/calendar/service.rs @@ -59,7 +59,7 @@ impl ResourceService for CalendarResourc let calendar = self.cal_store.get_calendar(principal, cal_id).await?; Ok(CalendarResource { cal: calendar, - read_only: self.cal_store.is_read_only(), + read_only: self.cal_store.is_read_only(cal_id), }) } diff --git a/crates/caldav/src/calendar_set/mod.rs b/crates/caldav/src/calendar_set/mod.rs deleted file mode 100644 index 20cfb59..0000000 --- a/crates/caldav/src/calendar_set/mod.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::Error; -use rustical_dav::extensions::CommonPropertiesExtension; -use rustical_dav::privileges::UserPrivilegeSet; -use rustical_dav::resource::{PrincipalUri, Resource, ResourceName}; -use rustical_dav::xml::{Resourcetype, ResourcetypeInner}; -use rustical_store::auth::User; - -mod service; -pub use service::*; -mod prop; -pub use prop::*; - -#[derive(Clone)] -pub struct CalendarSetResource { - pub(crate) principal: String, - pub(crate) read_only: bool, - pub(crate) name: &'static str, -} - -impl ResourceName for CalendarSetResource { - fn get_name(&self) -> String { - self.name.to_owned() - } -} - -impl Resource for CalendarSetResource { - type Prop = PrincipalPropWrapper; - type Error = Error; - type Principal = User; - - const IS_COLLECTION: bool = true; - - fn get_resourcetype(&self) -> Resourcetype { - Resourcetype(&[ResourcetypeInner( - Some(rustical_dav::namespace::NS_DAV), - "collection", - )]) - } - - fn get_prop( - &self, - puri: &impl PrincipalUri, - user: &User, - prop: &PrincipalPropWrapperName, - ) -> Result { - Ok(match prop { - PrincipalPropWrapperName::Common(prop) => PrincipalPropWrapper::Common( - ::get_prop(self, puri, user, prop)?, - ), - }) - } - - fn get_owner(&self) -> Option<&str> { - Some(&self.principal) - } - - fn get_user_privileges(&self, user: &User) -> Result { - Ok(if self.read_only { - UserPrivilegeSet::owner_read(user.is_principal(&self.principal)) - } else { - UserPrivilegeSet::owner_only(user.is_principal(&self.principal)) - }) - } -} diff --git a/crates/caldav/src/calendar_set/prop.rs b/crates/caldav/src/calendar_set/prop.rs deleted file mode 100644 index 589c2a4..0000000 --- a/crates/caldav/src/calendar_set/prop.rs +++ /dev/null @@ -1,8 +0,0 @@ -use rustical_dav::extensions::CommonPropertiesProp; -use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize}; - -#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)] -#[xml(unit_variants_ident = "PrincipalPropWrapperName", untagged)] -pub enum PrincipalPropWrapper { - Common(CommonPropertiesProp), -} diff --git a/crates/caldav/src/calendar_set/service.rs b/crates/caldav/src/calendar_set/service.rs deleted file mode 100644 index 68d15da..0000000 --- a/crates/caldav/src/calendar_set/service.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::calendar::CalendarResourceService; -use crate::calendar::resource::CalendarResource; -use crate::calendar_set::CalendarSetResource; -use crate::{CalDavPrincipalUri, Error}; -use async_trait::async_trait; -use axum::Router; -use rustical_dav::resource::{AxumMethods, ResourceService}; -use rustical_store::auth::User; -use rustical_store::{CalendarStore, SubscriptionStore}; -use std::sync::Arc; - -pub struct CalendarSetResourceService { - name: &'static str, - cal_store: Arc, - sub_store: Arc, -} - -impl Clone for CalendarSetResourceService { - fn clone(&self) -> Self { - Self { - name: self.name, - cal_store: self.cal_store.clone(), - sub_store: self.sub_store.clone(), - } - } -} - -impl CalendarSetResourceService { - pub fn new(name: &'static str, cal_store: Arc, sub_store: Arc) -> Self { - Self { - name, - cal_store, - sub_store, - } - } -} - -#[async_trait] -impl ResourceService for CalendarSetResourceService { - type PathComponents = (String,); - type MemberType = CalendarResource; - type Resource = CalendarSetResource; - type Error = Error; - type Principal = User; - type PrincipalUri = CalDavPrincipalUri; - - const DAV_HEADER: &str = "1, 3, access-control, extended-mkcol, calendar-access"; - - async fn get_resource( - &self, - (principal,): &Self::PathComponents, - ) -> Result { - Ok(CalendarSetResource { - principal: principal.to_owned(), - read_only: self.cal_store.is_read_only(), - name: self.name, - }) - } - - async fn get_members( - &self, - (principal,): &Self::PathComponents, - ) -> Result, Self::Error> { - let calendars = self.cal_store.get_calendars(principal).await?; - Ok(calendars - .into_iter() - .map(|cal| CalendarResource { - cal, - read_only: self.cal_store.is_read_only(), - }) - .collect()) - } - - fn axum_router(self) -> axum::Router { - Router::new() - .nest( - "/{calendar_id}", - CalendarResourceService::new(self.cal_store.clone(), self.sub_store.clone()) - .axum_router(), - ) - .route_service("/", self.axum_service()) - } -} -impl AxumMethods for CalendarSetResourceService {} diff --git a/crates/caldav/src/lib.rs b/crates/caldav/src/lib.rs index 37f8113..0a15a17 100644 --- a/crates/caldav/src/lib.rs +++ b/crates/caldav/src/lib.rs @@ -7,12 +7,11 @@ use rustical_dav::resource::{PrincipalUri, ResourceService}; use rustical_dav::resources::RootResourceService; use rustical_store::auth::middleware::AuthenticationLayer; use rustical_store::auth::{AuthenticationProvider, User}; -use rustical_store::{AddressbookStore, CalendarStore, ContactBirthdayStore, SubscriptionStore}; +use rustical_store::{CalendarStore, SubscriptionStore}; use std::sync::Arc; pub mod calendar; pub mod calendar_object; -pub mod calendar_set; pub mod error; pub mod principal; // mod subscription; @@ -28,23 +27,15 @@ impl PrincipalUri for CalDavPrincipalUri { } } -pub fn caldav_router< - AP: AuthenticationProvider, - AS: AddressbookStore, - C: CalendarStore, - S: SubscriptionStore, ->( +pub fn caldav_router( prefix: &'static str, auth_provider: Arc, store: Arc, - addr_store: Arc, subscription_store: Arc, ) -> Router { - let birthday_store = Arc::new(ContactBirthdayStore::new(addr_store)); let principal_service = PrincipalResourceService { auth_provider: auth_provider.clone(), sub_store: subscription_store.clone(), - birthday_store: birthday_store.clone(), cal_store: store.clone(), }; diff --git a/crates/caldav/src/principal/mod.rs b/crates/caldav/src/principal/mod.rs index f6d8ff0..e0794e4 100644 --- a/crates/caldav/src/principal/mod.rs +++ b/crates/caldav/src/principal/mod.rs @@ -2,7 +2,7 @@ use crate::Error; use rustical_dav::extensions::CommonPropertiesExtension; use rustical_dav::privileges::UserPrivilegeSet; use rustical_dav::resource::{PrincipalUri, Resource, ResourceName}; -use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner}; +use rustical_dav::xml::{Resourcetype, ResourcetypeInner}; use rustical_store::auth::User; mod service; @@ -13,7 +13,6 @@ pub use prop::*; #[derive(Clone)] pub struct PrincipalResource { principal: User, - home_set: &'static [&'static str], } impl ResourceName for PrincipalResource { @@ -47,12 +46,7 @@ impl Resource for PrincipalResource { let home_set = CalendarHomeSet( user.memberships() .into_iter() - .map(|principal| puri.principal_uri(principal)) - .flat_map(|principal_url| { - self.home_set.iter().map(move |&home_name| { - HrefElement::new(format!("{}{}/", &principal_url, home_name)) - }) - }) + .map(|principal| puri.principal_uri(principal).into()) .collect(), ); diff --git a/crates/caldav/src/principal/service.rs b/crates/caldav/src/principal/service.rs index ee6099a..b24cbcf 100644 --- a/crates/caldav/src/principal/service.rs +++ b/crates/caldav/src/principal/service.rs @@ -1,4 +1,5 @@ -use crate::calendar_set::{CalendarSetResource, CalendarSetResourceService}; +use crate::calendar::CalendarResourceService; +use crate::calendar::resource::CalendarResource; use crate::principal::PrincipalResource; use crate::{CalDavPrincipalUri, Error}; use async_trait::async_trait; @@ -13,33 +14,30 @@ pub struct PrincipalResourceService< AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore, - BS: CalendarStore, > { pub(crate) auth_provider: Arc, pub(crate) sub_store: Arc, pub(crate) cal_store: Arc, - pub(crate) birthday_store: Arc, } -impl Clone - for PrincipalResourceService +impl Clone + for PrincipalResourceService { fn clone(&self) -> Self { Self { auth_provider: self.auth_provider.clone(), sub_store: self.sub_store.clone(), cal_store: self.cal_store.clone(), - birthday_store: self.birthday_store.clone(), } } } #[async_trait] -impl - ResourceService for PrincipalResourceService +impl ResourceService + for PrincipalResourceService { type PathComponents = (String,); - type MemberType = CalendarSetResource; + type MemberType = CalendarResource; type Resource = PrincipalResource; type Error = Error; type Principal = User; @@ -56,55 +54,36 @@ impl Result, Self::Error> { - Ok(vec![ - CalendarSetResource { - name: "calendar", - principal: principal.to_owned(), - read_only: false, - }, - CalendarSetResource { - name: "birthdays", - principal: principal.to_owned(), - read_only: true, - }, - ]) + let calendars = self.cal_store.get_calendars(principal).await?; + + Ok(calendars + .into_iter() + .map(|cal| CalendarResource { + read_only: self.cal_store.is_read_only(&cal.id), + cal, + }) + .collect()) } fn axum_router(self) -> axum::Router { Router::new() .nest( - "/calendar", - CalendarSetResourceService::new( - "calendar", - self.cal_store.clone(), - self.sub_store.clone(), - ) - .axum_router(), - ) - .nest( - "/birthdays", - CalendarSetResourceService::new( - "birthdays", - self.birthday_store.clone(), - self.sub_store.clone(), - ) - .axum_router(), + "/{calendar_id}", + CalendarResourceService::new(self.cal_store.clone(), self.sub_store.clone()) + .axum_router(), ) .route_service("/", self.axum_service()) } } -impl - AxumMethods for PrincipalResourceService +impl AxumMethods + for PrincipalResourceService { } diff --git a/crates/store/src/calendar_store.rs b/crates/store/src/calendar_store.rs index aabaf07..4bcfe08 100644 --- a/crates/store/src/calendar_store.rs +++ b/crates/store/src/calendar_store.rs @@ -80,5 +80,5 @@ pub trait CalendarStore: Send + Sync + 'static { object_id: &str, ) -> Result<(), Error>; - fn is_read_only(&self) -> bool; + fn is_read_only(&self, cal_id: &str) -> bool; } diff --git a/crates/store/src/combined_calendar_store.rs b/crates/store/src/combined_calendar_store.rs new file mode 100644 index 0000000..b016f27 --- /dev/null +++ b/crates/store/src/combined_calendar_store.rs @@ -0,0 +1,240 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use derive_more::Constructor; +use rustical_ical::CalendarObject; + +use crate::{ + Calendar, CalendarStore, Error, calendar_store::CalendarQuery, + contact_birthday_store::BIRTHDAYS_PREFIX, +}; + +#[derive(Debug, Constructor)] +pub struct CombinedCalendarStore { + cal_store: Arc, + birthday_store: Arc, +} + +impl Clone for CombinedCalendarStore { + fn clone(&self) -> Self { + Self { + cal_store: self.cal_store.clone(), + birthday_store: self.birthday_store.clone(), + } + } +} + +#[async_trait] +impl CalendarStore for CombinedCalendarStore { + #[inline] + async fn get_calendar(&self, principal: &str, id: &str) -> Result { + if id.starts_with(BIRTHDAYS_PREFIX) { + self.birthday_store.get_calendar(principal, id).await + } else { + self.cal_store.get_calendar(principal, id).await + } + } + + #[inline] + async fn update_calendar( + &self, + principal: String, + id: String, + calendar: Calendar, + ) -> Result<(), crate::Error> { + if id.starts_with(BIRTHDAYS_PREFIX) { + self.birthday_store + .update_calendar(principal, id, calendar) + .await + } else { + self.cal_store + .update_calendar(principal, id, calendar) + .await + } + } + + #[inline] + async fn insert_calendar(&self, calendar: Calendar) -> Result<(), Error> { + if calendar.id.starts_with(BIRTHDAYS_PREFIX) { + Err(Error::ReadOnly) + } else { + self.cal_store.insert_calendar(calendar).await + } + } + + #[inline] + async fn get_calendars(&self, principal: &str) -> Result, Error> { + Ok([ + self.cal_store.get_calendars(principal).await?, + self.birthday_store.get_calendars(principal).await?, + ] + .concat()) + } + + #[inline] + async fn delete_object( + &self, + principal: &str, + cal_id: &str, + object_id: &str, + use_trashbin: bool, + ) -> Result<(), Error> { + if cal_id.starts_with(BIRTHDAYS_PREFIX) { + Err(Error::ReadOnly) + } else { + self.birthday_store + .delete_object(principal, cal_id, object_id, use_trashbin) + .await + } + } + + #[inline] + async fn get_object( + &self, + principal: &str, + cal_id: &str, + object_id: &str, + ) -> Result { + if cal_id.starts_with(BIRTHDAYS_PREFIX) { + self.birthday_store + .get_object(principal, cal_id, object_id) + .await + } else { + self.cal_store + .get_object(principal, cal_id, object_id) + .await + } + } + + #[inline] + async fn sync_changes( + &self, + principal: &str, + cal_id: &str, + synctoken: i64, + ) -> Result<(Vec, Vec, i64), Error> { + if cal_id.starts_with(BIRTHDAYS_PREFIX) { + self.birthday_store + .sync_changes(principal, cal_id, synctoken) + .await + } else { + self.cal_store + .sync_changes(principal, cal_id, synctoken) + .await + } + } + + #[inline] + async fn get_objects( + &self, + principal: &str, + cal_id: &str, + ) -> Result, Error> { + if cal_id.starts_with(BIRTHDAYS_PREFIX) { + self.birthday_store.get_objects(principal, cal_id).await + } else { + self.cal_store.get_objects(principal, cal_id).await + } + } + + #[inline] + async fn calendar_query( + &self, + principal: &str, + cal_id: &str, + query: CalendarQuery, + ) -> Result, Error> { + if cal_id.starts_with(BIRTHDAYS_PREFIX) { + self.birthday_store + .calendar_query(principal, cal_id, query) + .await + } else { + self.cal_store + .calendar_query(principal, cal_id, query) + .await + } + } + + #[inline] + async fn restore_calendar(&self, principal: &str, name: &str) -> Result<(), Error> { + if name.starts_with(BIRTHDAYS_PREFIX) { + self.birthday_store.restore_calendar(principal, name).await + } else { + self.cal_store.restore_calendar(principal, name).await + } + } + + #[inline] + async fn delete_calendar( + &self, + principal: &str, + name: &str, + use_trashbin: bool, + ) -> Result<(), Error> { + if name.starts_with(BIRTHDAYS_PREFIX) { + self.birthday_store + .delete_calendar(principal, name, use_trashbin) + .await + } else { + self.cal_store + .delete_calendar(principal, name, use_trashbin) + .await + } + } + + #[inline] + async fn get_deleted_calendars(&self, principal: &str) -> Result, Error> { + Ok([ + self.birthday_store.get_deleted_calendars(principal).await?, + self.cal_store.get_deleted_calendars(principal).await?, + ] + .concat()) + } + + #[inline] + async fn restore_object( + &self, + principal: &str, + cal_id: &str, + object_id: &str, + ) -> Result<(), Error> { + if cal_id.starts_with(BIRTHDAYS_PREFIX) { + self.birthday_store + .restore_object(principal, cal_id, object_id) + .await + } else { + self.cal_store + .restore_object(principal, cal_id, object_id) + .await + } + } + + #[inline] + async fn put_object( + &self, + principal: String, + cal_id: String, + object: CalendarObject, + overwrite: bool, + ) -> Result<(), Error> { + if cal_id.starts_with(BIRTHDAYS_PREFIX) { + self.birthday_store + .put_object(principal, cal_id, object, overwrite) + .await + } else { + self.cal_store + .put_object(principal, cal_id, object, overwrite) + .await + } + } + + #[inline] + fn is_read_only(&self, cal_id: &str) -> bool { + if cal_id.starts_with(BIRTHDAYS_PREFIX) { + self.birthday_store.is_read_only(cal_id) + } else { + self.cal_store.is_read_only(cal_id) + } + } +} + diff --git a/crates/store/src/contact_birthday_store.rs b/crates/store/src/contact_birthday_store.rs index 7d7d881..3e187bc 100644 --- a/crates/store/src/contact_birthday_store.rs +++ b/crates/store/src/contact_birthday_store.rs @@ -5,13 +5,15 @@ use rustical_ical::{AddressObject, CalendarObject, CalendarObjectType}; use sha2::{Digest, Sha256}; use std::{collections::HashMap, sync::Arc}; +pub(crate) const BIRTHDAYS_PREFIX: &str = "_birthdays_"; + #[derive(Constructor, Clone)] pub struct ContactBirthdayStore(Arc); fn birthday_calendar(addressbook: Addressbook) -> Calendar { Calendar { principal: addressbook.principal, - id: addressbook.id, + id: format!("{}{}", BIRTHDAYS_PREFIX, addressbook.id), displayname: addressbook .displayname .map(|name| format!("{} birthdays", name)), @@ -33,9 +35,11 @@ fn birthday_calendar(addressbook: Addressbook) -> Calendar { } } +/// Objects are all prefixed with BIRTHDAYS_PREFIX #[async_trait] impl CalendarStore for ContactBirthdayStore { async fn get_calendar(&self, principal: &str, id: &str) -> Result { + let id = id.strip_prefix(BIRTHDAYS_PREFIX).ok_or(Error::NotFound)?; let addressbook = self.0.get_addressbook(principal, id, false).await?; Ok(birthday_calendar(addressbook)) } @@ -80,6 +84,9 @@ impl CalendarStore for ContactBirthdayStore { cal_id: &str, synctoken: i64, ) -> Result<(Vec, Vec, i64), Error> { + let cal_id = cal_id + .strip_prefix(BIRTHDAYS_PREFIX) + .ok_or(Error::NotFound)?; let (objects, deleted_objects, new_synctoken) = self.0.sync_changes(principal, cal_id, synctoken).await?; let objects: Result>, rustical_ical::Error> = objects @@ -96,6 +103,9 @@ impl CalendarStore for ContactBirthdayStore { principal: &str, cal_id: &str, ) -> Result, Error> { + let cal_id = cal_id + .strip_prefix(BIRTHDAYS_PREFIX) + .ok_or(Error::NotFound)?; let objects: Result>, rustical_ical::Error> = self.0 .get_objects(principal, cal_id) @@ -117,6 +127,9 @@ impl CalendarStore for ContactBirthdayStore { cal_id: &str, object_id: &str, ) -> Result { + let cal_id = cal_id + .strip_prefix(BIRTHDAYS_PREFIX) + .ok_or(Error::NotFound)?; let (addressobject_id, date_type) = object_id.rsplit_once("-").ok_or(Error::NotFound)?; self.0 .get_object(principal, cal_id, addressobject_id, false) @@ -155,7 +168,7 @@ impl CalendarStore for ContactBirthdayStore { Err(Error::ReadOnly) } - fn is_read_only(&self) -> bool { + fn is_read_only(&self, _cal_id: &str) -> bool { true } } diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index 9259fcf..bda62a8 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -5,6 +5,7 @@ pub mod error; pub use error::Error; pub mod auth; mod calendar; +mod combined_calendar_store; mod contact_birthday_store; mod secret; mod subscription_store; @@ -12,6 +13,7 @@ pub mod synctoken; pub use addressbook_store::AddressbookStore; pub use calendar_store::CalendarStore; +pub use combined_calendar_store::CombinedCalendarStore; pub use contact_birthday_store::ContactBirthdayStore; pub use secret::Secret; pub use subscription_store::*; diff --git a/crates/store_sqlite/src/calendar_store.rs b/crates/store_sqlite/src/calendar_store.rs index b5db422..aa2b0bf 100644 --- a/crates/store_sqlite/src/calendar_store.rs +++ b/crates/store_sqlite/src/calendar_store.rs @@ -671,7 +671,7 @@ impl CalendarStore for SqliteCalendarStore { Self::_sync_changes(&self.db, principal, cal_id, synctoken).await } - fn is_read_only(&self) -> bool { + fn is_read_only(&self, _cal_id: &str) -> bool { false } } diff --git a/src/app.rs b/src/app.rs index d0b9bd7..38990e3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,7 +2,7 @@ use crate::config::NextcloudLoginConfig; use axum::Router; use axum::body::Body; use axum::extract::Request; -use axum::response::{IntoResponse, Response}; +use axum::response::Response; use axum::routing::options; use headers::{HeaderMapExt, UserAgent}; use http::{HeaderValue, StatusCode}; @@ -12,7 +12,9 @@ use rustical_frontend::nextcloud_login::nextcloud_login_router; use rustical_frontend::{FrontendConfig, frontend_router}; use rustical_oidc::OidcConfig; use rustical_store::auth::AuthenticationProvider; -use rustical_store::{AddressbookStore, CalendarStore, SubscriptionStore}; +use rustical_store::{ + AddressbookStore, CalendarStore, CombinedCalendarStore, ContactBirthdayStore, SubscriptionStore, +}; use std::sync::Arc; use std::time::Duration; use tower_http::catch_panic::CatchPanicLayer; @@ -33,12 +35,16 @@ pub fn make_app( oidc_config: Option, nextcloud_login_config: NextcloudLoginConfig, ) -> Router<()> { + let combined_cal_store = Arc::new(CombinedCalendarStore::new( + cal_store.clone(), + ContactBirthdayStore::new(addr_store.clone()).into(), + )); + let mut router = Router::new() .merge(caldav_router( "/caldav", auth_provider.clone(), - cal_store.clone(), - addr_store.clone(), + combined_cal_store.clone(), subscription_store.clone(), )) .merge(carddav_router( @@ -71,7 +77,7 @@ pub fn make_app( router = router.merge(frontend_router( "/frontend", auth_provider.clone(), - cal_store.clone(), + combined_cal_store.clone(), addr_store.clone(), frontend_config, oidc_config,