Compare commits

..

27 Commits

Author SHA1 Message Date
Lennart
d2f5f7c89b version 0.11.0 2025-12-05 15:06:01 +01:00
Lennart Kämmle
15e431ce12 Merge pull request #138 from lennart-k/feature/birthday-calendar
Feature/birthday calendar
2025-12-05 15:03:56 +01:00
Lennart
96a16951f4 sqlx prepare 2025-12-05 14:55:30 +01:00
Lennart
a32b766c0c Merge branch 'main' into feature/birthday-calendar 2025-12-05 14:51:51 +01:00
Lennart
7a101b7364 frontend: Fix cursor for anchors 2025-12-05 14:51:34 +01:00
Lennart
57275a10b4 Add birthday calendar creation to frontend 2025-12-05 14:50:02 +01:00
Lennart
af239e34bf birthday calendar store: Support manual birthday calendar creation 2025-12-05 14:49:09 +01:00
Lennart
e99b1d9123 calendar resource: Remove prop write guards 2025-12-05 14:48:35 +01:00
Lennart
e39657eb29 PROPPATCH: Fix privileges 2025-12-05 14:48:11 +01:00
Lennart
607db62859 Merge branch 'main' into feature/birthday-calendar 2025-12-05 11:47:42 +01:00
Lennart
eba377b980 update dependencies 2025-12-05 11:47:11 +01:00
Lennart
d5c1ddc590 caldav: Update test_propfind regression test 2025-11-22 18:49:32 +01:00
Lennart
a79e1901b8 test_propfind: Revert assert_eq order 2025-11-22 18:48:36 +01:00
Lennart
f29c8fa925 Merge branch 'main' into feature/birthday-calendar 2025-11-22 18:46:59 +01:00
Lennart
54f1ee0788 use similar-asserts for regression tests 2025-11-22 18:46:47 +01:00
Lennart
96f221f721 birthday_calendar: Refactor insert_birthday_calendar 2025-11-22 18:35:26 +01:00
Lennart
ba3b64a9c4 Merge branch 'main' into feature/birthday-calendar 2025-11-22 18:30:44 +01:00
Lennart
873b40ad10 stylesheet: Add flex-wrap to actions 2025-11-05 16:05:55 +01:00
Lennart
5588137f73 sqlx prepare 2025-11-04 17:01:54 +01:00
Lennart
7bf00da0e5 implement deleting and restoring birthday calendars 2025-11-04 16:56:17 +01:00
Lennart
be08275cd3 Merge branch 'main' into feature/birthday-calendar 2025-11-04 16:28:08 +01:00
Lennart
381af1b877 run .sqlx prepare 2025-11-03 15:37:40 +01:00
Lennart
425d10cb99 CalendarStore::is_read_only now refers to its content only and not its metadata 2025-11-02 21:07:06 +01:00
Lennart
5cdbb3b9d3 migrate birthday store to sqlite 2025-11-02 21:06:43 +01:00
Lennart
547e477eca make sure a birthday calendar will be created for each addressbook 2025-11-02 21:05:31 +01:00
Lennart
c19c3492c3 frontend: Remove birthday calendar guard 2025-11-02 20:45:58 +01:00
Lennart
5878b93d62 add birthday_calendar table migrations 2025-11-02 20:45:31 +01:00
33 changed files with 1213 additions and 383 deletions

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE birthday_calendars SET principal = ?, id = ?, displayname = ?, description = ?, \"order\" = ?, color = ?, timezone_id = ?, push_topic = ?\n WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 10
},
"nullable": []
},
"hash": "4a05eda4e23e8652312548b179a1cc16f43768074ab9e7ab7b7783395384984e"
}

View File

@@ -0,0 +1,74 @@
{
"db_name": "SQLite",
"query": "SELECT principal, id, displayname, description, \"order\", color, timezone_id, deleted_at, addr_synctoken, push_topic\n FROM birthday_calendars\n INNER JOIN (\n SELECT principal AS addr_principal,\n id AS addr_id,\n synctoken AS addr_synctoken\n FROM addressbooks\n ) ON (principal, id) = (addr_principal, addr_id)\n WHERE (principal, id) = (?, ?)\n AND ((deleted_at IS NULL) OR ?)\n ",
"describe": {
"columns": [
{
"name": "principal",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "id",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "displayname",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "description",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "order",
"ordinal": 4,
"type_info": "Integer"
},
{
"name": "color",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "timezone_id",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "deleted_at",
"ordinal": 7,
"type_info": "Datetime"
},
{
"name": "addr_synctoken",
"ordinal": 8,
"type_info": "Integer"
},
{
"name": "push_topic",
"ordinal": 9,
"type_info": "Text"
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
true,
true,
false,
true,
true,
true,
false,
false
]
},
"hash": "525fc4eab8a0f3eacff7e3c78ce809943f817abf8c8f9ae50073924bccdea2dc"
}

View File

@@ -0,0 +1,74 @@
{
"db_name": "SQLite",
"query": "SELECT principal, id, displayname, description, \"order\", color, timezone_id, deleted_at, addr_synctoken, push_topic\n FROM birthday_calendars\n INNER JOIN (\n SELECT principal AS addr_principal,\n id AS addr_id,\n synctoken AS addr_synctoken\n FROM addressbooks\n ) ON (principal, id) = (addr_principal, addr_id)\n WHERE principal = ?\n AND (\n (deleted_at IS NULL AND NOT ?) -- not deleted, want not deleted\n OR (deleted_at IS NOT NULL AND ?) -- deleted, want deleted\n )\n ",
"describe": {
"columns": [
{
"name": "principal",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "id",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "displayname",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "description",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "order",
"ordinal": 4,
"type_info": "Integer"
},
{
"name": "color",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "timezone_id",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "deleted_at",
"ordinal": 7,
"type_info": "Datetime"
},
{
"name": "addr_synctoken",
"ordinal": 8,
"type_info": "Integer"
},
{
"name": "push_topic",
"ordinal": 9,
"type_info": "Text"
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
true,
true,
false,
true,
true,
true,
false,
false
]
},
"hash": "66d57f2c99ef37b383a478aff99110e1efbc7ce9332f10da4fa69f7594fb7455"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE birthday_calendars SET deleted_at = NULL WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "6c039308ad2ec29570ab492d7a0e85fb79c0a4d3b882b74ff1c2786c12324896"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO birthday_calendars (principal, id, displayname, description, \"order\", color, push_topic)\n VALUES (?, ?, ?, ?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "72c7c67f4952ad669ecd54d96bbcb717815081f74575f0a65987163faf9fe30a"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE birthday_calendars SET deleted_at = datetime() WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "83f0aaf406785e323ac12019ac24f603c53125a1b2326f324c1e2d7b6c690adc"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "DELETE FROM birthday_calendars WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "cadc4ac16b7ac22b71c91ab36ad9dbb1dec943798d795fcbc811f4c651fea02a"
}

367
Cargo.lock generated
View File

@@ -139,7 +139,7 @@ dependencies = [
"rustc-hash", "rustc-hash",
"serde", "serde",
"serde_derive", "serde_derive",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -175,7 +175,7 @@ checksum = "34921de3d57974069bad483fdfe0ec65d88c4ff892edd1ab4d8b03be0dda1b9b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -310,7 +310,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -505,6 +505,17 @@ dependencies = [
"piper", "piper",
] ]
[[package]]
name = "bstr"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
dependencies = [
"memchr",
"regex-automata",
"serde",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.19.0" version = "3.19.0"
@@ -531,9 +542,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.46" version = "1.2.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"shlex", "shlex",
@@ -586,9 +597,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.52" version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa8120877db0e5c011242f96806ce3c94e0737ab8108532a76a3300a01db2ab8" checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -596,9 +607,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.52" version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02576b399397b659c26064fbc92a75fede9d18ffd5f80ca1cd74ddab167016e1" checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@@ -615,7 +626,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -639,12 +650,33 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "console"
version = "0.15.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "const-oid" name = "const-oid"
version = "0.9.6" version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "convert_case"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "cookie" name = "cookie"
version = "0.18.1" version = "0.18.1"
@@ -673,9 +705,9 @@ dependencies = [
[[package]] [[package]]
name = "crc" name = "crc"
version = "3.3.0" version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
dependencies = [ dependencies = [
"crc-catalog", "crc-catalog",
] ]
@@ -747,7 +779,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -756,8 +788,18 @@ version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
dependencies = [ dependencies = [
"darling_core", "darling_core 0.21.3",
"darling_macro", "darling_macro 0.21.3",
]
[[package]]
name = "darling"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
dependencies = [
"darling_core 0.23.0",
"darling_macro 0.23.0",
] ]
[[package]] [[package]]
@@ -771,7 +813,20 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim", "strsim",
"syn 2.0.110", "syn 2.0.111",
]
[[package]]
name = "darling_core"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.111",
] ]
[[package]] [[package]]
@@ -780,9 +835,20 @@ version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
dependencies = [ dependencies = [
"darling_core", "darling_core 0.21.3",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
]
[[package]]
name = "darling_macro"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
dependencies = [
"darling_core 0.23.0",
"quote",
"syn 2.0.111",
] ]
[[package]] [[package]]
@@ -808,22 +874,24 @@ dependencies = [
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "2.0.1" version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618"
dependencies = [ dependencies = [
"derive_more-impl", "derive_more-impl",
] ]
[[package]] [[package]]
name = "derive_more-impl" name = "derive_more-impl"
version = "2.0.1" version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b"
dependencies = [ dependencies = [
"convert_case",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "rustc_version",
"syn 2.0.111",
"unicode-xid", "unicode-xid",
] ]
@@ -847,7 +915,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -947,6 +1015,12 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.35" version = "0.8.35"
@@ -1178,7 +1252,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -1301,7 +1375,7 @@ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"http", "http",
"indexmap 2.12.0", "indexmap 2.12.1",
"slab", "slab",
"tokio", "tokio",
"tokio-util", "tokio-util",
@@ -1327,9 +1401,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.16.0" version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]] [[package]]
name = "hashlink" name = "hashlink"
@@ -1414,12 +1488,11 @@ dependencies = [
[[package]] [[package]]
name = "http" name = "http"
version = "1.3.1" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv",
"itoa", "itoa",
] ]
@@ -1519,9 +1592,9 @@ dependencies = [
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.18" version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
@@ -1700,12 +1773,12 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.12.0" version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.16.0", "hashbrown 0.16.1",
"serde", "serde",
"serde_core", "serde_core",
] ]
@@ -1764,9 +1837,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.82" version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"wasm-bindgen", "wasm-bindgen",
@@ -1792,9 +1865,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.177" version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]] [[package]]
name = "libm" name = "libm"
@@ -1848,9 +1921,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.28" version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
dependencies = [ dependencies = [
"value-bag", "value-bag",
] ]
@@ -1937,9 +2010,9 @@ dependencies = [
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.1.0" version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [ dependencies = [
"libc", "libc",
"wasi", "wasi",
@@ -2102,7 +2175,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -2327,7 +2400,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"proc-macro2-diagnostics", "proc-macro2-diagnostics",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -2400,7 +2473,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -2526,7 +2599,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
"version_check", "version_check",
"yansi", "yansi",
] ]
@@ -2551,7 +2624,7 @@ dependencies = [
"itertools 0.14.0", "itertools 0.14.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -2718,7 +2791,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -2892,7 +2965,7 @@ dependencies = [
"regex", "regex",
"relative-path", "relative-path",
"rustc_version", "rustc_version",
"syn 2.0.110", "syn 2.0.111",
"unicode-ident", "unicode-ident",
] ]
@@ -2904,7 +2977,7 @@ checksum = "b3a8fb4672e840a587a66fc577a5491375df51ddb88f2a2c2a792598c326fe14"
dependencies = [ dependencies = [
"quote", "quote",
"rand 0.8.5", "rand 0.8.5",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -2937,7 +3010,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rust-embed-utils", "rust-embed-utils",
"syn 2.0.110", "syn 2.0.111",
"walkdir", "walkdir",
] ]
@@ -2974,7 +3047,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical" name = "rustical"
version = "0.10.5" version = "0.11.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@@ -3017,7 +3090,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_caldav" name = "rustical_caldav"
version = "0.10.5" version = "0.11.0"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-trait", "async-trait",
@@ -3043,6 +3116,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
"similar-asserts",
"strum", "strum",
"strum_macros", "strum_macros",
"thiserror 2.0.17", "thiserror 2.0.17",
@@ -3057,7 +3131,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_carddav" name = "rustical_carddav"
version = "0.10.5" version = "0.11.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3089,7 +3163,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav" name = "rustical_dav"
version = "0.10.5" version = "0.11.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3114,7 +3188,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav_push" name = "rustical_dav_push"
version = "0.10.5" version = "0.11.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3139,7 +3213,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_frontend" name = "rustical_frontend"
version = "0.10.5" version = "0.11.0"
dependencies = [ dependencies = [
"askama", "askama",
"askama_web", "askama_web",
@@ -3175,7 +3249,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_ical" name = "rustical_ical"
version = "0.10.5" version = "0.11.0"
dependencies = [ dependencies = [
"axum", "axum",
"chrono", "chrono",
@@ -3192,7 +3266,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_oidc" name = "rustical_oidc"
version = "0.10.5" version = "0.11.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3208,7 +3282,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store" name = "rustical_store"
version = "0.10.5" version = "0.11.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -3241,7 +3315,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store_sqlite" name = "rustical_store_sqlite"
version = "0.10.5" version = "0.11.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
@@ -3253,6 +3327,7 @@ dependencies = [
"rustical_ical", "rustical_ical",
"rustical_store", "rustical_store",
"serde", "serde",
"sha2",
"sqlx", "sqlx",
"thiserror 2.0.17", "thiserror 2.0.17",
"tokio", "tokio",
@@ -3262,7 +3337,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_xml" name = "rustical_xml"
version = "0.10.5" version = "0.11.0"
dependencies = [ dependencies = [
"quick-xml", "quick-xml",
"thiserror 2.0.17", "thiserror 2.0.17",
@@ -3298,9 +3373,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.13.0" version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c"
dependencies = [ dependencies = [
"web-time", "web-time",
"zeroize", "zeroize",
@@ -3425,7 +3500,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -3493,15 +3568,15 @@ dependencies = [
[[package]] [[package]]
name = "serde_with" name = "serde_with"
version = "3.16.0" version = "3.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"chrono", "chrono",
"hex", "hex",
"indexmap 1.9.3", "indexmap 1.9.3",
"indexmap 2.12.0", "indexmap 2.12.1",
"schemars 0.9.0", "schemars 0.9.0",
"schemars 1.1.0", "schemars 1.1.0",
"serde_core", "serde_core",
@@ -3512,14 +3587,14 @@ dependencies = [
[[package]] [[package]]
name = "serde_with_macros" name = "serde_with_macros"
version = "3.16.0" version = "3.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c"
dependencies = [ dependencies = [
"darling", "darling 0.21.3",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -3561,9 +3636,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.6" version = "1.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@@ -3578,6 +3653,26 @@ dependencies = [
"rand_core 0.6.4", "rand_core 0.6.4",
] ]
[[package]]
name = "similar"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
dependencies = [
"bstr",
"unicode-segmentation",
]
[[package]]
name = "similar-asserts"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b441962c817e33508847a22bd82f03a30cff43642dc2fae8b050566121eb9a"
dependencies = [
"console",
"similar",
]
[[package]] [[package]]
name = "siphasher" name = "siphasher"
version = "1.0.1" version = "1.0.1"
@@ -3660,7 +3755,7 @@ dependencies = [
"futures-util", "futures-util",
"hashbrown 0.15.5", "hashbrown 0.15.5",
"hashlink", "hashlink",
"indexmap 2.12.0", "indexmap 2.12.1",
"log", "log",
"memchr", "memchr",
"once_cell", "once_cell",
@@ -3687,7 +3782,7 @@ dependencies = [
"quote", "quote",
"sqlx-core", "sqlx-core",
"sqlx-macros-core", "sqlx-macros-core",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -3710,7 +3805,7 @@ dependencies = [
"sqlx-mysql", "sqlx-mysql",
"sqlx-postgres", "sqlx-postgres",
"sqlx-sqlite", "sqlx-sqlite",
"syn 2.0.110", "syn 2.0.111",
"tokio", "tokio",
"url", "url",
] ]
@@ -3862,7 +3957,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -3884,9 +3979,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.110" version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -3910,7 +4005,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -3939,7 +4034,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -3950,7 +4045,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -4044,7 +4139,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -4099,7 +4194,7 @@ version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
dependencies = [ dependencies = [
"indexmap 2.12.0", "indexmap 2.12.1",
"serde_core", "serde_core",
"serde_spanned 1.0.3", "serde_spanned 1.0.3",
"toml_datetime 0.7.3", "toml_datetime 0.7.3",
@@ -4132,7 +4227,7 @@ version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [ dependencies = [
"indexmap 2.12.0", "indexmap 2.12.1",
"serde", "serde",
"serde_spanned 0.6.9", "serde_spanned 0.6.9",
"toml_datetime 0.6.11", "toml_datetime 0.6.11",
@@ -4146,7 +4241,7 @@ version = "0.23.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d"
dependencies = [ dependencies = [
"indexmap 2.12.0", "indexmap 2.12.1",
"toml_datetime 0.7.3", "toml_datetime 0.7.3",
"toml_parser", "toml_parser",
"winnow", "winnow",
@@ -4218,7 +4313,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-util", "futures-util",
"indexmap 2.12.0", "indexmap 2.12.1",
"pin-project-lite", "pin-project-lite",
"slab", "slab",
"sync_wrapper", "sync_wrapper",
@@ -4247,9 +4342,9 @@ dependencies = [
[[package]] [[package]]
name = "tower-http" name = "tower-http"
version = "0.6.6" version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"bytes", "bytes",
@@ -4338,9 +4433,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.41" version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
dependencies = [ dependencies = [
"log", "log",
"pin-project-lite", "pin-project-lite",
@@ -4350,20 +4445,20 @@ dependencies = [
[[package]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.30" version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.34" version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"valuable", "valuable",
@@ -4401,9 +4496,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-subscriber" name = "tracing-subscriber"
version = "0.3.20" version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [ dependencies = [
"matchers", "matchers",
"nu-ansi-term", "nu-ansi-term",
@@ -4471,6 +4566,12 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.6" version = "0.2.6"
@@ -4509,9 +4610,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.18.1" version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
dependencies = [ dependencies = [
"getrandom 0.3.4", "getrandom 0.3.4",
"js-sys", "js-sys",
@@ -4527,9 +4628,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "value-bag" name = "value-bag"
version = "1.11.1" version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0"
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
@@ -4596,9 +4697,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.105" version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -4609,9 +4710,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.55" version = "0.4.56"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
@@ -4622,9 +4723,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.105" version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -4632,31 +4733,31 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.105" version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.105" version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.82" version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@@ -4721,7 +4822,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -4732,7 +4833,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -4992,9 +5093,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.13" version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -5013,14 +5114,14 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]] [[package]]
name = "xml_derive" name = "xml_derive"
version = "0.10.5" version = "0.11.0"
dependencies = [ dependencies = [
"darling", "darling 0.23.0",
"heck", "heck",
"itertools 0.14.0", "itertools 0.14.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -5048,28 +5149,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
"synstructure", "synstructure",
] ]
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.27" version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.27" version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]
[[package]] [[package]]
@@ -5089,7 +5190,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
"synstructure", "synstructure",
] ]
@@ -5129,5 +5230,5 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.110", "syn 2.0.111",
] ]

View File

@@ -2,7 +2,7 @@
members = ["crates/*"] members = ["crates/*"]
[workspace.package] [workspace.package]
version = "0.10.5" version = "0.11.0"
rust-version = "1.91" rust-version = "1.91"
edition = "2024" edition = "2024"
description = "A CalDAV server" description = "A CalDAV server"
@@ -133,7 +133,7 @@ syn = { version = "2.0", features = ["full"] }
quote = "1.0" quote = "1.0"
proc-macro2 = "1.0" proc-macro2 = "1.0"
heck = "0.5" heck = "0.5"
darling = "0.21" darling = "0.23"
reqwest = { version = "0.12", features = [ reqwest = { version = "0.12", features = [
"rustls-tls", "rustls-tls",
"charset", "charset",
@@ -148,6 +148,7 @@ ece = { version = "2.3", default-features = false, features = [
] } ] }
openssl = { version = "0.10", features = ["vendored"] } openssl = { version = "0.10", features = ["vendored"] }
async-std = { version = "1.13", features = ["attributes"] } async-std = { version = "1.13", features = ["attributes"] }
similar-asserts = "1.7"
[dependencies] [dependencies]
rustical_store.workspace = true rustical_store.workspace = true

View File

@@ -45,3 +45,4 @@ tower-http.workspace = true
strum.workspace = true strum.workspace = true
strum_macros.workspace = true strum_macros.workspace = true
vtimezones-rs.workspace = true vtimezones-rs.workspace = true
similar-asserts.workspace = true

View File

@@ -188,9 +188,6 @@ impl Resource for CalendarResource {
} }
fn set_prop(&mut self, prop: Self::Prop) -> Result<(), rustical_dav::Error> { fn set_prop(&mut self, prop: Self::Prop) -> Result<(), rustical_dav::Error> {
if self.read_only {
return Err(rustical_dav::Error::PropReadOnly);
}
match prop { match prop {
CalendarPropWrapper::Calendar(prop) => match prop { CalendarPropWrapper::Calendar(prop) => match prop {
CalendarProp::CalendarColor(color) => { CalendarProp::CalendarColor(color) => {
@@ -263,9 +260,6 @@ impl Resource for CalendarResource {
} }
fn remove_prop(&mut self, prop: &CalendarPropWrapperName) -> Result<(), rustical_dav::Error> { fn remove_prop(&mut self, prop: &CalendarPropWrapperName) -> Result<(), rustical_dav::Error> {
if self.read_only {
return Err(rustical_dav::Error::PropReadOnly);
}
match prop { match prop {
CalendarPropWrapperName::Calendar(prop) => match prop { CalendarPropWrapperName::Calendar(prop) => match prop {
CalendarPropName::CalendarColor => { CalendarPropName::CalendarColor => {
@@ -317,16 +311,11 @@ 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() { if self.cal.subscription_url.is_some() || self.read_only {
return Ok(UserPrivilegeSet::owner_write_properties( return Ok(UserPrivilegeSet::owner_write_properties(
user.is_principal(&self.cal.principal), user.is_principal(&self.cal.principal),
)); ));
} }
if self.read_only {
return Ok(UserPrivilegeSet::owner_read(
user.is_principal(&self.cal.principal),
));
}
Ok(UserPrivilegeSet::owner_only( Ok(UserPrivilegeSet::owner_only(
user.is_principal(&self.cal.principal), user.is_principal(&self.cal.principal),

View File

@@ -211,6 +211,9 @@ END:VCALENDAR
<privilege> <privilege>
<read/> <read/>
</privilege> </privilege>
<privilege>
<write-properties/>
</privilege>
<privilege> <privilege>
<read-acl/> <read-acl/>
</privilege> </privilege>

View File

@@ -39,9 +39,7 @@ async fn test_propfind() {
.unwrap() .unwrap()
.trim() .trim()
.replace("\r\n", "\n"); .replace("\r\n", "\n");
println!("{output}"); similar_asserts::assert_eq!(expected_output, output);
println!("{}, {} \n\n\n", output.len(), expected_output.len());
assert_eq!(output, expected_output);
} }
} }
} }

View File

@@ -88,7 +88,7 @@ pub async fn route_proppatch<R: ResourceService>(
.get_resource(path_components, false) .get_resource(path_components, false)
.await?; .await?;
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());
} }

View File

@@ -0,0 +1,102 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { escapeXml } from ".";
import { getTimezones } from "./timezones.ts";
@customElement("create-birthday-calendar-form")
export class CreateCalendarForm extends LitElement {
protected override createRenderRoot() {
return this
}
@property()
principal: string = ''
@property()
addr_id: string = ''
@property()
displayname: string = ''
@property()
description: string = ''
@property()
color: string = ''
dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef()
@property()
timezones: Array<String> = []
override render() {
return html`
<button @click=${() => this.dialog.value.showModal()}>Create birthday calendar</button>
<dialog ${ref(this.dialog)}>
<h3>Create calendar</h3>
<form @submit=${this.submit} ${ref(this.form)}>
<label>
Displayname
<input type="text" name="displayname" value=${this.displayname} @change=${e => this.displayname = e.target.value} />
</label>
<br>
<label>
Description
<input type="text" name="description" @change=${e => this.description = e.target.value} />
</label>
<br>
<label>
Color
<input type="color" name="color" @change=${e => this.color = e.target.value} />
</label>
<br>
<button type="submit">Create</button>
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
</form>
</dialog>
`
}
async submit(e: SubmitEvent) {
e.preventDefault()
if (!this.addr_id) {
alert("Empty id")
return
}
if (!this.displayname) {
alert("Empty displayname")
return
}
let response = await fetch(`/caldav/principal/${this.principal}/_birthdays_${this.addr_id}`, {
method: 'MKCOL',
headers: {
'Content-Type': 'application/xml'
},
body: `
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
<set>
<prop>
<displayname>${escapeXml(this.displayname)}</displayname>
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ''}
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ''}
<CAL:supported-calendar-component-set>
<CAL:comp name="VEVENT" />
</CAL:supported-calendar-component-set>
</prop>
</set>
</mkcol>
`
})
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`)
return null
}
window.location.reload()
return null
}
}
declare global {
interface HTMLElementTagNameMap {
'create-calendar-form': CreateCalendarForm
}
}

View File

@@ -14,6 +14,7 @@ export default defineConfig({
rollupOptions: { rollupOptions: {
input: [ input: [
"lib/create-birthday-calendar-form.ts",
"lib/create-calendar-form.ts", "lib/create-calendar-form.ts",
"lib/edit-calendar-form.ts", "lib/edit-calendar-form.ts",
"lib/import-calendar-form.ts", "lib/import-calendar-form.ts",

View File

@@ -0,0 +1,122 @@
import { i, x } from "./lit-DkXrt_Iv.mjs";
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
import { e, n } from "./ref-BwbQvJBB.mjs";
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
if (decorator = decorators[i2])
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
if (kind && result) __defProp(target, key, result);
return result;
};
let CreateCalendarForm = class extends i {
constructor() {
super(...arguments);
this.principal = "";
this.addr_id = "";
this.displayname = "";
this.description = "";
this.color = "";
this.dialog = e();
this.form = e();
this.timezones = [];
}
createRenderRoot() {
return this;
}
render() {
return x`
<button @click=${() => this.dialog.value.showModal()}>Create birthday calendar</button>
<dialog ${n(this.dialog)}>
<h3>Create calendar</h3>
<form @submit=${this.submit} ${n(this.form)}>
<label>
Displayname
<input type="text" name="displayname" value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
</label>
<br>
<label>
Description
<input type="text" name="description" @change=${(e2) => this.description = e2.target.value} />
</label>
<br>
<label>
Color
<input type="color" name="color" @change=${(e2) => this.color = e2.target.value} />
</label>
<br>
<button type="submit">Create</button>
<button type="submit" @click=${(event) => {
event.preventDefault();
this.dialog.value.close();
this.form.value.reset();
}} class="cancel">Cancel</button>
</form>
</dialog>
`;
}
async submit(e2) {
e2.preventDefault();
if (!this.addr_id) {
alert("Empty id");
return;
}
if (!this.displayname) {
alert("Empty displayname");
return;
}
let response = await fetch(`/caldav/principal/${this.principal}/_birthdays_${this.addr_id}`, {
method: "MKCOL",
headers: {
"Content-Type": "application/xml"
},
body: `
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
<set>
<prop>
<displayname>${escapeXml(this.displayname)}</displayname>
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ""}
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ""}
<CAL:supported-calendar-component-set>
<CAL:comp name="VEVENT" />
</CAL:supported-calendar-component-set>
</prop>
</set>
</mkcol>
`
});
if (response.status >= 400) {
alert(`Error ${response.status}: ${await response.text()}`);
return null;
}
window.location.reload();
return null;
}
};
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "principal", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "addr_id", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "displayname", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "description", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "color", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "timezones", 2);
CreateCalendarForm = __decorateClass([
t("create-birthday-calendar-form")
], CreateCalendarForm);
export {
CreateCalendarForm
};

View File

@@ -53,6 +53,11 @@ a {
color: var(--text-on-background-color); color: var(--text-on-background-color);
} }
a,
button {
cursor: pointer;
}
header { header {
background: var(--background-darker); background: var(--background-darker);
min-height: 60px; min-height: 60px;
@@ -239,10 +244,8 @@ ul.collection-list {
z-index: 1; z-index: 1;
pointer-events: none; pointer-events: none;
a,
button { button {
pointer-events: all; pointer-events: all;
cursor: pointer;
} }
.title { .title {
@@ -282,6 +285,7 @@ ul.collection-list {
grid-area: actions; grid-area: actions;
width: fit-content; width: fit-content;
display: flex; display: flex;
flex-wrap: wrap;
gap: 12px; gap: 12px;
} }
} }

View File

@@ -1,6 +1,6 @@
<h2>{{user.id }}'s Addressbooks</h2> <h2>{{user.id }}'s Addressbooks</h2>
<ul class="collection-list"> <ul class="collection-list">
{% for (meta, addressbook) in addressbooks %} {% for (meta, birthday_cal, addressbook) in addressbooks %}
<li class="collection-list-item"> <li class="collection-list-item">
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"></a> <a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"></a>
<div class="inner"> <div class="inner">
@@ -24,9 +24,17 @@
></edit-addressbook-form> ></edit-addressbook-form>
<delete-button trash <delete-button trash
href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button> href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
{% if !birthday_cal.is_some() %}
<create-birthday-calendar-form
principal="{{ addressbook.principal }}"
addr_id="{{ addressbook.id }}"
displayname="{{ addressbook.displayname.as_deref().unwrap_or_default() }} birthdays"
></create-birthday-calendar-form>
{% endif %}
</div> </div>
<div class="metadata"> <div class="metadata">
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects {{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
{{ birthday_cal.is_some() }}
</div> </div>
</div> </div>
</li> </li>
@@ -37,7 +45,7 @@
{%if !deleted_addressbooks.is_empty() %} {%if !deleted_addressbooks.is_empty() %}
<h3>Deleted Addressbooks</h3> <h3>Deleted Addressbooks</h3>
<ul class="collection-list"> <ul class="collection-list">
{% for (meta, addressbook) in deleted_addressbooks %} {% for (meta, birthday_cal, addressbook) in deleted_addressbooks %}
<li class="collection-list-item"> <li class="collection-list-item">
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"></a> <a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"></a>
<div class="inner"> <div class="inner">

View File

@@ -24,7 +24,6 @@
<form action="/caldav/principal/{{ calendar.principal }}/{{ 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>
{% if !calendar.id.starts_with("_birthdays_") %}
<edit-calendar-form <edit-calendar-form
principal="{{ calendar.principal }}" principal="{{ calendar.principal }}"
cal_id="{{ calendar.id }}" cal_id="{{ calendar.id }}"
@@ -35,7 +34,6 @@
components="{{ calendar.components | json }}" components="{{ calendar.components | json }}"
></edit-calendar-form> ></edit-calendar-form>
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button> <delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
{% endif %}
</div> </div>
<div class="metadata"> <div class="metadata">
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects {{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects

View File

@@ -5,6 +5,7 @@
<script> <script>
window.rusticalUser = JSON.parse(document.querySelector('#data-rustical-user').innerHTML) window.rusticalUser = JSON.parse(document.querySelector('#data-rustical-user').innerHTML)
</script> </script>
<script type="module" src="/frontend/assets/js/create-birthday-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-calendar-form.mjs" async></script>
<script type="module" src="/frontend/assets/js/edit-calendar-form.mjs" async></script> <script type="module" src="/frontend/assets/js/edit-calendar-form.mjs" async></script>
<script type="module" src="/frontend/assets/js/import-calendar-form.mjs" async></script> <script type="module" src="/frontend/assets/js/import-calendar-form.mjs" async></script>

View File

@@ -12,7 +12,7 @@ use http::{Method, StatusCode};
use routes::{addressbooks::route_addressbooks, calendars::route_calendars}; use routes::{addressbooks::route_addressbooks, calendars::route_calendars};
use rustical_oidc::{OidcConfig, OidcServiceConfig, route_get_oidc_callback, route_post_oidc}; use rustical_oidc::{OidcConfig, OidcServiceConfig, route_get_oidc_callback, route_post_oidc};
use rustical_store::{ use rustical_store::{
AddressbookStore, CalendarStore, AddressbookStore, CalendarStore, PrefixedCalendarStore,
auth::{AuthenticationProvider, middleware::AuthenticationLayer}, auth::{AuthenticationProvider, middleware::AuthenticationLayer},
}; };
use std::sync::Arc; use std::sync::Arc;
@@ -39,7 +39,11 @@ use crate::routes::{
#[cfg(not(feature = "dev"))] #[cfg(not(feature = "dev"))]
use assets::{Assets, EmbedService}; use assets::{Assets, EmbedService};
pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: AddressbookStore>( pub fn frontend_router<
AP: AuthenticationProvider,
CS: CalendarStore,
AS: AddressbookStore + PrefixedCalendarStore,
>(
prefix: &'static str, prefix: &'static str,
auth_provider: Arc<AP>, auth_provider: Arc<AP>,
cal_store: Arc<CS>, cal_store: Arc<CS>,

View File

@@ -1,10 +1,12 @@
use std::sync::Arc;
use askama::Template; use askama::Template;
use askama_web::WebTemplate; use askama_web::WebTemplate;
use axum::{Extension, extract::Path, response::IntoResponse}; use axum::{Extension, extract::Path, response::IntoResponse};
use http::StatusCode; use http::StatusCode;
use rustical_store::{Addressbook, AddressbookStore, CollectionMetadata, auth::Principal}; use rustical_store::{
Addressbook, AddressbookStore, Calendar, CollectionMetadata, PrefixedCalendarStore,
auth::Principal,
};
use std::sync::Arc;
use crate::pages::user::{Section, UserPage}; use crate::pages::user::{Section, UserPage};
@@ -18,11 +20,11 @@ impl Section for AddressbooksSection {
#[template(path = "components/sections/addressbooks_section.html")] #[template(path = "components/sections/addressbooks_section.html")]
pub struct AddressbooksSection { pub struct AddressbooksSection {
pub user: Principal, pub user: Principal,
pub addressbooks: Vec<(CollectionMetadata, Addressbook)>, pub addressbooks: Vec<(CollectionMetadata, Option<Calendar>, Addressbook)>,
pub deleted_addressbooks: Vec<(CollectionMetadata, Addressbook)>, pub deleted_addressbooks: Vec<(CollectionMetadata, Option<Calendar>, Addressbook)>,
} }
pub async fn route_addressbooks<AS: AddressbookStore>( pub async fn route_addressbooks<AS: AddressbookStore + PrefixedCalendarStore>(
Path(user_id): Path<String>, Path(user_id): Path<String>,
Extension(addr_store): Extension<Arc<AS>>, Extension(addr_store): Extension<Arc<AS>>,
user: Principal, user: Principal,
@@ -43,22 +45,42 @@ pub async fn route_addressbooks<AS: AddressbookStore>(
let mut addressbook_infos = vec![]; let mut addressbook_infos = vec![];
for addressbook in addressbooks { for addressbook in addressbooks {
let birthday_id = format!("{}{}", AS::PREFIX, &addressbook.id);
let birthday_cal = match addr_store
.get_calendar(&addressbook.principal, &birthday_id, true)
.await
{
Ok(cal) => Some(cal),
Err(rustical_store::Error::NotFound) => None,
err => Some(err.unwrap()),
};
addressbook_infos.push(( addressbook_infos.push((
addr_store addr_store
.addressbook_metadata(&addressbook.principal, &addressbook.id) .addressbook_metadata(&addressbook.principal, &addressbook.id)
.await .await
.unwrap(), .unwrap(),
birthday_cal,
addressbook, addressbook,
)); ));
} }
let mut deleted_addressbook_infos = vec![]; let mut deleted_addressbook_infos = vec![];
for addressbook in deleted_addressbooks { for addressbook in deleted_addressbooks {
let birthday_id = format!("{}{}", AS::PREFIX, &addressbook.id);
let birthday_cal = match addr_store
.get_calendar(&addressbook.principal, &birthday_id, true)
.await
{
Ok(cal) => Some(cal),
Err(rustical_store::Error::NotFound) => None,
err => Some(err.unwrap()),
};
deleted_addressbook_infos.push(( deleted_addressbook_infos.push((
addr_store addr_store
.addressbook_metadata(&addressbook.principal, &addressbook.id) .addressbook_metadata(&addressbook.principal, &addressbook.id)
.await .await
.unwrap(), .unwrap(),
birthday_cal,
addressbook, addressbook,
)); ));
} }

View File

@@ -98,5 +98,6 @@ pub trait CalendarStore: Send + Sync + 'static {
object_id: &str, object_id: &str,
) -> Result<(), Error>; ) -> Result<(), Error>;
// read_only refers to objects, metadata may still be updated
fn is_read_only(&self, cal_id: &str) -> bool; fn is_read_only(&self, cal_id: &str) -> bool;
} }

View File

@@ -1,209 +0,0 @@
use crate::{
Addressbook, AddressbookStore, Calendar, CalendarStore, Error, calendar::CalendarMetadata,
combined_calendar_store::PrefixedCalendarStore,
};
use async_trait::async_trait;
use derive_more::derive::Constructor;
use rustical_ical::{AddressObject, CalendarObject, CalendarObjectType};
use sha2::{Digest, Sha256};
use std::{collections::HashMap, sync::Arc};
pub const BIRTHDAYS_PREFIX: &str = "_birthdays_";
#[derive(Constructor, Clone)]
pub struct ContactBirthdayStore<AS: AddressbookStore>(Arc<AS>);
impl<AS: AddressbookStore> PrefixedCalendarStore for ContactBirthdayStore<AS> {
const PREFIX: &'static str = BIRTHDAYS_PREFIX;
}
fn birthday_calendar(addressbook: Addressbook) -> Calendar {
Calendar {
principal: addressbook.principal,
id: format!("{}{}", BIRTHDAYS_PREFIX, addressbook.id),
meta: CalendarMetadata {
displayname: addressbook
.displayname
.map(|name| format!("{name} birthdays")),
order: 0,
description: None,
color: None,
},
timezone_id: None,
deleted_at: addressbook.deleted_at,
synctoken: addressbook.synctoken,
subscription_url: None,
push_topic: {
let mut hasher = Sha256::new();
hasher.update("birthdays");
hasher.update(addressbook.push_topic);
format!("{:x}", hasher.finalize())
},
components: vec![CalendarObjectType::Event],
}
}
/// Objects are all prefixed with `BIRTHDAYS_PREFIX`
#[async_trait]
impl<AS: AddressbookStore> CalendarStore for ContactBirthdayStore<AS> {
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 addressbook = self.0.get_addressbook(principal, id, show_deleted).await?;
Ok(birthday_calendar(addressbook))
}
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
let addressbooks = self.0.get_addressbooks(principal).await?;
Ok(addressbooks.into_iter().map(birthday_calendar).collect())
}
async fn get_deleted_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
let addressbooks = self.0.get_deleted_addressbooks(principal).await?;
Ok(addressbooks.into_iter().map(birthday_calendar).collect())
}
async fn update_calendar(
&self,
_principal: String,
_id: String,
_calendar: Calendar,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
async fn insert_calendar(&self, _calendar: Calendar) -> Result<(), Error> {
Err(Error::ReadOnly)
}
async fn delete_calendar(
&self,
_principal: &str,
_name: &str,
_use_trashbin: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
async fn restore_calendar(&self, _principal: &str, _name: &str) -> Result<(), Error> {
Err(Error::ReadOnly)
}
async fn import_calendar(
&self,
_calendar: Calendar,
_objects: Vec<CalendarObject>,
_merge_existing: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
async fn sync_changes(
&self,
principal: &str,
cal_id: &str,
synctoken: i64,
) -> Result<(Vec<CalendarObject>, Vec<String>, 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<Vec<Option<CalendarObject>>, rustical_ical::Error> = objects
.iter()
.map(AddressObject::get_birthday_object)
.collect();
let objects = objects?.into_iter().flatten().collect();
Ok((objects, deleted_objects, new_synctoken))
}
async fn calendar_metadata(
&self,
principal: &str,
cal_id: &str,
) -> Result<crate::CollectionMetadata, Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
self.0.addressbook_metadata(principal, cal_id).await
}
async fn get_objects(
&self,
principal: &str,
cal_id: &str,
) -> Result<Vec<CalendarObject>, Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
let objects: Result<Vec<HashMap<&'static str, CalendarObject>>, rustical_ical::Error> =
self.0
.get_objects(principal, cal_id)
.await?
.iter()
.map(AddressObject::get_significant_dates)
.collect();
let objects = objects?
.into_iter()
.flat_map(HashMap::into_values)
.collect();
Ok(objects)
}
async fn get_object(
&self,
principal: &str,
cal_id: &str,
object_id: &str,
show_deleted: bool,
) -> Result<CalendarObject, Error> {
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, show_deleted)
.await?
.get_significant_dates()?
.remove(date_type)
.ok_or(Error::NotFound)
}
async fn put_object(
&self,
_principal: String,
_cal_id: String,
_object: CalendarObject,
_overwrite: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
async fn delete_object(
&self,
_principal: &str,
_cal_id: &str,
_object_id: &str,
_use_trashbin: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
async fn restore_object(
&self,
_principal: &str,
_cal_id: &str,
_object_id: &str,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
fn is_read_only(&self, _cal_id: &str) -> bool {
true
}
}

View File

@@ -7,7 +7,6 @@ pub use error::Error;
pub mod auth; pub mod auth;
mod calendar; mod calendar;
mod combined_calendar_store; mod combined_calendar_store;
mod contact_birthday_store;
mod secret; mod secret;
mod subscription_store; mod subscription_store;
pub mod synctoken; pub mod synctoken;
@@ -17,8 +16,7 @@ pub mod tests;
pub use addressbook_store::AddressbookStore; pub use addressbook_store::AddressbookStore;
pub use calendar_store::CalendarStore; pub use calendar_store::CalendarStore;
pub use combined_calendar_store::CombinedCalendarStore; pub use combined_calendar_store::{CombinedCalendarStore, PrefixedCalendarStore};
pub use contact_birthday_store::ContactBirthdayStore;
pub use secret::Secret; pub use secret::Secret;
pub use subscription_store::*; pub use subscription_store::*;

View File

@@ -30,3 +30,4 @@ uuid.workspace = true
pbkdf2.workspace = true pbkdf2.workspace = true
rustical_ical.workspace = true rustical_ical.workspace = true
rstest = { workspace = true, optional = true } rstest = { workspace = true, optional = true }
sha2.workspace = true

View File

@@ -0,0 +1 @@
DROP TABLE birthday_calendars;

View File

@@ -0,0 +1,26 @@
CREATE TABLE birthday_calendars (
principal TEXT NOT NULL,
id TEXT NOT NULL,
displayname TEXT,
description TEXT,
"order" INT DEFAULT 0 NOT NULL,
color TEXT,
timezone_id TEXT,
deleted_at DATETIME,
push_topic TEXT NOT NULL,
PRIMARY KEY (principal, id),
CONSTRAINT fk_birthdays_addressbooks FOREIGN KEY (principal, id)
REFERENCES addressbooks (principal, id) ON DELETE CASCADE
-- birthday calendar stores no meaningful data so we can cascade
);
INSERT INTO birthday_calendars
(principal, id, displayname, deleted_at, push_topic)
SELECT
principal,
id,
displayname || ' birthdays' AS displayname,
deleted_at,
push_topic || substr(printf('%d', random()), -4) AS push_topic
-- jank suffix to ensure that new push_topic is different :D
FROM addressbooks;

View File

@@ -0,0 +1,431 @@
use crate::addressbook_store::SqliteAddressbookStore;
use async_trait::async_trait;
use chrono::NaiveDateTime;
use rustical_ical::{AddressObject, CalendarObject, CalendarObjectType};
use rustical_store::{
Addressbook, AddressbookStore, Calendar, CalendarMetadata, CalendarStore, CollectionMetadata,
Error, PrefixedCalendarStore,
};
use sha2::{Digest, Sha256};
use sqlx::{Executor, Sqlite};
use std::collections::HashMap;
use tracing::instrument;
pub const BIRTHDAYS_PREFIX: &str = "_birthdays_";
struct BirthdayCalendarJoinRow {
principal: String,
id: String,
displayname: Option<String>,
description: Option<String>,
order: i64,
color: Option<String>,
timezone_id: Option<String>,
deleted_at: Option<NaiveDateTime>,
push_topic: String,
addr_synctoken: i64,
}
impl From<BirthdayCalendarJoinRow> for Calendar {
fn from(value: BirthdayCalendarJoinRow) -> Self {
Self {
principal: value.principal,
id: format!("{}{}", BIRTHDAYS_PREFIX, value.id),
meta: CalendarMetadata {
displayname: value.displayname,
order: value.order,
description: value.description,
color: value.color,
},
deleted_at: value.deleted_at,
components: vec![CalendarObjectType::Event],
timezone_id: value.timezone_id,
synctoken: value.addr_synctoken,
subscription_url: None,
push_topic: value.push_topic,
}
}
}
impl PrefixedCalendarStore for SqliteAddressbookStore {
const PREFIX: &'static str = BIRTHDAYS_PREFIX;
}
impl SqliteAddressbookStore {
#[instrument]
pub async fn _get_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
id: &str,
show_deleted: bool,
) -> Result<Calendar, Error> {
let cal = sqlx::query_as!(
BirthdayCalendarJoinRow,
r#"SELECT principal, id, displayname, description, "order", color, timezone_id, deleted_at, addr_synctoken, push_topic
FROM birthday_calendars
INNER JOIN (
SELECT principal AS addr_principal,
id AS addr_id,
synctoken AS addr_synctoken
FROM addressbooks
) ON (principal, id) = (addr_principal, addr_id)
WHERE (principal, id) = (?, ?)
AND ((deleted_at IS NULL) OR ?)
"#,
principal,
id,
show_deleted
)
.fetch_one(executor)
.await
.map_err(crate::Error::from)?;
Ok(cal.into())
}
#[instrument]
pub async fn _get_birthday_calendars<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
deleted: bool,
) -> Result<Vec<Calendar>, Error> {
Ok(
sqlx::query_as!(
BirthdayCalendarJoinRow,
r#"SELECT principal, id, displayname, description, "order", color, timezone_id, deleted_at, addr_synctoken, push_topic
FROM birthday_calendars
INNER JOIN (
SELECT principal AS addr_principal,
id AS addr_id,
synctoken AS addr_synctoken
FROM addressbooks
) ON (principal, id) = (addr_principal, addr_id)
WHERE principal = ?
AND (
(deleted_at IS NULL AND NOT ?) -- not deleted, want not deleted
OR (deleted_at IS NOT NULL AND ?) -- deleted, want deleted
)
"#,
principal,
deleted,
deleted
)
.fetch_all(executor)
.await
.map_err(crate::Error::from).map(|cals| cals.into_iter().map(BirthdayCalendarJoinRow::into).collect())?)
}
#[must_use]
pub fn default_birthday_calendar(addressbook: Addressbook) -> Calendar {
let birthday_name = addressbook
.displayname
.as_ref()
.map(|name| format!("{name} birthdays"));
let birthday_push_topic = {
let mut hasher = Sha256::new();
hasher.update("birthdays");
hasher.update(&addressbook.push_topic);
format!("{:x}", hasher.finalize())
};
Calendar {
principal: addressbook.principal,
meta: CalendarMetadata {
displayname: birthday_name,
order: 0,
description: None,
color: None,
},
id: format!("{}{}", Self::PREFIX, addressbook.id),
components: vec![CalendarObjectType::Event],
timezone_id: None,
deleted_at: None,
synctoken: Default::default(),
subscription_url: None,
push_topic: birthday_push_topic,
}
}
#[instrument]
pub async fn _insert_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
calendar: &Calendar,
) -> Result<(), rustical_store::Error> {
let id = calendar
.id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
sqlx::query!(
r#"INSERT INTO birthday_calendars (principal, id, displayname, description, "order", color, push_topic)
VALUES (?, ?, ?, ?, ?, ?, ?)"#,
calendar.principal,
id,
calendar.meta.displayname,
calendar.meta.description,
calendar.meta.order,
calendar.meta.color,
calendar.push_topic,
)
.execute(executor)
.await
.map_err(crate::Error::from)?;
Ok(())
}
async fn _delete_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
id: &str,
use_trashbin: bool,
) -> Result<(), Error> {
if use_trashbin {
sqlx::query!(
r#"UPDATE birthday_calendars SET deleted_at = datetime() WHERE (principal, id) = (?, ?)"#,
principal,
id
)
.execute(executor)
.await
.map_err(crate::Error::from)?
} else {
sqlx::query!(
r#"DELETE FROM birthday_calendars WHERE (principal, id) = (?, ?)"#,
principal,
id
)
.execute(executor)
.await
.map_err(crate::Error::from)?
};
Ok(())
}
async fn _restore_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
id: &str,
) -> Result<(), Error> {
sqlx::query!(
r"UPDATE birthday_calendars SET deleted_at = NULL WHERE (principal, id) = (?, ?)",
principal,
id
)
.execute(executor)
.await
.map_err(crate::Error::from)?;
Ok(())
}
#[instrument]
async fn _update_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
calendar: &Calendar,
) -> Result<(), Error> {
let result = sqlx::query!(
r#"UPDATE birthday_calendars SET principal = ?, id = ?, displayname = ?, description = ?, "order" = ?, color = ?, timezone_id = ?, push_topic = ?
WHERE (principal, id) = (?, ?)"#,
calendar.principal,
calendar.id,
calendar.meta.displayname,
calendar.meta.description,
calendar.meta.order,
calendar.meta.color,
calendar.timezone_id,
calendar.push_topic,
principal,
calendar.id,
).execute(executor).await.map_err(crate::Error::from)?;
if result.rows_affected() == 0 {
return Err(rustical_store::Error::NotFound);
}
Ok(())
}
}
#[async_trait]
impl CalendarStore for SqliteAddressbookStore {
#[instrument]
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)?;
Self::_get_birthday_calendar(&self.db, principal, id, show_deleted).await
}
#[instrument]
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
Self::_get_birthday_calendars(&self.db, principal, false).await
}
#[instrument]
async fn get_deleted_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
Self::_get_birthday_calendars(&self.db, principal, true).await
}
#[instrument]
async fn update_calendar(
&self,
principal: String,
id: String,
mut calendar: Calendar,
) -> Result<(), Error> {
assert_eq!(id, calendar.id);
calendar.id = calendar
.id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?
.to_string();
Self::_update_birthday_calendar(&self.db, &principal, &calendar).await
}
#[instrument]
async fn insert_calendar(&self, calendar: Calendar) -> Result<(), Error> {
Self::_insert_birthday_calendar(&self.db, &calendar).await
}
#[instrument]
async fn delete_calendar(
&self,
principal: &str,
id: &str,
use_trashbin: bool,
) -> Result<(), Error> {
let Some(id) = id.strip_prefix(BIRTHDAYS_PREFIX) else {
return Ok(());
};
Self::_delete_birthday_calendar(&self.db, principal, id, use_trashbin).await
}
#[instrument]
async fn restore_calendar(&self, principal: &str, id: &str) -> Result<(), Error> {
let Some(id) = id.strip_prefix(BIRTHDAYS_PREFIX) else {
return Err(Error::NotFound);
};
Self::_restore_birthday_calendar(&self.db, principal, id).await
}
#[instrument]
async fn import_calendar(
&self,
_calendar: Calendar,
_objects: Vec<CalendarObject>,
_merge_existing: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
#[instrument]
async fn sync_changes(
&self,
principal: &str,
cal_id: &str,
synctoken: i64,
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
let (objects, deleted_objects, new_synctoken) =
AddressbookStore::sync_changes(self, principal, cal_id, synctoken).await?;
let objects: Result<Vec<Option<CalendarObject>>, rustical_ical::Error> = objects
.iter()
.map(AddressObject::get_birthday_object)
.collect();
let objects = objects?.into_iter().flatten().collect();
Ok((objects, deleted_objects, new_synctoken))
}
#[instrument]
async fn calendar_metadata(
&self,
principal: &str,
cal_id: &str,
) -> Result<CollectionMetadata, Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
self.addressbook_metadata(principal, cal_id).await
}
#[instrument]
async fn get_objects(
&self,
principal: &str,
cal_id: &str,
) -> Result<Vec<CalendarObject>, Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
let objects: Result<Vec<HashMap<&'static str, CalendarObject>>, rustical_ical::Error> =
AddressbookStore::get_objects(self, principal, cal_id)
.await?
.iter()
.map(AddressObject::get_significant_dates)
.collect();
let objects = objects?
.into_iter()
.flat_map(HashMap::into_values)
.collect();
Ok(objects)
}
#[instrument]
async fn get_object(
&self,
principal: &str,
cal_id: &str,
object_id: &str,
show_deleted: bool,
) -> Result<CalendarObject, Error> {
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)?;
AddressbookStore::get_object(self, principal, cal_id, addressobject_id, show_deleted)
.await?
.get_significant_dates()?
.remove(date_type)
.ok_or(Error::NotFound)
}
#[instrument]
async fn put_object(
&self,
_principal: String,
_cal_id: String,
_object: CalendarObject,
_overwrite: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
#[instrument]
async fn delete_object(
&self,
_principal: &str,
_cal_id: &str,
_object_id: &str,
_use_trashbin: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
#[instrument]
async fn restore_object(
&self,
_principal: &str,
_cal_id: &str,
_object_id: &str,
) -> Result<(), Error> {
Err(Error::ReadOnly)
}
fn is_read_only(&self, _cal_id: &str) -> bool {
true
}
}

View File

@@ -11,6 +11,8 @@ use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tracing::{error, instrument, warn}; use tracing::{error, instrument, warn};
pub mod birthday_calendar;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct AddressObjectRow { struct AddressObjectRow {
id: String, id: String,
@@ -459,7 +461,16 @@ impl AddressbookStore for SqliteAddressbookStore {
&self, &self,
addressbook: Addressbook, addressbook: Addressbook,
) -> Result<(), rustical_store::Error> { ) -> Result<(), rustical_store::Error> {
Self::_insert_addressbook(&self.db, &addressbook).await let mut tx = self
.db
.begin_with(BEGIN_IMMEDIATE)
.await
.map_err(crate::Error::from)?;
Self::_insert_addressbook(&mut *tx, &addressbook).await?;
let birthday_cal = Self::default_birthday_calendar(addressbook);
Self::_insert_birthday_calendar(&mut *tx, &birthday_cal).await?;
tx.commit().await.map_err(crate::Error::from)?;
Ok(())
} }
#[instrument] #[instrument]

View File

@@ -16,7 +16,8 @@ use rustical_frontend::{FrontendConfig, frontend_router};
use rustical_oidc::OidcConfig; use rustical_oidc::OidcConfig;
use rustical_store::auth::AuthenticationProvider; use rustical_store::auth::AuthenticationProvider;
use rustical_store::{ use rustical_store::{
AddressbookStore, CalendarStore, CombinedCalendarStore, ContactBirthdayStore, SubscriptionStore, AddressbookStore, CalendarStore, CombinedCalendarStore, PrefixedCalendarStore,
SubscriptionStore,
}; };
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@@ -33,7 +34,11 @@ use tracing::field::display;
clippy::too_many_lines, clippy::too_many_lines,
clippy::cognitive_complexity clippy::cognitive_complexity
)] )]
pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>( pub fn make_app<
AS: AddressbookStore + PrefixedCalendarStore,
CS: CalendarStore,
S: SubscriptionStore,
>(
addr_store: Arc<AS>, addr_store: Arc<AS>,
cal_store: Arc<CS>, cal_store: Arc<CS>,
subscription_store: Arc<S>, subscription_store: Arc<S>,
@@ -45,7 +50,7 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
session_cookie_samesite_strict: bool, session_cookie_samesite_strict: bool,
payload_limit_mb: usize, payload_limit_mb: usize,
) -> Router<()> { ) -> Router<()> {
let birthday_store = Arc::new(ContactBirthdayStore::new(addr_store.clone())); let birthday_store = addr_store.clone();
let combined_cal_store = let combined_cal_store =
Arc::new(CombinedCalendarStore::new(cal_store).with_store(birthday_store)); Arc::new(CombinedCalendarStore::new(cal_store).with_store(birthday_store));

View File

@@ -13,7 +13,9 @@ use figment::Figment;
use figment::providers::{Env, Format, Toml}; use figment::providers::{Env, Format, Toml};
use rustical_dav_push::DavPushController; use rustical_dav_push::DavPushController;
use rustical_store::auth::AuthenticationProvider; use rustical_store::auth::AuthenticationProvider;
use rustical_store::{AddressbookStore, CalendarStore, CollectionOperation, SubscriptionStore}; use rustical_store::{
AddressbookStore, CalendarStore, CollectionOperation, PrefixedCalendarStore, SubscriptionStore,
};
use rustical_store_sqlite::addressbook_store::SqliteAddressbookStore; use rustical_store_sqlite::addressbook_store::SqliteAddressbookStore;
use rustical_store_sqlite::calendar_store::SqliteCalendarStore; use rustical_store_sqlite::calendar_store::SqliteCalendarStore;
use rustical_store_sqlite::principal_store::SqlitePrincipalStore; use rustical_store_sqlite::principal_store::SqlitePrincipalStore;
@@ -56,7 +58,7 @@ async fn get_data_stores(
migrate: bool, migrate: bool,
config: &DataStoreConfig, config: &DataStoreConfig,
) -> Result<( ) -> Result<(
Arc<impl AddressbookStore>, Arc<impl AddressbookStore + PrefixedCalendarStore>,
Arc<impl CalendarStore>, Arc<impl CalendarStore>,
Arc<impl SubscriptionStore>, Arc<impl SubscriptionStore>,
Arc<impl AuthenticationProvider>, Arc<impl AuthenticationProvider>,