mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 21:42:34 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d639b18005 | ||
|
|
6046439fc7 | ||
|
|
f9de8a4687 | ||
|
|
8dfb47b28f | ||
|
|
eb720ded99 | ||
|
|
89ef7b2ced | ||
|
|
6e0129130e | ||
|
|
c646986c56 | ||
|
|
503cbe3699 | ||
|
|
79c66a0b46 | ||
|
|
e5687c6e43 | ||
|
|
79b67a17c3 | ||
|
|
7d18faff69 | ||
|
|
753f8e90d3 |
4
.github/workflows/docker-publish.yml
vendored
4
.github/workflows/docker-publish.yml
vendored
@@ -41,12 +41,10 @@ jobs:
|
|||||||
# https://github.com/docker/metadata-action
|
# https://github.com/docker/metadata-action
|
||||||
- name: Extract Docker metadata
|
- name: Extract Docker metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
# As long as we don't have releases everything on the main branch shall be tagged as latest
|
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=ref,event=pr
|
type=ref,event=pr
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "SELECT *\n FROM calendars\n WHERE (principal, id) = (?, ?)",
|
"query": "SELECT *\n FROM calendars\n WHERE (principal, id) = (?, ?)\n AND ((deleted_at IS NULL) OR ?) ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 2
|
"Right": 3
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [
|
||||||
false,
|
false,
|
||||||
@@ -100,5 +100,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "9f930775043a6d4571a8ffd5a981cadf7c51f3f11a189f8461505abec31076e6"
|
"hash": "bb2fa030f2e7c7afdb38c5c54cb31de5293be332d86cf643977d479999542553"
|
||||||
}
|
}
|
||||||
22
Cargo.lock
generated
22
Cargo.lock
generated
@@ -2736,7 +2736,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical"
|
name = "rustical"
|
||||||
version = "0.3.5"
|
version = "0.4.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
@@ -2779,7 +2779,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_caldav"
|
name = "rustical_caldav"
|
||||||
version = "0.3.5"
|
version = "0.4.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -2814,7 +2814,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_carddav"
|
name = "rustical_carddav"
|
||||||
version = "0.3.5"
|
version = "0.4.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -2846,7 +2846,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_dav"
|
name = "rustical_dav"
|
||||||
version = "0.3.5"
|
version = "0.4.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -2871,7 +2871,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_dav_push"
|
name = "rustical_dav_push"
|
||||||
version = "0.3.5"
|
version = "0.4.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -2897,7 +2897,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_frontend"
|
name = "rustical_frontend"
|
||||||
version = "0.3.5"
|
version = "0.4.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"askama",
|
"askama",
|
||||||
"askama_web",
|
"askama_web",
|
||||||
@@ -2930,7 +2930,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_ical"
|
name = "rustical_ical"
|
||||||
version = "0.3.5"
|
version = "0.4.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -2948,7 +2948,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_oidc"
|
name = "rustical_oidc"
|
||||||
version = "0.3.5"
|
version = "0.4.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -2963,7 +2963,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_store"
|
name = "rustical_store"
|
||||||
version = "0.3.5"
|
version = "0.4.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -2997,7 +2997,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_store_sqlite"
|
name = "rustical_store_sqlite"
|
||||||
version = "0.3.5"
|
version = "0.4.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -3017,7 +3017,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustical_xml"
|
name = "rustical_xml"
|
||||||
version = "0.3.5"
|
version = "0.4.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
members = ["crates/*"]
|
members = ["crates/*"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.3.5"
|
version = "0.4.3"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "A CalDAV server"
|
description = "A CalDAV server"
|
||||||
repository = "https://github.com/lennart-k/rustical"
|
repository = "https://github.com/lennart-k/rustical"
|
||||||
|
|||||||
@@ -24,12 +24,16 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
|
|||||||
return Err(crate::Error::Unauthorized);
|
return Err(crate::Error::Unauthorized);
|
||||||
}
|
}
|
||||||
|
|
||||||
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?;
|
let calendar = cal_store
|
||||||
|
.get_calendar(&principal, &calendar_id, true)
|
||||||
|
.await?;
|
||||||
if !user.is_principal(&calendar.principal) {
|
if !user.is_principal(&calendar.principal) {
|
||||||
return Err(crate::Error::Unauthorized);
|
return Err(crate::Error::Unauthorized);
|
||||||
}
|
}
|
||||||
|
|
||||||
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?;
|
let calendar = cal_store
|
||||||
|
.get_calendar(&principal, &calendar_id, true)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let mut timezones = HashMap::new();
|
let mut timezones = HashMap::new();
|
||||||
let objects = cal_store.get_objects(&principal, &calendar_id).await?;
|
let objects = cal_store.get_objects(&principal, &calendar_id).await?;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ pub async fn route_post<C: CalendarStore, S: SubscriptionStore>(
|
|||||||
|
|
||||||
let calendar = resource_service
|
let calendar = resource_service
|
||||||
.cal_store
|
.cal_store
|
||||||
.get_calendar(&principal, &cal_id)
|
.get_calendar(&principal, &cal_id, false)
|
||||||
.await?;
|
.await?;
|
||||||
let calendar_resource = CalendarResource {
|
let calendar_resource = CalendarResource {
|
||||||
cal: calendar,
|
cal: calendar,
|
||||||
|
|||||||
@@ -292,7 +292,12 @@ impl Resource for CalendarResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
|
fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
|
||||||
if self.cal.subscription_url.is_some() || self.read_only {
|
if self.cal.subscription_url.is_some() {
|
||||||
|
return Ok(UserPrivilegeSet::owner_write_properties(
|
||||||
|
user.is_principal(&self.cal.principal),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.read_only {
|
||||||
return Ok(UserPrivilegeSet::owner_read(
|
return Ok(UserPrivilegeSet::owner_read(
|
||||||
user.is_principal(&self.cal.principal),
|
user.is_principal(&self.cal.principal),
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -56,8 +56,12 @@ impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourc
|
|||||||
async fn get_resource(
|
async fn get_resource(
|
||||||
&self,
|
&self,
|
||||||
(principal, cal_id): &Self::PathComponents,
|
(principal, cal_id): &Self::PathComponents,
|
||||||
|
show_deleted: bool,
|
||||||
) -> Result<Self::Resource, Error> {
|
) -> Result<Self::Resource, Error> {
|
||||||
let calendar = self.cal_store.get_calendar(principal, cal_id).await?;
|
let calendar = self
|
||||||
|
.cal_store
|
||||||
|
.get_calendar(principal, cal_id, show_deleted)
|
||||||
|
.await?;
|
||||||
Ok(CalendarResource {
|
Ok(CalendarResource {
|
||||||
cal: calendar,
|
cal: calendar,
|
||||||
read_only: self.cal_store.is_read_only(cal_id),
|
read_only: self.cal_store.is_read_only(cal_id),
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ pub async fn get_event<C: CalendarStore>(
|
|||||||
return Err(crate::Error::Unauthorized);
|
return Err(crate::Error::Unauthorized);
|
||||||
}
|
}
|
||||||
|
|
||||||
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?;
|
let calendar = cal_store
|
||||||
|
.get_calendar(&principal, &calendar_id, false)
|
||||||
|
.await?;
|
||||||
if !user.is_principal(&calendar.principal) {
|
if !user.is_principal(&calendar.principal) {
|
||||||
return Err(crate::Error::Unauthorized);
|
return Err(crate::Error::Unauthorized);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,10 +58,11 @@ impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
|
|||||||
calendar_id,
|
calendar_id,
|
||||||
object_id,
|
object_id,
|
||||||
}: &Self::PathComponents,
|
}: &Self::PathComponents,
|
||||||
|
show_deleted: bool,
|
||||||
) -> Result<Self::Resource, Self::Error> {
|
) -> Result<Self::Resource, Self::Error> {
|
||||||
let object = self
|
let object = self
|
||||||
.cal_store
|
.cal_store
|
||||||
.get_object(principal, calendar_id, object_id, false)
|
.get_object(principal, calendar_id, object_id, show_deleted)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(CalendarObjectResource {
|
Ok(CalendarObjectResource {
|
||||||
object,
|
object,
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore> Resour
|
|||||||
async fn get_resource(
|
async fn get_resource(
|
||||||
&self,
|
&self,
|
||||||
(principal,): &Self::PathComponents,
|
(principal,): &Self::PathComponents,
|
||||||
|
_show_deleted: bool,
|
||||||
) -> Result<Self::Resource, Self::Error> {
|
) -> Result<Self::Resource, Self::Error> {
|
||||||
let user = self
|
let user = self
|
||||||
.auth_provider
|
.auth_provider
|
||||||
|
|||||||
@@ -49,10 +49,11 @@ impl<AS: AddressbookStore> ResourceService for AddressObjectResourceService<AS>
|
|||||||
addressbook_id,
|
addressbook_id,
|
||||||
object_id,
|
object_id,
|
||||||
}: &Self::PathComponents,
|
}: &Self::PathComponents,
|
||||||
|
show_deleted: bool,
|
||||||
) -> Result<Self::Resource, Self::Error> {
|
) -> Result<Self::Resource, Self::Error> {
|
||||||
let object = self
|
let object = self
|
||||||
.addr_store
|
.addr_store
|
||||||
.get_object(principal, addressbook_id, object_id, false)
|
.get_object(principal, addressbook_id, object_id, show_deleted)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(AddressObjectResource {
|
Ok(AddressObjectResource {
|
||||||
object,
|
object,
|
||||||
|
|||||||
@@ -59,10 +59,11 @@ impl<AS: AddressbookStore, S: SubscriptionStore> ResourceService
|
|||||||
async fn get_resource(
|
async fn get_resource(
|
||||||
&self,
|
&self,
|
||||||
(principal, addressbook_id): &Self::PathComponents,
|
(principal, addressbook_id): &Self::PathComponents,
|
||||||
|
show_deleted: bool,
|
||||||
) -> Result<Self::Resource, Error> {
|
) -> Result<Self::Resource, Error> {
|
||||||
let addressbook = self
|
let addressbook = self
|
||||||
.addr_store
|
.addr_store
|
||||||
.get_addressbook(principal, addressbook_id, false)
|
.get_addressbook(principal, addressbook_id, show_deleted)
|
||||||
.await
|
.await
|
||||||
.map_err(|_e| Error::NotFound)?;
|
.map_err(|_e| Error::NotFound)?;
|
||||||
Ok(addressbook.into())
|
Ok(addressbook.into())
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ impl<A: AddressbookStore, AP: AuthenticationProvider, S: SubscriptionStore> Reso
|
|||||||
async fn get_resource(
|
async fn get_resource(
|
||||||
&self,
|
&self,
|
||||||
(principal,): &Self::PathComponents,
|
(principal,): &Self::PathComponents,
|
||||||
|
_show_deleted: bool,
|
||||||
) -> Result<Self::Resource, Self::Error> {
|
) -> Result<Self::Resource, Self::Error> {
|
||||||
let user = self
|
let user = self
|
||||||
.auth_provider
|
.auth_provider
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use quick_xml::name::Namespace;
|
|||||||
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc3744
|
||||||
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, Eq, Hash, PartialEq)]
|
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, Eq, Hash, PartialEq)]
|
||||||
pub enum UserPrivilege {
|
pub enum UserPrivilege {
|
||||||
Read,
|
Read,
|
||||||
@@ -47,6 +48,12 @@ pub struct UserPrivilegeSet {
|
|||||||
|
|
||||||
impl UserPrivilegeSet {
|
impl UserPrivilegeSet {
|
||||||
pub fn has(&self, privilege: &UserPrivilege) -> bool {
|
pub fn has(&self, privilege: &UserPrivilege) -> bool {
|
||||||
|
if (privilege == &UserPrivilege::WriteProperties
|
||||||
|
|| privilege == &UserPrivilege::WriteContent)
|
||||||
|
&& self.privileges.contains(&UserPrivilege::Write)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
self.privileges.contains(privilege) || self.privileges.contains(&UserPrivilege::All)
|
self.privileges.contains(privilege) || self.privileges.contains(&UserPrivilege::All)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +79,15 @@ impl UserPrivilegeSet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn owner_write_properties(is_owner: bool) -> Self {
|
||||||
|
// Content is read-only but we can write properties
|
||||||
|
if is_owner {
|
||||||
|
Self::write_properties()
|
||||||
|
} else {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read_only() -> Self {
|
pub fn read_only() -> Self {
|
||||||
Self {
|
Self {
|
||||||
privileges: HashSet::from([
|
privileges: HashSet::from([
|
||||||
@@ -81,6 +97,17 @@ impl UserPrivilegeSet {
|
|||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn write_properties() -> Self {
|
||||||
|
Self {
|
||||||
|
privileges: HashSet::from([
|
||||||
|
UserPrivilege::Read,
|
||||||
|
UserPrivilege::WriteProperties,
|
||||||
|
UserPrivilege::ReadAcl,
|
||||||
|
UserPrivilege::ReadCurrentUserPrivilegeSet,
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const N: usize> From<[UserPrivilege; N]> for UserPrivilegeSet {
|
impl<const N: usize> From<[UserPrivilege; N]> for UserPrivilegeSet {
|
||||||
|
|||||||
@@ -45,10 +45,11 @@ pub async fn route_delete<R: ResourceService>(
|
|||||||
if_match: Option<IfMatch>,
|
if_match: Option<IfMatch>,
|
||||||
if_none_match: Option<IfNoneMatch>,
|
if_none_match: Option<IfNoneMatch>,
|
||||||
) -> Result<(), R::Error> {
|
) -> Result<(), R::Error> {
|
||||||
let resource = resource_service.get_resource(path_components).await?;
|
let resource = resource_service.get_resource(path_components, true).await?;
|
||||||
|
|
||||||
|
// Kind of a bodge since we don't get unbind from the parent
|
||||||
let privileges = resource.get_user_privileges(principal)?;
|
let privileges = resource.get_user_privileges(principal)?;
|
||||||
if !privileges.has(&UserPrivilege::Write) {
|
if !privileges.has(&UserPrivilege::WriteProperties) {
|
||||||
return Err(Error::Unauthorized.into());
|
return Err(Error::Unauthorized.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ pub(crate) async fn route_propfind<R: ResourceService>(
|
|||||||
resource_service: &R,
|
resource_service: &R,
|
||||||
puri: &impl PrincipalUri,
|
puri: &impl PrincipalUri,
|
||||||
) -> Result<RSMultistatus<R>, R::Error> {
|
) -> Result<RSMultistatus<R>, R::Error> {
|
||||||
let resource = resource_service.get_resource(path_components).await?;
|
let resource = resource_service
|
||||||
|
.get_resource(path_components, false)
|
||||||
|
.await?;
|
||||||
let privileges = resource.get_user_privileges(principal)?;
|
let privileges = resource.get_user_privileges(principal)?;
|
||||||
if !privileges.has(&UserPrivilege::Read) {
|
if !privileges.has(&UserPrivilege::Read) {
|
||||||
return Err(Error::Unauthorized.into());
|
return Err(Error::Unauthorized.into());
|
||||||
|
|||||||
@@ -85,7 +85,9 @@ pub(crate) async fn route_proppatch<R: ResourceService>(
|
|||||||
operations,
|
operations,
|
||||||
) = XmlDocument::parse_str(body).map_err(Error::XmlError)?;
|
) = XmlDocument::parse_str(body).map_err(Error::XmlError)?;
|
||||||
|
|
||||||
let mut resource = resource_service.get_resource(path_components).await?;
|
let mut resource = resource_service
|
||||||
|
.get_resource(path_components, false)
|
||||||
|
.await?;
|
||||||
let privileges = resource.get_user_privileges(principal)?;
|
let privileges = resource.get_user_privileges(principal)?;
|
||||||
if !privileges.has(&UserPrivilege::Write) {
|
if !privileges.has(&UserPrivilege::Write) {
|
||||||
return Err(Error::Unauthorized.into());
|
return Err(Error::Unauthorized.into());
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ pub trait ResourceService: Clone + Sized + Send + Sync + AxumMethods + 'static {
|
|||||||
|
|
||||||
async fn get_resource(
|
async fn get_resource(
|
||||||
&self,
|
&self,
|
||||||
_path: &Self::PathComponents,
|
path: &Self::PathComponents,
|
||||||
|
show_deleted: bool,
|
||||||
) -> Result<Self::Resource, Self::Error>;
|
) -> Result<Self::Resource, Self::Error>;
|
||||||
|
|
||||||
async fn save_resource(
|
async fn save_resource(
|
||||||
|
|||||||
@@ -86,7 +86,11 @@ where
|
|||||||
|
|
||||||
const DAV_HEADER: &str = "1, 3, access-control";
|
const DAV_HEADER: &str = "1, 3, access-control";
|
||||||
|
|
||||||
async fn get_resource(&self, _: &()) -> Result<Self::Resource, Self::Error> {
|
async fn get_resource(
|
||||||
|
&self,
|
||||||
|
_: &(),
|
||||||
|
_show_deleted: bool,
|
||||||
|
) -> Result<Self::Resource, Self::Error> {
|
||||||
Ok(RootResource::<PRS::Resource, P>::default())
|
Ok(RootResource::<PRS::Resource, P>::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export class CreateCalendarForm extends LitElement {
|
|||||||
<displayname>${this.displayname}</displayname>
|
<displayname>${this.displayname}</displayname>
|
||||||
${this.description ? `<CAL:calendar-description>${this.description}</CAL:calendar-description>` : ''}
|
${this.description ? `<CAL:calendar-description>${this.description}</CAL:calendar-description>` : ''}
|
||||||
${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ''}
|
${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ''}
|
||||||
${this.subscriptionUrl ? `<CS:source>${this.subscriptionUrl}</CS:source>` : ''}
|
${this.subscriptionUrl ? `<CS:source><href>${this.subscriptionUrl}</href></CS:source>` : ''}
|
||||||
<CAL:supported-calendar-component-set>
|
<CAL:supported-calendar-component-set>
|
||||||
${Array.from(this.components.keys()).map(comp => `<CAL:comp name="${comp}" />`).join('\n')}
|
${Array.from(this.components.keys()).map(comp => `<CAL:comp name="${comp}" />`).join('\n')}
|
||||||
</CAL:supported-calendar-component-set>
|
</CAL:supported-calendar-component-set>
|
||||||
|
|||||||
44
crates/frontend/js-components/lib/delete-button.ts
Normal file
44
crates/frontend/js-components/lib/delete-button.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { html, LitElement } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
import { createClient } from "webdav";
|
||||||
|
|
||||||
|
@customElement("delete-button")
|
||||||
|
export class DeleteButton extends LitElement {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
trash: boolean = false
|
||||||
|
@property()
|
||||||
|
href: string
|
||||||
|
|
||||||
|
protected createRenderRoot() {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
let text = this.trash ? 'Move to trash' : 'Delete'
|
||||||
|
return html`<button class="delete" @click=${e => this._onClick(e)}>${text}</button>`
|
||||||
|
}
|
||||||
|
|
||||||
|
async _onClick(event: Event) {
|
||||||
|
event.preventDefault()
|
||||||
|
if (!this.trash && !confirm('Do you want to delete this collection permanently?')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = await fetch(this.href, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'X-No-Trashbin': this.trash ? '0' : '1'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (response.status < 200 || response.status >= 300) {
|
||||||
|
alert('An error occured, look into the console')
|
||||||
|
console.error(response)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ export default defineConfig({
|
|||||||
input: [
|
input: [
|
||||||
"lib/create-calendar-form.ts",
|
"lib/create-calendar-form.ts",
|
||||||
"lib/create-addressbook-form.ts",
|
"lib/create-addressbook-form.ts",
|
||||||
|
"lib/delete-button.ts",
|
||||||
],
|
],
|
||||||
output: {
|
output: {
|
||||||
dir: "../public/assets/js/",
|
dir: "../public/assets/js/",
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { i as c, x as u } from "./lit-CWlWuEHk.mjs";
|
import { i as c, x as u } from "./lit-CWlWuEHk.mjs";
|
||||||
import { e as d, n as m, a as o, t as h } from "./ref-DuYNkSJ_.mjs";
|
import { n as o, t as h } from "./property-DYFkTqgI.mjs";
|
||||||
|
import { e as d, n as m } from "./ref-nf9JiOyl.mjs";
|
||||||
import { a as b } from "./webdav-Bz4I5vNH.mjs";
|
import { a as b } from "./webdav-Bz4I5vNH.mjs";
|
||||||
var y = Object.defineProperty, f = Object.getOwnPropertyDescriptor, a = (t, s, l, r) => {
|
var y = Object.defineProperty, f = Object.getOwnPropertyDescriptor, r = (t, a, n, s) => {
|
||||||
for (var e = r > 1 ? void 0 : r ? f(s, l) : s, n = t.length - 1, p; n >= 0; n--)
|
for (var e = s > 1 ? void 0 : s ? f(a, n) : a, l = t.length - 1, p; l >= 0; l--)
|
||||||
(p = t[n]) && (e = (r ? p(s, l, e) : p(e)) || e);
|
(p = t[l]) && (e = (s ? p(a, n, e) : p(e)) || e);
|
||||||
return r && e && y(s, l, e), e;
|
return s && e && y(a, n, e), e;
|
||||||
};
|
};
|
||||||
let i = class extends c {
|
let i = class extends c {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -65,19 +66,19 @@ let i = class extends c {
|
|||||||
}), window.location.reload(), null;
|
}), window.location.reload(), null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
a([
|
r([
|
||||||
o()
|
o()
|
||||||
], i.prototype, "user", 2);
|
], i.prototype, "user", 2);
|
||||||
a([
|
r([
|
||||||
o()
|
o()
|
||||||
], i.prototype, "id", 2);
|
], i.prototype, "id", 2);
|
||||||
a([
|
r([
|
||||||
o()
|
o()
|
||||||
], i.prototype, "displayname", 2);
|
], i.prototype, "displayname", 2);
|
||||||
a([
|
r([
|
||||||
o()
|
o()
|
||||||
], i.prototype, "description", 2);
|
], i.prototype, "description", 2);
|
||||||
i = a([
|
i = r([
|
||||||
h("create-addressbook-form")
|
h("create-addressbook-form")
|
||||||
], i);
|
], i);
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { i as u, x as c } from "./lit-CWlWuEHk.mjs";
|
import { i as u, x as c } from "./lit-CWlWuEHk.mjs";
|
||||||
import { e as d, n as m, a as o, t as h } from "./ref-DuYNkSJ_.mjs";
|
import { n as o, t as h } from "./property-DYFkTqgI.mjs";
|
||||||
|
import { e as m, n as d } from "./ref-nf9JiOyl.mjs";
|
||||||
import { a as b } from "./webdav-Bz4I5vNH.mjs";
|
import { a as b } from "./webdav-Bz4I5vNH.mjs";
|
||||||
var y = Object.defineProperty, $ = Object.getOwnPropertyDescriptor, a = (t, e, l, s) => {
|
var y = Object.defineProperty, $ = Object.getOwnPropertyDescriptor, a = (t, e, n, s) => {
|
||||||
for (var i = s > 1 ? void 0 : s ? $(e, l) : e, n = t.length - 1, p; n >= 0; n--)
|
for (var i = s > 1 ? void 0 : s ? $(e, n) : e, l = t.length - 1, p; l >= 0; l--)
|
||||||
(p = t[n]) && (i = (s ? p(e, l, i) : p(i)) || i);
|
(p = t[l]) && (i = (s ? p(e, n, i) : p(i)) || i);
|
||||||
return s && i && y(e, l, i), i;
|
return s && i && y(e, n, i), i;
|
||||||
};
|
};
|
||||||
let r = class extends u {
|
let r = class extends u {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(), this.client = b("/caldav"), this.user = "", this.id = "", this.displayname = "", this.description = "", this.color = "", this.subscriptionUrl = "", this.components = /* @__PURE__ */ new Set(), this.dialog = d(), this.form = d();
|
super(), this.client = b("/caldav"), this.user = "", this.id = "", this.displayname = "", this.description = "", this.color = "", this.subscriptionUrl = "", this.components = /* @__PURE__ */ new Set(), this.dialog = m(), this.form = m();
|
||||||
}
|
}
|
||||||
createRenderRoot() {
|
createRenderRoot() {
|
||||||
return this;
|
return this;
|
||||||
@@ -16,9 +17,9 @@ let r = class extends u {
|
|||||||
render() {
|
render() {
|
||||||
return c`
|
return c`
|
||||||
<button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
|
<button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
|
||||||
<dialog ${m(this.dialog)}>
|
<dialog ${d(this.dialog)}>
|
||||||
<h3>Create calendar</h3>
|
<h3>Create calendar</h3>
|
||||||
<form @submit=${this.submit} ${m(this.form)}>
|
<form @submit=${this.submit} ${d(this.form)}>
|
||||||
<label>
|
<label>
|
||||||
id
|
id
|
||||||
<input type="text" name="id" @change=${(t) => this.id = t.target.value} />
|
<input type="text" name="id" @change=${(t) => this.id = t.target.value} />
|
||||||
@@ -80,7 +81,7 @@ let r = class extends u {
|
|||||||
<displayname>${this.displayname}</displayname>
|
<displayname>${this.displayname}</displayname>
|
||||||
${this.description ? `<CAL:calendar-description>${this.description}</CAL:calendar-description>` : ""}
|
${this.description ? `<CAL:calendar-description>${this.description}</CAL:calendar-description>` : ""}
|
||||||
${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ""}
|
${this.color ? `<ICAL:calendar-color>${this.color}</ICAL:calendar-color>` : ""}
|
||||||
${this.subscriptionUrl ? `<CS:source>${this.subscriptionUrl}</CS:source>` : ""}
|
${this.subscriptionUrl ? `<CS:source><href>${this.subscriptionUrl}</href></CS:source>` : ""}
|
||||||
<CAL:supported-calendar-component-set>
|
<CAL:supported-calendar-component-set>
|
||||||
${Array.from(this.components.keys()).map((e) => `<CAL:comp name="${e}" />`).join(`
|
${Array.from(this.components.keys()).map((e) => `<CAL:comp name="${e}" />`).join(`
|
||||||
`)}
|
`)}
|
||||||
|
|||||||
46
crates/frontend/public/assets/js/delete-button.mjs
Normal file
46
crates/frontend/public/assets/js/delete-button.mjs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { i as c, x as p } from "./lit-CWlWuEHk.mjs";
|
||||||
|
import { n as h, t as u } from "./property-DYFkTqgI.mjs";
|
||||||
|
var f = Object.defineProperty, d = Object.getOwnPropertyDescriptor, i = (r, t, n, o) => {
|
||||||
|
for (var e = o > 1 ? void 0 : o ? d(t, n) : t, l = r.length - 1, a; l >= 0; l--)
|
||||||
|
(a = r[l]) && (e = (o ? a(t, n, e) : a(e)) || e);
|
||||||
|
return o && e && f(t, n, e), e;
|
||||||
|
};
|
||||||
|
let s = class extends c {
|
||||||
|
constructor() {
|
||||||
|
super(), this.trash = !1;
|
||||||
|
}
|
||||||
|
createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
let r = this.trash ? "Move to trash" : "Delete";
|
||||||
|
return p`<button class="delete" @click=${(t) => this._onClick(t)}>${r}</button>`;
|
||||||
|
}
|
||||||
|
async _onClick(r) {
|
||||||
|
if (r.preventDefault(), !this.trash && !confirm("Do you want to delete this collection permanently?"))
|
||||||
|
return;
|
||||||
|
let t = await fetch(this.href, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"X-No-Trashbin": this.trash ? "0" : "1"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (t.status < 200 || t.status >= 300) {
|
||||||
|
alert("An error occured, look into the console"), console.error(t);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
i([
|
||||||
|
h({ type: Boolean })
|
||||||
|
], s.prototype, "trash", 2);
|
||||||
|
i([
|
||||||
|
h()
|
||||||
|
], s.prototype, "href", 2);
|
||||||
|
s = i([
|
||||||
|
u("delete-button")
|
||||||
|
], s);
|
||||||
|
export {
|
||||||
|
s as DeleteButton
|
||||||
|
};
|
||||||
47
crates/frontend/public/assets/js/property-DYFkTqgI.mjs
Normal file
47
crates/frontend/public/assets/js/property-DYFkTqgI.mjs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { f as d, u as l } from "./lit-CWlWuEHk.mjs";
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2017 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
const f = (t) => (r, e) => {
|
||||||
|
e !== void 0 ? e.addInitializer(() => {
|
||||||
|
customElements.define(t, r);
|
||||||
|
}) : customElements.define(t, r);
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2017 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
const p = { attribute: !0, type: String, converter: l, reflect: !1, hasChanged: d }, u = (t = p, r, e) => {
|
||||||
|
const { kind: i, metadata: a } = e;
|
||||||
|
let n = globalThis.litPropertyMetadata.get(a);
|
||||||
|
if (n === void 0 && globalThis.litPropertyMetadata.set(a, n = /* @__PURE__ */ new Map()), i === "setter" && ((t = Object.create(t)).wrapped = !0), n.set(e.name, t), i === "accessor") {
|
||||||
|
const { name: o } = e;
|
||||||
|
return { set(s) {
|
||||||
|
const c = r.get.call(this);
|
||||||
|
r.set.call(this, s), this.requestUpdate(o, c, t);
|
||||||
|
}, init(s) {
|
||||||
|
return s !== void 0 && this.C(o, void 0, t, s), s;
|
||||||
|
} };
|
||||||
|
}
|
||||||
|
if (i === "setter") {
|
||||||
|
const { name: o } = e;
|
||||||
|
return function(s) {
|
||||||
|
const c = this[o];
|
||||||
|
r.call(this, s), this.requestUpdate(o, c, t);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw Error("Unsupported decorator location: " + i);
|
||||||
|
};
|
||||||
|
function m(t) {
|
||||||
|
return (r, e) => typeof e == "object" ? u(t, r, e) : ((i, a, n) => {
|
||||||
|
const o = a.hasOwnProperty(n);
|
||||||
|
return a.constructor.createProperty(n, i), o ? Object.getOwnPropertyDescriptor(a, n) : void 0;
|
||||||
|
})(t, r, e);
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
m as n,
|
||||||
|
f as t
|
||||||
|
};
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
import { f, u as _, E as $ } from "./lit-CWlWuEHk.mjs";
|
|
||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2017 Google LLC
|
|
||||||
* SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
*/
|
|
||||||
const T = (t) => (e, s) => {
|
|
||||||
s !== void 0 ? s.addInitializer(() => {
|
|
||||||
customElements.define(t, e);
|
|
||||||
}) : customElements.define(t, e);
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2017 Google LLC
|
|
||||||
* SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
*/
|
|
||||||
const A = { attribute: !0, type: String, converter: _, reflect: !1, hasChanged: f }, p = (t = A, e, s) => {
|
|
||||||
const { kind: i, metadata: n } = s;
|
|
||||||
let r = globalThis.litPropertyMetadata.get(n);
|
|
||||||
if (r === void 0 && globalThis.litPropertyMetadata.set(n, r = /* @__PURE__ */ new Map()), i === "setter" && ((t = Object.create(t)).wrapped = !0), r.set(s.name, t), i === "accessor") {
|
|
||||||
const { name: o } = s;
|
|
||||||
return { set(h) {
|
|
||||||
const l = e.get.call(this);
|
|
||||||
e.set.call(this, h), this.requestUpdate(o, l, t);
|
|
||||||
}, init(h) {
|
|
||||||
return h !== void 0 && this.C(o, void 0, t, h), h;
|
|
||||||
} };
|
|
||||||
}
|
|
||||||
if (i === "setter") {
|
|
||||||
const { name: o } = s;
|
|
||||||
return function(h) {
|
|
||||||
const l = this[o];
|
|
||||||
e.call(this, h), this.requestUpdate(o, l, t);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
throw Error("Unsupported decorator location: " + i);
|
|
||||||
};
|
|
||||||
function O(t) {
|
|
||||||
return (e, s) => typeof s == "object" ? p(t, e, s) : ((i, n, r) => {
|
|
||||||
const o = n.hasOwnProperty(r);
|
|
||||||
return n.constructor.createProperty(r, i), o ? Object.getOwnPropertyDescriptor(n, r) : void 0;
|
|
||||||
})(t, e, s);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2020 Google LLC
|
|
||||||
* SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
*/
|
|
||||||
const v = (t) => t.strings === void 0;
|
|
||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2017 Google LLC
|
|
||||||
* SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
*/
|
|
||||||
const g = { CHILD: 2 }, C = (t) => (...e) => ({ _$litDirective$: t, values: e });
|
|
||||||
class m {
|
|
||||||
constructor(e) {
|
|
||||||
}
|
|
||||||
get _$AU() {
|
|
||||||
return this._$AM._$AU;
|
|
||||||
}
|
|
||||||
_$AT(e, s, i) {
|
|
||||||
this._$Ct = e, this._$AM = s, this._$Ci = i;
|
|
||||||
}
|
|
||||||
_$AS(e, s) {
|
|
||||||
return this.update(e, s);
|
|
||||||
}
|
|
||||||
update(e, s) {
|
|
||||||
return this.render(...s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2017 Google LLC
|
|
||||||
* SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
*/
|
|
||||||
const c = (t, e) => {
|
|
||||||
var i;
|
|
||||||
const s = t._$AN;
|
|
||||||
if (s === void 0) return !1;
|
|
||||||
for (const n of s) (i = n._$AO) == null || i.call(n, e, !1), c(n, e);
|
|
||||||
return !0;
|
|
||||||
}, a = (t) => {
|
|
||||||
let e, s;
|
|
||||||
do {
|
|
||||||
if ((e = t._$AM) === void 0) break;
|
|
||||||
s = e._$AN, s.delete(t), t = e;
|
|
||||||
} while ((s == null ? void 0 : s.size) === 0);
|
|
||||||
}, u = (t) => {
|
|
||||||
for (let e; e = t._$AM; t = e) {
|
|
||||||
let s = e._$AN;
|
|
||||||
if (s === void 0) e._$AN = s = /* @__PURE__ */ new Set();
|
|
||||||
else if (s.has(t)) break;
|
|
||||||
s.add(t), M(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
function y(t) {
|
|
||||||
this._$AN !== void 0 ? (a(this), this._$AM = t, u(this)) : this._$AM = t;
|
|
||||||
}
|
|
||||||
function G(t, e = !1, s = 0) {
|
|
||||||
const i = this._$AH, n = this._$AN;
|
|
||||||
if (n !== void 0 && n.size !== 0) if (e) if (Array.isArray(i)) for (let r = s; r < i.length; r++) c(i[r], !1), a(i[r]);
|
|
||||||
else i != null && (c(i, !1), a(i));
|
|
||||||
else c(this, t);
|
|
||||||
}
|
|
||||||
const M = (t) => {
|
|
||||||
t.type == g.CHILD && (t._$AP ?? (t._$AP = G), t._$AQ ?? (t._$AQ = y));
|
|
||||||
};
|
|
||||||
class b extends m {
|
|
||||||
constructor() {
|
|
||||||
super(...arguments), this._$AN = void 0;
|
|
||||||
}
|
|
||||||
_$AT(e, s, i) {
|
|
||||||
super._$AT(e, s, i), u(this), this.isConnected = e._$AU;
|
|
||||||
}
|
|
||||||
_$AO(e, s = !0) {
|
|
||||||
var i, n;
|
|
||||||
e !== this.isConnected && (this.isConnected = e, e ? (i = this.reconnected) == null || i.call(this) : (n = this.disconnected) == null || n.call(this)), s && (c(this, e), a(this));
|
|
||||||
}
|
|
||||||
setValue(e) {
|
|
||||||
if (v(this._$Ct)) this._$Ct._$AI(e, this);
|
|
||||||
else {
|
|
||||||
const s = [...this._$Ct._$AH];
|
|
||||||
s[this._$Ci] = e, this._$Ct._$AI(s, this, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
disconnected() {
|
|
||||||
}
|
|
||||||
reconnected() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2020 Google LLC
|
|
||||||
* SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
*/
|
|
||||||
const P = () => new w();
|
|
||||||
class w {
|
|
||||||
}
|
|
||||||
const d = /* @__PURE__ */ new WeakMap(), U = C(class extends b {
|
|
||||||
render(t) {
|
|
||||||
return $;
|
|
||||||
}
|
|
||||||
update(t, [e]) {
|
|
||||||
var i;
|
|
||||||
const s = e !== this.G;
|
|
||||||
return s && this.G !== void 0 && this.rt(void 0), (s || this.lt !== this.ct) && (this.G = e, this.ht = (i = t.options) == null ? void 0 : i.host, this.rt(this.ct = t.element)), $;
|
|
||||||
}
|
|
||||||
rt(t) {
|
|
||||||
if (this.isConnected || (t = void 0), typeof this.G == "function") {
|
|
||||||
const e = this.ht ?? globalThis;
|
|
||||||
let s = d.get(e);
|
|
||||||
s === void 0 && (s = /* @__PURE__ */ new WeakMap(), d.set(e, s)), s.get(this.G) !== void 0 && this.G.call(this.ht, void 0), s.set(this.G, t), t !== void 0 && this.G.call(this.ht, t);
|
|
||||||
} else this.G.value = t;
|
|
||||||
}
|
|
||||||
get lt() {
|
|
||||||
var t, e;
|
|
||||||
return typeof this.G == "function" ? (t = d.get(this.ht ?? globalThis)) == null ? void 0 : t.get(this.G) : (e = this.G) == null ? void 0 : e.value;
|
|
||||||
}
|
|
||||||
disconnected() {
|
|
||||||
this.lt === this.ct && this.rt(void 0);
|
|
||||||
}
|
|
||||||
reconnected() {
|
|
||||||
this.rt(this.ct);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
export {
|
|
||||||
O as a,
|
|
||||||
P as e,
|
|
||||||
U as n,
|
|
||||||
T as t
|
|
||||||
};
|
|
||||||
128
crates/frontend/public/assets/js/ref-nf9JiOyl.mjs
Normal file
128
crates/frontend/public/assets/js/ref-nf9JiOyl.mjs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { E as $ } from "./lit-CWlWuEHk.mjs";
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2020 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
const d = (t) => t.strings === void 0;
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2017 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
const _ = { CHILD: 2 }, a = (t) => (...s) => ({ _$litDirective$: t, values: s });
|
||||||
|
class A {
|
||||||
|
constructor(s) {
|
||||||
|
}
|
||||||
|
get _$AU() {
|
||||||
|
return this._$AM._$AU;
|
||||||
|
}
|
||||||
|
_$AT(s, e, i) {
|
||||||
|
this._$Ct = s, this._$AM = e, this._$Ci = i;
|
||||||
|
}
|
||||||
|
_$AS(s, e) {
|
||||||
|
return this.update(s, e);
|
||||||
|
}
|
||||||
|
update(s, e) {
|
||||||
|
return this.render(...e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2017 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
const n = (t, s) => {
|
||||||
|
var i;
|
||||||
|
const e = t._$AN;
|
||||||
|
if (e === void 0) return !1;
|
||||||
|
for (const h of e) (i = h._$AO) == null || i.call(h, s, !1), n(h, s);
|
||||||
|
return !0;
|
||||||
|
}, r = (t) => {
|
||||||
|
let s, e;
|
||||||
|
do {
|
||||||
|
if ((s = t._$AM) === void 0) break;
|
||||||
|
e = s._$AN, e.delete(t), t = s;
|
||||||
|
} while ((e == null ? void 0 : e.size) === 0);
|
||||||
|
}, l = (t) => {
|
||||||
|
for (let s; s = t._$AM; t = s) {
|
||||||
|
let e = s._$AN;
|
||||||
|
if (e === void 0) s._$AN = e = /* @__PURE__ */ new Set();
|
||||||
|
else if (e.has(t)) break;
|
||||||
|
e.add(t), v(s);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
function f(t) {
|
||||||
|
this._$AN !== void 0 ? (r(this), this._$AM = t, l(this)) : this._$AM = t;
|
||||||
|
}
|
||||||
|
function u(t, s = !1, e = 0) {
|
||||||
|
const i = this._$AH, h = this._$AN;
|
||||||
|
if (h !== void 0 && h.size !== 0) if (s) if (Array.isArray(i)) for (let o = e; o < i.length; o++) n(i[o], !1), r(i[o]);
|
||||||
|
else i != null && (n(i, !1), r(i));
|
||||||
|
else n(this, t);
|
||||||
|
}
|
||||||
|
const v = (t) => {
|
||||||
|
t.type == _.CHILD && (t._$AP ?? (t._$AP = u), t._$AQ ?? (t._$AQ = f));
|
||||||
|
};
|
||||||
|
class p extends A {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments), this._$AN = void 0;
|
||||||
|
}
|
||||||
|
_$AT(s, e, i) {
|
||||||
|
super._$AT(s, e, i), l(this), this.isConnected = s._$AU;
|
||||||
|
}
|
||||||
|
_$AO(s, e = !0) {
|
||||||
|
var i, h;
|
||||||
|
s !== this.isConnected && (this.isConnected = s, s ? (i = this.reconnected) == null || i.call(this) : (h = this.disconnected) == null || h.call(this)), e && (n(this, s), r(this));
|
||||||
|
}
|
||||||
|
setValue(s) {
|
||||||
|
if (d(this._$Ct)) this._$Ct._$AI(s, this);
|
||||||
|
else {
|
||||||
|
const e = [...this._$Ct._$AH];
|
||||||
|
e[this._$Ci] = s, this._$Ct._$AI(e, this, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disconnected() {
|
||||||
|
}
|
||||||
|
reconnected() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2020 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
const g = () => new C();
|
||||||
|
class C {
|
||||||
|
}
|
||||||
|
const c = /* @__PURE__ */ new WeakMap(), M = a(class extends p {
|
||||||
|
render(t) {
|
||||||
|
return $;
|
||||||
|
}
|
||||||
|
update(t, [s]) {
|
||||||
|
var i;
|
||||||
|
const e = s !== this.G;
|
||||||
|
return e && this.G !== void 0 && this.rt(void 0), (e || this.lt !== this.ct) && (this.G = s, this.ht = (i = t.options) == null ? void 0 : i.host, this.rt(this.ct = t.element)), $;
|
||||||
|
}
|
||||||
|
rt(t) {
|
||||||
|
if (this.isConnected || (t = void 0), typeof this.G == "function") {
|
||||||
|
const s = this.ht ?? globalThis;
|
||||||
|
let e = c.get(s);
|
||||||
|
e === void 0 && (e = /* @__PURE__ */ new WeakMap(), c.set(s, e)), e.get(this.G) !== void 0 && this.G.call(this.ht, void 0), e.set(this.G, t), t !== void 0 && this.G.call(this.ht, t);
|
||||||
|
} else this.G.value = t;
|
||||||
|
}
|
||||||
|
get lt() {
|
||||||
|
var t, s;
|
||||||
|
return typeof this.G == "function" ? (t = c.get(this.ht ?? globalThis)) == null ? void 0 : t.get(this.G) : (s = this.G) == null ? void 0 : s.value;
|
||||||
|
}
|
||||||
|
disconnected() {
|
||||||
|
this.lt === this.ct && this.rt(void 0);
|
||||||
|
}
|
||||||
|
reconnected() {
|
||||||
|
this.rt(this.ct);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
export {
|
||||||
|
g as e,
|
||||||
|
M as n
|
||||||
|
};
|
||||||
@@ -160,12 +160,12 @@ table {
|
|||||||
display: grid;
|
display: grid;
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
". . color-chip"
|
". . color-chip"
|
||||||
"title comps color-chip"
|
"title comps color-chip"
|
||||||
"description . color-chip"
|
"description description color-chip"
|
||||||
"subscription-url . color-chip"
|
"subscription-url subscription-url color-chip"
|
||||||
"actions . color-chip"
|
"actions actions color-chip"
|
||||||
". . color-chip";
|
". . color-chip";
|
||||||
grid-template-rows: 12px auto auto auto auto 12px;
|
grid-template-rows: 12px auto auto auto auto 12px;
|
||||||
grid-template-columns: min-content auto 80px;
|
grid-template-columns: min-content auto 80px;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@@ -220,6 +220,8 @@ table {
|
|||||||
.actions {
|
.actions {
|
||||||
grid-area: actions;
|
grid-area: actions;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
@@ -10,12 +10,4 @@
|
|||||||
|
|
||||||
<pre>{{ addressbook|json }}</pre>
|
<pre>{{ addressbook|json }}</pre>
|
||||||
|
|
||||||
<h2>Delete</h2>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<form method="POST" action="/frontend/user/{{addressbook.principal}}/addressbook/{{addressbook.id}}/delete">
|
|
||||||
<button type="submit">Move to trash</button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -31,12 +31,4 @@
|
|||||||
|
|
||||||
<pre>{{ calendar|json }}</pre>
|
<pre>{{ calendar|json }}</pre>
|
||||||
|
|
||||||
<h2>Delete</h2>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<form method="POST" action="/frontend/user/{{calendar.principal}}/calendar/{{calendar.id}}/delete">
|
|
||||||
<button type="submit">Move to trash</button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{%endblock %}
|
{%endblock %}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
{% block imports %}
|
{% block imports %}
|
||||||
<script type="module" src="/frontend/assets/js/create-calendar-form.mjs" async></script>
|
<script type="module" src="/frontend/assets/js/create-calendar-form.mjs" async></script>
|
||||||
<script type="module" src="/frontend/assets/js/create-addressbook-form.mjs" async></script>
|
<script type="module" src="/frontend/assets/js/create-addressbook-form.mjs" async></script>
|
||||||
|
<script type="module" src="/frontend/assets/js/delete-button.mjs" async></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@@ -83,9 +84,10 @@
|
|||||||
<span class="subscription-url">{{ subscription_url }}</span>
|
<span class="subscription-url">{{ subscription_url }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<form action="/caldav/principal/{{ calendar.principal }}/calendar/{{ calendar.id }}" target="_blank" method="GET">
|
<form action="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}" target="_blank" method="GET">
|
||||||
<button type="submit">Download</button>
|
<button type="submit">Download</button>
|
||||||
</form>
|
</form>
|
||||||
|
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="color-chip"></div>
|
<div class="color-chip"></div>
|
||||||
</a>
|
</a>
|
||||||
@@ -114,6 +116,7 @@
|
|||||||
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST" class="restore-form">
|
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST" class="restore-form">
|
||||||
<button type="submit">Restore</button>
|
<button type="submit">Restore</button>
|
||||||
</form>
|
</form>
|
||||||
|
<delete-button href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="color-chip"></div>
|
<div class="color-chip"></div>
|
||||||
</a>
|
</a>
|
||||||
@@ -139,6 +142,7 @@
|
|||||||
<form action="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}" target="_blank" method="GET">
|
<form action="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}" target="_blank" method="GET">
|
||||||
<button type="submit">Download</button>
|
<button type="submit">Download</button>
|
||||||
</form>
|
</form>
|
||||||
|
<delete-button trash href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -160,6 +164,7 @@
|
|||||||
<form action="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}/restore" method="POST" class="restore-form">
|
<form action="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}/restore" method="POST" class="restore-form">
|
||||||
<button type="submit">Restore</button>
|
<button type="submit">Restore</button>
|
||||||
</form>
|
</form>
|
||||||
|
<delete-button href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -12,3 +12,12 @@ pub struct FrontendConfig {
|
|||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub allow_password_login: bool,
|
pub allow_password_login: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for FrontendConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: true,
|
||||||
|
allow_password_login: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ pub use config::FrontendConfig;
|
|||||||
use oidc_user_store::OidcUserStore;
|
use oidc_user_store::OidcUserStore;
|
||||||
|
|
||||||
use crate::routes::{
|
use crate::routes::{
|
||||||
addressbook::{route_addressbook, route_addressbook_restore, route_delete_addressbook},
|
addressbook::{route_addressbook, route_addressbook_restore},
|
||||||
app_token::{route_delete_app_token, route_post_app_token},
|
app_token::{route_delete_app_token, route_post_app_token},
|
||||||
calendar::{route_calendar, route_calendar_restore, route_delete_calendar},
|
calendar::{route_calendar, route_calendar_restore},
|
||||||
login::{route_get_login, route_post_login, route_post_logout},
|
login::{route_get_login, route_post_login, route_post_logout},
|
||||||
user::{route_get_home, route_root, route_user_named},
|
user::{route_get_home, route_root, route_user_named},
|
||||||
};
|
};
|
||||||
@@ -60,10 +60,6 @@ pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: Addres
|
|||||||
"/user/{user}/calendar/{calendar}",
|
"/user/{user}/calendar/{calendar}",
|
||||||
get(route_calendar::<CS>),
|
get(route_calendar::<CS>),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/user/{user}/calendar/{calendar}/delete",
|
|
||||||
post(route_delete_calendar::<CS>),
|
|
||||||
)
|
|
||||||
.route(
|
.route(
|
||||||
"/user/{user}/calendar/{calendar}/restore",
|
"/user/{user}/calendar/{calendar}/restore",
|
||||||
post(route_calendar_restore::<CS>),
|
post(route_calendar_restore::<CS>),
|
||||||
@@ -73,10 +69,6 @@ pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: Addres
|
|||||||
"/user/{user}/addressbook/{addressbook}",
|
"/user/{user}/addressbook/{addressbook}",
|
||||||
get(route_addressbook::<AS>),
|
get(route_addressbook::<AS>),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/user/{user}/addressbook/{addressbook}/delete",
|
|
||||||
post(route_delete_addressbook::<AS>),
|
|
||||||
)
|
|
||||||
.route(
|
.route(
|
||||||
"/user/{user}/addressbook/{addressbook}/restore",
|
"/user/{user}/addressbook/{addressbook}/restore",
|
||||||
post(route_addressbook_restore::<AS>),
|
post(route_addressbook_restore::<AS>),
|
||||||
|
|||||||
@@ -47,19 +47,3 @@ pub async fn route_addressbook_restore<AS: AddressbookStore>(
|
|||||||
None => (StatusCode::CREATED, "Restored").into_response(),
|
None => (StatusCode::CREATED, "Restored").into_response(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn route_delete_addressbook<AS: AddressbookStore>(
|
|
||||||
Path((owner, addressbook_id)): Path<(String, String)>,
|
|
||||||
Extension(store): Extension<Arc<AS>>,
|
|
||||||
user: Principal,
|
|
||||||
) -> Result<Response, rustical_store::Error> {
|
|
||||||
if !user.is_principal(&owner) {
|
|
||||||
return Ok(StatusCode::UNAUTHORIZED.into_response());
|
|
||||||
}
|
|
||||||
|
|
||||||
store
|
|
||||||
.delete_addressbook(&owner, &addressbook_id, true)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Redirect::to(&format!("/frontend/user/{}", user.id)).into_response())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ pub async fn route_calendar<C: CalendarStore>(
|
|||||||
return Ok(StatusCode::UNAUTHORIZED.into_response());
|
return Ok(StatusCode::UNAUTHORIZED.into_response());
|
||||||
}
|
}
|
||||||
Ok(CalendarPage {
|
Ok(CalendarPage {
|
||||||
calendar: store.get_calendar(&owner, &cal_id).await?,
|
calendar: store.get_calendar(&owner, &cal_id, true).await?,
|
||||||
}
|
}
|
||||||
.into_response())
|
.into_response())
|
||||||
}
|
}
|
||||||
@@ -47,17 +47,3 @@ pub async fn route_calendar_restore<CS: CalendarStore>(
|
|||||||
None => (StatusCode::CREATED, "Restored").into_response(),
|
None => (StatusCode::CREATED, "Restored").into_response(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn route_delete_calendar<C: CalendarStore>(
|
|
||||||
Path((owner, cal_id)): Path<(String, String)>,
|
|
||||||
Extension(store): Extension<Arc<C>>,
|
|
||||||
user: Principal,
|
|
||||||
) -> Result<Response, rustical_store::Error> {
|
|
||||||
if !user.is_principal(&owner) {
|
|
||||||
return Ok(StatusCode::UNAUTHORIZED.into_response());
|
|
||||||
}
|
|
||||||
|
|
||||||
store.delete_calendar(&owner, &cal_id, true).await?;
|
|
||||||
|
|
||||||
Ok(Redirect::to(&format!("/frontend/user/{}", user.id)).into_response())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -62,14 +62,14 @@ impl AddressObject {
|
|||||||
&self.vcf
|
&self.vcf
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_anniversary(&self) -> Option<CalDateTime> {
|
pub fn get_anniversary(&self) -> Option<(CalDateTime, bool)> {
|
||||||
let prop = self.vcard.get_property("ANNIVERSARY")?;
|
let prop = self.vcard.get_property("ANNIVERSARY")?.value.as_deref()?;
|
||||||
CalDateTime::parse_prop(prop, &HashMap::default()).ok()
|
CalDateTime::parse_vcard(prop).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_birthday(&self) -> Option<CalDateTime> {
|
pub fn get_birthday(&self) -> Option<(CalDateTime, bool)> {
|
||||||
let prop = self.vcard.get_property("BDAY")?;
|
let prop = self.vcard.get_property("BDAY")?.value.as_deref()?;
|
||||||
CalDateTime::parse_prop(prop, &HashMap::default()).ok()
|
CalDateTime::parse_vcard(prop).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_full_name(&self) -> Option<&str> {
|
pub fn get_full_name(&self) -> Option<&str> {
|
||||||
@@ -78,25 +78,27 @@ impl AddressObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_anniversary_object(&self) -> Result<Option<CalendarObject>, Error> {
|
pub fn get_anniversary_object(&self) -> Result<Option<CalendarObject>, Error> {
|
||||||
Ok(if let Some(anniversary) = self.get_anniversary() {
|
Ok(
|
||||||
let fullname = if let Some(name) = self.get_full_name() {
|
if let Some((anniversary, contains_year)) = self.get_anniversary() {
|
||||||
name
|
let fullname = if let Some(name) = self.get_full_name() {
|
||||||
} else {
|
name
|
||||||
return Ok(None);
|
} else {
|
||||||
};
|
return Ok(None);
|
||||||
let anniversary = anniversary.date();
|
};
|
||||||
let year = anniversary.year();
|
let anniversary = anniversary.date();
|
||||||
let anniversary_start = anniversary.format(LOCAL_DATE);
|
let year = contains_year.then_some(anniversary.year());
|
||||||
let anniversary_end = anniversary
|
let anniversary_start = anniversary.format(LOCAL_DATE);
|
||||||
.succ_opt()
|
let anniversary_end = anniversary
|
||||||
.unwrap_or(anniversary)
|
.succ_opt()
|
||||||
.format(LOCAL_DATE);
|
.unwrap_or(anniversary)
|
||||||
let uid = format!("{}-anniversary", self.get_id());
|
.format(LOCAL_DATE);
|
||||||
|
let uid = format!("{}-anniversary", self.get_id());
|
||||||
|
|
||||||
Some(CalendarObject::from_ics(
|
let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
|
||||||
uid.clone(),
|
Some(CalendarObject::from_ics(
|
||||||
format!(
|
uid.clone(),
|
||||||
r#"BEGIN:VCALENDAR
|
format!(
|
||||||
|
r#"BEGIN:VCALENDAR
|
||||||
VERSION:2.0
|
VERSION:2.0
|
||||||
CALSCALE:GREGORIAN
|
CALSCALE:GREGORIAN
|
||||||
PRODID:-//github.com/lennart-k/rustical birthday calendar//EN
|
PRODID:-//github.com/lennart-k/rustical birthday calendar//EN
|
||||||
@@ -105,39 +107,42 @@ DTSTART;VALUE=DATE:{anniversary_start}
|
|||||||
DTEND;VALUE=DATE:{anniversary_end}
|
DTEND;VALUE=DATE:{anniversary_end}
|
||||||
UID:{uid}
|
UID:{uid}
|
||||||
RRULE:FREQ=YEARLY
|
RRULE:FREQ=YEARLY
|
||||||
SUMMARY:💍 {fullname} ({year})
|
SUMMARY:💍 {fullname}{year_suffix}
|
||||||
TRANSP:TRANSPARENT
|
TRANSP:TRANSPARENT
|
||||||
BEGIN:VALARM
|
BEGIN:VALARM
|
||||||
TRIGGER;VALUE=DURATION:-PT0M
|
TRIGGER;VALUE=DURATION:-PT0M
|
||||||
ACTION:DISPLAY
|
ACTION:DISPLAY
|
||||||
DESCRIPTION:💍 {fullname} ({year})
|
DESCRIPTION:💍 {fullname}{year_suffix}
|
||||||
END:VALARM
|
END:VALARM
|
||||||
END:VEVENT
|
END:VEVENT
|
||||||
END:VCALENDAR"#,
|
END:VCALENDAR"#,
|
||||||
),
|
),
|
||||||
)?)
|
)?)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_birthday_object(&self) -> Result<Option<CalendarObject>, Error> {
|
pub fn get_birthday_object(&self) -> Result<Option<CalendarObject>, Error> {
|
||||||
Ok(if let Some(birthday) = self.get_birthday() {
|
Ok(
|
||||||
let fullname = if let Some(name) = self.get_full_name() {
|
if let Some((birthday, contains_year)) = self.get_birthday() {
|
||||||
name
|
let fullname = if let Some(name) = self.get_full_name() {
|
||||||
} else {
|
name
|
||||||
return Ok(None);
|
} else {
|
||||||
};
|
return Ok(None);
|
||||||
let birthday = birthday.date();
|
};
|
||||||
let year = birthday.year();
|
let birthday = birthday.date();
|
||||||
let birthday_start = birthday.format(LOCAL_DATE);
|
let year = contains_year.then_some(birthday.year());
|
||||||
let birthday_end = birthday.succ_opt().unwrap_or(birthday).format(LOCAL_DATE);
|
let birthday_start = birthday.format(LOCAL_DATE);
|
||||||
let uid = format!("{}-birthday", self.get_id());
|
let birthday_end = birthday.succ_opt().unwrap_or(birthday).format(LOCAL_DATE);
|
||||||
|
let uid = format!("{}-birthday", self.get_id());
|
||||||
|
|
||||||
Some(CalendarObject::from_ics(
|
let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
|
||||||
uid.clone(),
|
Some(CalendarObject::from_ics(
|
||||||
format!(
|
uid.clone(),
|
||||||
r#"BEGIN:VCALENDAR
|
format!(
|
||||||
|
r#"BEGIN:VCALENDAR
|
||||||
VERSION:2.0
|
VERSION:2.0
|
||||||
CALSCALE:GREGORIAN
|
CALSCALE:GREGORIAN
|
||||||
PRODID:-//github.com/lennart-k/rustical birthday calendar//EN
|
PRODID:-//github.com/lennart-k/rustical birthday calendar//EN
|
||||||
@@ -146,20 +151,21 @@ DTSTART;VALUE=DATE:{birthday_start}
|
|||||||
DTEND;VALUE=DATE:{birthday_end}
|
DTEND;VALUE=DATE:{birthday_end}
|
||||||
UID:{uid}
|
UID:{uid}
|
||||||
RRULE:FREQ=YEARLY
|
RRULE:FREQ=YEARLY
|
||||||
SUMMARY:🎂 {fullname} ({year})
|
SUMMARY:🎂 {fullname}{year_suffix}
|
||||||
TRANSP:TRANSPARENT
|
TRANSP:TRANSPARENT
|
||||||
BEGIN:VALARM
|
BEGIN:VALARM
|
||||||
TRIGGER;VALUE=DURATION:-PT0M
|
TRIGGER;VALUE=DURATION:-PT0M
|
||||||
ACTION:DISPLAY
|
ACTION:DISPLAY
|
||||||
DESCRIPTION:🎂 {fullname} ({year})
|
DESCRIPTION:🎂 {fullname}{year_suffix}
|
||||||
END:VALARM
|
END:VALARM
|
||||||
END:VEVENT
|
END:VEVENT
|
||||||
END:VCALENDAR"#,
|
END:VCALENDAR"#,
|
||||||
),
|
),
|
||||||
)?)
|
)?)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get significant dates associated with this address object
|
/// Get significant dates associated with this address object
|
||||||
|
|||||||
@@ -254,6 +254,16 @@ impl CalDateTime {
|
|||||||
if let Ok(date) = NaiveDate::parse_from_str(value, "%Y%m%d") {
|
if let Ok(date) = NaiveDate::parse_from_str(value, "%Y%m%d") {
|
||||||
return Ok(CalDateTime::Date(date, timezone));
|
return Ok(CalDateTime::Date(date, timezone));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Err(CalDateTimeError::InvalidDatetimeFormat(value.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also returns whether the date contains a year
|
||||||
|
pub fn parse_vcard(value: &str) -> Result<(Self, bool), CalDateTimeError> {
|
||||||
|
if let Ok(datetime) = Self::parse(value, None) {
|
||||||
|
return Ok((datetime, true));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(captures) = RE_VCARD_DATE_MM_DD.captures(value) {
|
if let Some(captures) = RE_VCARD_DATE_MM_DD.captures(value) {
|
||||||
// Because 1972 is a leap year
|
// Because 1972 is a leap year
|
||||||
let year = 1972;
|
let year = 1972;
|
||||||
@@ -261,13 +271,15 @@ impl CalDateTime {
|
|||||||
let month = captures.name("m").unwrap().as_str().parse().ok().unwrap();
|
let month = captures.name("m").unwrap().as_str().parse().ok().unwrap();
|
||||||
let day = captures.name("d").unwrap().as_str().parse().ok().unwrap();
|
let day = captures.name("d").unwrap().as_str().parse().ok().unwrap();
|
||||||
|
|
||||||
return Ok(CalDateTime::Date(
|
return Ok((
|
||||||
NaiveDate::from_ymd_opt(year, month, day)
|
CalDateTime::Date(
|
||||||
.ok_or(CalDateTimeError::ParseError(value.to_string()))?,
|
NaiveDate::from_ymd_opt(year, month, day)
|
||||||
timezone,
|
.ok_or(CalDateTimeError::ParseError(value.to_string()))?,
|
||||||
|
CalTimezone::Local,
|
||||||
|
),
|
||||||
|
false,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(CalDateTimeError::InvalidDatetimeFormat(value.to_string()))
|
Err(CalDateTimeError::InvalidDatetimeFormat(value.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,24 +419,33 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_vcard_date() {
|
fn test_vcard_date() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
CalDateTime::parse("19850412", None).unwrap(),
|
CalDateTime::parse_vcard("19850412").unwrap(),
|
||||||
CalDateTime::Date(
|
(
|
||||||
NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(),
|
CalDateTime::Date(
|
||||||
crate::CalTimezone::Local
|
NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(),
|
||||||
|
crate::CalTimezone::Local
|
||||||
|
),
|
||||||
|
true
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
CalDateTime::parse("1985-04-12", None).unwrap(),
|
CalDateTime::parse_vcard("1985-04-12").unwrap(),
|
||||||
CalDateTime::Date(
|
(
|
||||||
NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(),
|
CalDateTime::Date(
|
||||||
crate::CalTimezone::Local
|
NaiveDate::from_ymd_opt(1985, 4, 12).unwrap(),
|
||||||
|
crate::CalTimezone::Local
|
||||||
|
),
|
||||||
|
true
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
CalDateTime::parse("--0412", None).unwrap(),
|
CalDateTime::parse_vcard("--0412").unwrap(),
|
||||||
CalDateTime::Date(
|
(
|
||||||
NaiveDate::from_ymd_opt(1972, 4, 12).unwrap(),
|
CalDateTime::Date(
|
||||||
crate::CalTimezone::Local
|
NaiveDate::from_ymd_opt(1972, 4, 12).unwrap(),
|
||||||
|
crate::CalTimezone::Local
|
||||||
|
),
|
||||||
|
false
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,12 @@ pub struct CalendarQuery {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait CalendarStore: Send + Sync + 'static {
|
pub trait CalendarStore: Send + Sync + 'static {
|
||||||
async fn get_calendar(&self, principal: &str, id: &str) -> Result<Calendar, Error>;
|
async fn get_calendar(
|
||||||
|
&self,
|
||||||
|
principal: &str,
|
||||||
|
id: &str,
|
||||||
|
show_deleted: bool,
|
||||||
|
) -> Result<Calendar, Error>;
|
||||||
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error>;
|
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error>;
|
||||||
async fn get_deleted_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error>;
|
async fn get_deleted_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use derive_more::Constructor;
|
use derive_more::Constructor;
|
||||||
use rustical_ical::CalendarObject;
|
use rustical_ical::CalendarObject;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Calendar, CalendarStore, Error, calendar_store::CalendarQuery,
|
Calendar, CalendarStore, Error, calendar_store::CalendarQuery,
|
||||||
@@ -27,11 +26,20 @@ impl<CS: CalendarStore, BS: CalendarStore> Clone for CombinedCalendarStore<CS, B
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<CS: CalendarStore, BS: CalendarStore> CalendarStore for CombinedCalendarStore<CS, BS> {
|
impl<CS: CalendarStore, BS: CalendarStore> CalendarStore for CombinedCalendarStore<CS, BS> {
|
||||||
#[inline]
|
#[inline]
|
||||||
async fn get_calendar(&self, principal: &str, id: &str) -> Result<Calendar, Error> {
|
async fn get_calendar(
|
||||||
|
&self,
|
||||||
|
principal: &str,
|
||||||
|
id: &str,
|
||||||
|
show_deleted: bool,
|
||||||
|
) -> Result<Calendar, Error> {
|
||||||
if id.starts_with(BIRTHDAYS_PREFIX) {
|
if id.starts_with(BIRTHDAYS_PREFIX) {
|
||||||
self.birthday_store.get_calendar(principal, id).await
|
self.birthday_store
|
||||||
|
.get_calendar(principal, id, show_deleted)
|
||||||
|
.await
|
||||||
} else {
|
} else {
|
||||||
self.cal_store.get_calendar(principal, id).await
|
self.cal_store
|
||||||
|
.get_calendar(principal, id, show_deleted)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,11 +38,17 @@ fn birthday_calendar(addressbook: Addressbook) -> Calendar {
|
|||||||
/// Objects are all prefixed with BIRTHDAYS_PREFIX
|
/// Objects are all prefixed with BIRTHDAYS_PREFIX
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<AS: AddressbookStore> CalendarStore for ContactBirthdayStore<AS> {
|
impl<AS: AddressbookStore> CalendarStore for ContactBirthdayStore<AS> {
|
||||||
async fn get_calendar(&self, principal: &str, id: &str) -> Result<Calendar, Error> {
|
async fn get_calendar(
|
||||||
|
&self,
|
||||||
|
principal: &str,
|
||||||
|
id: &str,
|
||||||
|
show_deleted: bool,
|
||||||
|
) -> Result<Calendar, Error> {
|
||||||
let id = id.strip_prefix(BIRTHDAYS_PREFIX).ok_or(Error::NotFound)?;
|
let id = id.strip_prefix(BIRTHDAYS_PREFIX).ok_or(Error::NotFound)?;
|
||||||
let addressbook = self.0.get_addressbook(principal, id, false).await?;
|
let addressbook = self.0.get_addressbook(principal, id, show_deleted).await?;
|
||||||
Ok(birthday_calendar(addressbook))
|
Ok(birthday_calendar(addressbook))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
|
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
|
||||||
let addressbooks = self.0.get_addressbooks(principal).await?;
|
let addressbooks = self.0.get_addressbooks(principal).await?;
|
||||||
Ok(addressbooks.into_iter().map(birthday_calendar).collect())
|
Ok(addressbooks.into_iter().map(birthday_calendar).collect())
|
||||||
|
|||||||
@@ -86,14 +86,17 @@ impl SqliteCalendarStore {
|
|||||||
executor: E,
|
executor: E,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
id: &str,
|
id: &str,
|
||||||
|
show_deleted: bool,
|
||||||
) -> Result<Calendar, Error> {
|
) -> Result<Calendar, Error> {
|
||||||
let cal = sqlx::query_as!(
|
let cal = sqlx::query_as!(
|
||||||
CalendarRow,
|
CalendarRow,
|
||||||
r#"SELECT *
|
r#"SELECT *
|
||||||
FROM calendars
|
FROM calendars
|
||||||
WHERE (principal, id) = (?, ?)"#,
|
WHERE (principal, id) = (?, ?)
|
||||||
|
AND ((deleted_at IS NULL) OR ?) "#,
|
||||||
principal,
|
principal,
|
||||||
id
|
id,
|
||||||
|
show_deleted
|
||||||
)
|
)
|
||||||
.fetch_one(executor)
|
.fetch_one(executor)
|
||||||
.await
|
.await
|
||||||
@@ -470,8 +473,13 @@ impl SqliteCalendarStore {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl CalendarStore for SqliteCalendarStore {
|
impl CalendarStore for SqliteCalendarStore {
|
||||||
#[instrument]
|
#[instrument]
|
||||||
async fn get_calendar(&self, principal: &str, id: &str) -> Result<Calendar, Error> {
|
async fn get_calendar(
|
||||||
Self::_get_calendar(&self.db, principal, id).await
|
&self,
|
||||||
|
principal: &str,
|
||||||
|
id: &str,
|
||||||
|
show_deleted: bool,
|
||||||
|
) -> Result<Calendar, Error> {
|
||||||
|
Self::_get_calendar(&self.db, principal, id, show_deleted).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
@@ -509,7 +517,7 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
|
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
let cal = match Self::_get_calendar(&mut *tx, principal, id).await {
|
let cal = match Self::_get_calendar(&mut *tx, principal, id, true).await {
|
||||||
Ok(cal) => Some(cal),
|
Ok(cal) => Some(cal),
|
||||||
Err(Error::NotFound) => None,
|
Err(Error::NotFound) => None,
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
@@ -599,7 +607,10 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
|
|
||||||
if let Err(err) = self.sender.try_send(CollectionOperation {
|
if let Err(err) = self.sender.try_send(CollectionOperation {
|
||||||
data: CollectionOperationInfo::Content { sync_token },
|
data: CollectionOperationInfo::Content { sync_token },
|
||||||
topic: self.get_calendar(&principal, &cal_id).await?.push_topic,
|
topic: self
|
||||||
|
.get_calendar(&principal, &cal_id, true)
|
||||||
|
.await?
|
||||||
|
.push_topic,
|
||||||
}) {
|
}) {
|
||||||
error!("Push notification about deleted calendar failed: {err}");
|
error!("Push notification about deleted calendar failed: {err}");
|
||||||
};
|
};
|
||||||
@@ -624,7 +635,7 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
|
|
||||||
if let Err(err) = self.sender.try_send(CollectionOperation {
|
if let Err(err) = self.sender.try_send(CollectionOperation {
|
||||||
data: CollectionOperationInfo::Content { sync_token },
|
data: CollectionOperationInfo::Content { sync_token },
|
||||||
topic: self.get_calendar(principal, cal_id).await?.push_topic,
|
topic: self.get_calendar(principal, cal_id, true).await?.push_topic,
|
||||||
}) {
|
}) {
|
||||||
error!("Push notification about deleted calendar failed: {err}");
|
error!("Push notification about deleted calendar failed: {err}");
|
||||||
};
|
};
|
||||||
@@ -649,7 +660,7 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
|
|
||||||
if let Err(err) = self.sender.try_send(CollectionOperation {
|
if let Err(err) = self.sender.try_send(CollectionOperation {
|
||||||
data: CollectionOperationInfo::Content { sync_token },
|
data: CollectionOperationInfo::Content { sync_token },
|
||||||
topic: self.get_calendar(principal, cal_id).await?.push_topic,
|
topic: self.get_calendar(principal, cal_id, true).await?.push_topic,
|
||||||
}) {
|
}) {
|
||||||
error!("Push notification about deleted calendar failed: {err}");
|
error!("Push notification about deleted calendar failed: {err}");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ pub struct Config {
|
|||||||
pub data_store: DataStoreConfig,
|
pub data_store: DataStoreConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub http: HttpConfig,
|
pub http: HttpConfig,
|
||||||
|
#[serde(default)]
|
||||||
pub frontend: FrontendConfig,
|
pub frontend: FrontendConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub oidc: Option<OidcConfig>,
|
pub oidc: Option<OidcConfig>,
|
||||||
|
|||||||
Reference in New Issue
Block a user