mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 21:42:34 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5bff08b08 | ||
|
|
3ca02d9792 | ||
|
|
ee2cc2174c | ||
|
|
caf10912e5 | ||
|
|
ec89cd6fa5 | ||
|
|
ae20573670 | ||
|
|
71cee2d20c | ||
|
|
83c6bf247e | ||
|
|
6bcc03d659 | ||
|
|
32f5c01716 | ||
|
|
40938cba02 | ||
|
|
a5663bf006 | ||
|
|
26306fd661 | ||
|
|
d8e4bd1cc4 | ||
|
|
a18ff2b400 | ||
|
|
bf13d95b97 | ||
|
|
ee1faa4c20 | ||
|
|
1e999ca0cc | ||
|
|
f27245f996 | ||
|
|
734455b5ab | ||
|
|
8c6a616015 | ||
|
|
828e7399c8 | ||
|
|
891ef6a9f3 | ||
|
|
7b27ac22a4 |
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n REPLACE INTO principals\n (id, displayname, principal_type, password_hash)\n VALUES (?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2f043f62a7c0eae1023e319f0bc8f35dfdcf6a8247e03b1de3e2cabb2d3ab8ae"
|
||||
}
|
||||
12
.sqlx/query-5c09c2a3c052188435409d4ff076575394e625dd19f00dea2d4c71a9f34a5952.json
generated
Normal file
12
.sqlx/query-5c09c2a3c052188435409d4ff076575394e625dd19f00dea2d4c71a9f34a5952.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO principals\n (id, displayname, principal_type, password_hash) VALUES (?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET\n (displayname, principal_type, password_hash)\n = (excluded.displayname, excluded.principal_type, excluded.password_hash)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5c09c2a3c052188435409d4ff076575394e625dd19f00dea2d4c71a9f34a5952"
|
||||
}
|
||||
26
.sqlx/query-660833e0505d3bbcd6dd736cce06b1bf14263d0e0e87b27d89d376d422e4e474.json
generated
Normal file
26
.sqlx/query-660833e0505d3bbcd6dd736cce06b1bf14263d0e0e87b27d89d376d422e4e474.json
generated
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT length(vcf) AS 'length!: u64', deleted_at AS 'deleted!: bool' FROM addressobjects WHERE principal = ? AND addressbook_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "length!: u64",
|
||||
"ordinal": 0,
|
||||
"type_info": "Null"
|
||||
},
|
||||
{
|
||||
"name": "deleted!: bool",
|
||||
"ordinal": 1,
|
||||
"type_info": "Datetime"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "660833e0505d3bbcd6dd736cce06b1bf14263d0e0e87b27d89d376d422e4e474"
|
||||
}
|
||||
26
.sqlx/query-d9f14260a46a7ccd137d462c35d350a7fe338a074131776596c5d803fcda1f48.json
generated
Normal file
26
.sqlx/query-d9f14260a46a7ccd137d462c35d350a7fe338a074131776596c5d803fcda1f48.json
generated
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT length(ics) AS 'length!: u64', deleted_at AS 'deleted!: bool' FROM calendarobjects WHERE principal = ? AND cal_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "length!: u64",
|
||||
"ordinal": 0,
|
||||
"type_info": "Null"
|
||||
},
|
||||
{
|
||||
"name": "deleted!: bool",
|
||||
"ordinal": 1,
|
||||
"type_info": "Datetime"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "d9f14260a46a7ccd137d462c35d350a7fe338a074131776596c5d803fcda1f48"
|
||||
}
|
||||
378
Cargo.lock
generated
378
Cargo.lock
generated
@@ -145,7 +145,7 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -181,9 +181,134 @@ checksum = "34921de3d57974069bad483fdfe0ec65d88c4ff892edd1ab4d8b03be0dda1b9b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-attributes"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener 2.5.3",
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"concurrent-queue",
|
||||
"fastrand",
|
||||
"futures-lite",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-global-executor"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
|
||||
dependencies = [
|
||||
"async-channel 2.3.1",
|
||||
"async-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"blocking",
|
||||
"futures-lite",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1237c0ae75a0f3765f58910ff9cdd0a12eeb39ab2f4c7de23262f337f0aacbb3"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"parking",
|
||||
"polling",
|
||||
"rustix",
|
||||
"slab",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-lock"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
|
||||
dependencies = [
|
||||
"event-listener 5.4.0",
|
||||
"event-listener-strategy",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-std"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "730294c1c08c2e0f85759590518f6333f0d5a0a766a27d519c1b244c3dfd8a24"
|
||||
dependencies = [
|
||||
"async-attributes",
|
||||
"async-channel 1.9.0",
|
||||
"async-global-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"crossbeam-utils",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"gloo-timers",
|
||||
"kv-log-macro",
|
||||
"log",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
"wasm-bindgen-futures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.88"
|
||||
@@ -192,7 +317,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -377,6 +502,19 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
|
||||
dependencies = [
|
||||
"async-channel 2.3.1",
|
||||
"async-task",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
@@ -498,7 +636,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -630,7 +768,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -654,7 +792,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -665,7 +803,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -706,7 +844,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
@@ -730,7 +868,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -845,6 +983,16 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "etcetera"
|
||||
version = "0.8.0"
|
||||
@@ -856,6 +1004,12 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "2.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.0"
|
||||
@@ -867,6 +1021,22 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener-strategy"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
||||
dependencies = [
|
||||
"event-listener 5.4.0",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "ff"
|
||||
version = "0.13.1"
|
||||
@@ -1002,6 +1172,19 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
@@ -1010,7 +1193,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1098,6 +1281,18 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||
|
||||
[[package]]
|
||||
name = "gloo-timers"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
version = "0.13.0"
|
||||
@@ -1184,6 +1379,12 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
@@ -1578,6 +1779,15 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kv-log-macro"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
|
||||
dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -1610,6 +1820,12 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.0"
|
||||
@@ -1632,6 +1848,9 @@ name = "log"
|
||||
version = "0.4.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
dependencies = [
|
||||
"value-bag",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
@@ -1876,7 +2095,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2116,7 +2335,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"proc-macro2-diagnostics",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2189,7 +2408,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2204,6 +2423,17 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "piper"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"fastrand",
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkcs1"
|
||||
version = "0.7.5"
|
||||
@@ -2231,6 +2461,21 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"hermit-abi",
|
||||
"pin-project-lite",
|
||||
"rustix",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.2"
|
||||
@@ -2290,7 +2535,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
"version_check",
|
||||
"yansi",
|
||||
]
|
||||
@@ -2315,7 +2560,7 @@ dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2482,7 +2727,7 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2672,7 +2917,7 @@ dependencies = [
|
||||
"regex",
|
||||
"relative-path",
|
||||
"rustc_version",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
@@ -2684,7 +2929,7 @@ checksum = "b3a8fb4672e840a587a66fc577a5491375df51ddb88f2a2c2a792598c326fe14"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"rand 0.8.5",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2717,7 +2962,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -2754,7 +2999,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical"
|
||||
version = "0.4.6"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -2797,8 +3042,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_caldav"
|
||||
version = "0.4.6"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"axum-extra",
|
||||
@@ -2812,6 +3058,7 @@ dependencies = [
|
||||
"ical",
|
||||
"percent-encoding",
|
||||
"quick-xml",
|
||||
"rstest",
|
||||
"rustical_dav",
|
||||
"rustical_dav_push",
|
||||
"rustical_ical",
|
||||
@@ -2833,7 +3080,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_carddav"
|
||||
version = "0.4.6"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -2865,7 +3112,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_dav"
|
||||
version = "0.4.6"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -2890,7 +3137,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_dav_push"
|
||||
version = "0.4.6"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -2916,7 +3163,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_frontend"
|
||||
version = "0.4.6"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"askama_web",
|
||||
@@ -2949,7 +3196,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_ical"
|
||||
version = "0.4.6"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
@@ -2967,7 +3214,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_oidc"
|
||||
version = "0.4.6"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -2982,7 +3229,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_store"
|
||||
version = "0.4.6"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3016,7 +3263,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_store_sqlite"
|
||||
version = "0.4.6"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -3024,6 +3271,7 @@ dependencies = [
|
||||
"password-auth",
|
||||
"password-hash",
|
||||
"pbkdf2",
|
||||
"rstest",
|
||||
"rustical_ical",
|
||||
"rustical_store",
|
||||
"serde",
|
||||
@@ -3036,13 +3284,26 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_xml"
|
||||
version = "0.4.6"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"quick-xml",
|
||||
"thiserror 2.0.12",
|
||||
"xml_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.28"
|
||||
@@ -3164,7 +3425,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3247,7 +3508,7 @@ dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3381,7 +3642,7 @@ dependencies = [
|
||||
"crc",
|
||||
"crossbeam-queue",
|
||||
"either",
|
||||
"event-listener",
|
||||
"event-listener 5.4.0",
|
||||
"futures-core",
|
||||
"futures-intrusive",
|
||||
"futures-io",
|
||||
@@ -3415,7 +3676,7 @@ dependencies = [
|
||||
"quote",
|
||||
"sqlx-core",
|
||||
"sqlx-macros-core",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3438,7 +3699,7 @@ dependencies = [
|
||||
"sqlx-mysql",
|
||||
"sqlx-postgres",
|
||||
"sqlx-sqlite",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
@@ -3591,7 +3852,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3600,6 +3861,17 @@ version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.104"
|
||||
@@ -3628,7 +3900,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3657,7 +3929,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3668,7 +3940,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3763,7 +4035,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4013,7 +4285,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4181,6 +4453,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "value-bag"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
@@ -4255,7 +4533,7 @@ dependencies = [
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -4290,7 +4568,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -4395,7 +4673,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4406,7 +4684,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4613,7 +4891,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4642,7 +4920,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -4663,7 +4941,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4683,7 +4961,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -4723,5 +5001,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
members = ["crates/*"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.6"
|
||||
version = "0.4.10"
|
||||
edition = "2024"
|
||||
description = "A CalDAV server"
|
||||
repository = "https://github.com/lennart-k/rustical"
|
||||
@@ -139,6 +139,7 @@ ece = { version = "2.3", default-features = false, features = [
|
||||
"backend-openssl",
|
||||
] }
|
||||
openssl = { version = "0.10", features = ["vendored"] }
|
||||
async-std = { version = "1.13", features = ["attributes"] }
|
||||
|
||||
[dependencies]
|
||||
rustical_store = { workspace = true }
|
||||
|
||||
@@ -31,3 +31,4 @@ a CalDAV/CardDAV server
|
||||
- Evolution
|
||||
- Apple Calendar
|
||||
- Home Assistant integration
|
||||
- Thunderbird
|
||||
|
||||
@@ -9,6 +9,8 @@ publish = false
|
||||
|
||||
[dev-dependencies]
|
||||
rustical_store_sqlite = { workspace = true, features = ["test"] }
|
||||
rstest.workspace = true
|
||||
async-std.workspace = true
|
||||
|
||||
[dependencies]
|
||||
axum.workspace = true
|
||||
|
||||
@@ -4,7 +4,7 @@ use axum::body::Body;
|
||||
use axum::extract::State;
|
||||
use axum::{extract::Path, response::Response};
|
||||
use headers::{ContentType, HeaderMapExt};
|
||||
use http::{HeaderValue, StatusCode, header};
|
||||
use http::{HeaderValue, Method, StatusCode, header};
|
||||
use ical::generator::{Emitter, IcalCalendarBuilder};
|
||||
use ical::property::Property;
|
||||
use percent_encoding::{CONTROLS, utf8_percent_encode};
|
||||
@@ -19,6 +19,7 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
|
||||
Path((principal, calendar_id)): Path<(String, String)>,
|
||||
State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
|
||||
user: Principal,
|
||||
method: Method,
|
||||
) -> Result<Response, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Err(crate::Error::Unauthorized);
|
||||
@@ -96,5 +97,9 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
|
||||
))
|
||||
.unwrap(),
|
||||
);
|
||||
Ok(resp.body(Body::new(ical_calendar.generate())).unwrap())
|
||||
if matches!(method, Method::HEAD) {
|
||||
Ok(resp.body(Body::empty()).unwrap())
|
||||
} else {
|
||||
Ok(resp.body(Body::new(ical_calendar.generate())).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use axum::extract::{Path, State};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum_extra::TypedHeader;
|
||||
use headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
|
||||
use http::{HeaderMap, StatusCode};
|
||||
use http::{HeaderMap, Method, StatusCode};
|
||||
use rustical_ical::CalendarObject;
|
||||
use rustical_store::CalendarStore;
|
||||
use rustical_store::auth::Principal;
|
||||
@@ -22,6 +22,7 @@ pub async fn get_event<C: CalendarStore>(
|
||||
}): Path<CalendarObjectPathComponents>,
|
||||
State(CalendarObjectResourceService { cal_store }): State<CalendarObjectResourceService<C>>,
|
||||
user: Principal,
|
||||
method: Method,
|
||||
) -> Result<Response, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Err(crate::Error::Unauthorized);
|
||||
@@ -42,7 +43,11 @@ pub async fn get_event<C: CalendarStore>(
|
||||
let hdrs = resp.headers_mut().unwrap();
|
||||
hdrs.typed_insert(ETag::from_str(&event.get_etag()).unwrap());
|
||||
hdrs.typed_insert(ContentType::from_str("text/calendar").unwrap());
|
||||
Ok(resp.body(Body::new(event.get_ics().to_owned())).unwrap())
|
||||
if matches!(method, Method::HEAD) {
|
||||
Ok(resp.body(Body::empty()).unwrap())
|
||||
} else {
|
||||
Ok(resp.body(Body::new(event.get_ics().to_owned())).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(cal_store))]
|
||||
|
||||
@@ -1,33 +1,38 @@
|
||||
use crate::principal::PrincipalResourceService;
|
||||
use rustical_dav::resource::ResourceService;
|
||||
use rustical_store::auth::{AuthenticationProvider, Principal, PrincipalType};
|
||||
use rustical_store_sqlite::tests::get_test_stores;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::principal::PrincipalResourceService;
|
||||
use rstest::rstest;
|
||||
use rustical_dav::resource::ResourceService;
|
||||
use rustical_store_sqlite::{
|
||||
SqliteStore,
|
||||
calendar_store::SqliteCalendarStore,
|
||||
principal_store::SqlitePrincipalStore,
|
||||
tests::{get_test_calendar_store, get_test_principal_store, get_test_subscription_store},
|
||||
};
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_principal_resource() {
|
||||
let (_, cal_store, sub_store, auth_provider, _) = get_test_stores().await;
|
||||
async fn test_principal_resource(
|
||||
#[from(get_test_calendar_store)]
|
||||
#[future]
|
||||
cal_store: SqliteCalendarStore,
|
||||
#[from(get_test_principal_store)]
|
||||
#[future]
|
||||
auth_provider: SqlitePrincipalStore,
|
||||
#[from(get_test_subscription_store)]
|
||||
#[future]
|
||||
sub_store: SqliteStore,
|
||||
) {
|
||||
let service = PrincipalResourceService {
|
||||
cal_store,
|
||||
sub_store,
|
||||
auth_provider: auth_provider.clone(),
|
||||
cal_store: Arc::new(cal_store.await),
|
||||
sub_store: Arc::new(sub_store.await),
|
||||
auth_provider: Arc::new(auth_provider.await),
|
||||
};
|
||||
|
||||
auth_provider
|
||||
.insert_principal(
|
||||
Principal {
|
||||
id: "user".to_owned(),
|
||||
displayname: None,
|
||||
memberships: vec![],
|
||||
password: None,
|
||||
principal_type: PrincipalType::Individual,
|
||||
},
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
service.get_resource(&("anonymous".to_owned(),), true).await,
|
||||
service
|
||||
.get_resource(&("invalid-user".to_owned(),), true)
|
||||
.await,
|
||||
Err(crate::Error::NotFound)
|
||||
));
|
||||
|
||||
@@ -37,3 +42,5 @@ async fn test_principal_resource() {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_propfind() {}
|
||||
|
||||
@@ -7,6 +7,7 @@ use axum::extract::{Path, State};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum_extra::TypedHeader;
|
||||
use axum_extra::headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
|
||||
use http::Method;
|
||||
use http::{HeaderMap, StatusCode};
|
||||
use rustical_dav::privileges::UserPrivilege;
|
||||
use rustical_dav::resource::Resource;
|
||||
@@ -25,6 +26,7 @@ pub async fn get_object<AS: AddressbookStore>(
|
||||
}): Path<AddressObjectPathComponents>,
|
||||
State(AddressObjectResourceService { addr_store }): State<AddressObjectResourceService<AS>>,
|
||||
user: Principal,
|
||||
method: Method,
|
||||
) -> Result<Response, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Err(Error::Unauthorized);
|
||||
@@ -49,7 +51,11 @@ pub async fn get_object<AS: AddressbookStore>(
|
||||
let hdrs = resp.headers_mut().unwrap();
|
||||
hdrs.typed_insert(ETag::from_str(&object.get_etag()).unwrap());
|
||||
hdrs.typed_insert(ContentType::from_str("text/vcard").unwrap());
|
||||
Ok(resp.body(Body::new(object.get_vcf().to_owned())).unwrap())
|
||||
if matches!(method, Method::HEAD) {
|
||||
Ok(resp.body(Body::empty()).unwrap())
|
||||
} else {
|
||||
Ok(resp.body(Body::new(object.get_vcf().to_owned())).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(addr_store, body))]
|
||||
|
||||
@@ -5,7 +5,7 @@ use axum::body::Body;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::response::Response;
|
||||
use axum_extra::headers::{ContentType, HeaderMapExt};
|
||||
use http::{HeaderValue, StatusCode, header};
|
||||
use http::{HeaderValue, Method, StatusCode, header};
|
||||
use percent_encoding::{CONTROLS, utf8_percent_encode};
|
||||
use rustical_dav::privileges::UserPrivilege;
|
||||
use rustical_dav::resource::Resource;
|
||||
@@ -20,6 +20,7 @@ pub async fn route_get<AS: AddressbookStore, S: SubscriptionStore>(
|
||||
Path((principal, addressbook_id)): Path<(String, String)>,
|
||||
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
|
||||
user: Principal,
|
||||
method: Method,
|
||||
) -> Result<Response, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Err(Error::Unauthorized);
|
||||
@@ -55,5 +56,9 @@ pub async fn route_get<AS: AddressbookStore, S: SubscriptionStore>(
|
||||
))
|
||||
.unwrap(),
|
||||
);
|
||||
Ok(resp.body(Body::new(vcf)).unwrap())
|
||||
if matches!(method, Method::HEAD) {
|
||||
Ok(resp.body(Body::empty()).unwrap())
|
||||
} else {
|
||||
Ok(resp.body(Body::new(vcf)).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,12 @@ pub enum UserPrivilege {
|
||||
}
|
||||
|
||||
impl XmlSerialize for UserPrivilegeSet {
|
||||
fn serialize<W: std::io::Write>(
|
||||
fn serialize(
|
||||
&self,
|
||||
ns: Option<Namespace>,
|
||||
tag: Option<&[u8]>,
|
||||
namespaces: &HashMap<Namespace, &[u8]>,
|
||||
writer: &mut quick_xml::Writer<W>,
|
||||
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
|
||||
) -> std::io::Result<()> {
|
||||
#[derive(XmlSerialize)]
|
||||
pub struct FakeUserPrivilegeSet {
|
||||
@@ -35,7 +35,6 @@ impl XmlSerialize for UserPrivilegeSet {
|
||||
.serialize(ns, tag, namespaces, writer)
|
||||
}
|
||||
|
||||
#[allow(refining_impl_trait)]
|
||||
fn attributes<'a>(&self) -> Option<Vec<quick_xml::events::attributes::Attribute<'a>>> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -18,11 +18,6 @@ pub trait AxumMethods: Sized + Send + Sync + 'static {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn head() -> Option<MethodFunction<Self>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn post() -> Option<MethodFunction<Self>> {
|
||||
None
|
||||
@@ -58,8 +53,6 @@ pub trait AxumMethods: Sized + Send + Sync + 'static {
|
||||
}
|
||||
if Self::get().is_some() {
|
||||
allow.push(Method::GET);
|
||||
}
|
||||
if Self::head().is_some() {
|
||||
allow.push(Method::HEAD);
|
||||
}
|
||||
if Self::post().is_some() {
|
||||
|
||||
@@ -72,16 +72,11 @@ where
|
||||
return svc(self.resource_service.clone(), req);
|
||||
}
|
||||
}
|
||||
"GET" => {
|
||||
"GET" | "HEAD" => {
|
||||
if let Some(svc) = RS::get() {
|
||||
return svc(self.resource_service.clone(), req);
|
||||
}
|
||||
}
|
||||
"HEAD" => {
|
||||
if let Some(svc) = RS::head() {
|
||||
return svc(self.resource_service.clone(), req);
|
||||
}
|
||||
}
|
||||
"POST" => {
|
||||
if let Some(svc) = RS::post() {
|
||||
return svc(self.resource_service.clone(), req);
|
||||
|
||||
@@ -77,6 +77,7 @@ pub(crate) async fn route_propfind<R: ResourceService>(
|
||||
|
||||
let mut member_responses = Vec::new();
|
||||
if depth != &Depth::Zero {
|
||||
// TODO: authorization check for member resources
|
||||
for member in resource_service.get_members(path_components).await? {
|
||||
member_responses.push(member.propfind(
|
||||
&format!("{}/{}", path.trim_end_matches('/'), member.get_name()),
|
||||
|
||||
@@ -19,12 +19,12 @@ pub struct PropstatElement<PropType: XmlSerialize> {
|
||||
pub status: StatusCode,
|
||||
}
|
||||
|
||||
fn xml_serialize_status<W: ::std::io::Write>(
|
||||
fn xml_serialize_status(
|
||||
status: &StatusCode,
|
||||
ns: Option<Namespace>,
|
||||
tag: Option<&[u8]>,
|
||||
namespaces: &HashMap<Namespace, &[u8]>,
|
||||
writer: &mut quick_xml::Writer<W>,
|
||||
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
|
||||
) -> std::io::Result<()> {
|
||||
XmlSerialize::serialize(&format!("HTTP/1.1 {}", status), ns, tag, namespaces, writer)
|
||||
}
|
||||
@@ -49,12 +49,12 @@ pub struct ResponseElement<PropstatType: XmlSerialize> {
|
||||
pub propstat: Vec<PropstatWrapper<PropstatType>>,
|
||||
}
|
||||
|
||||
fn xml_serialize_optional_status<W: ::std::io::Write>(
|
||||
fn xml_serialize_optional_status(
|
||||
val: &Option<StatusCode>,
|
||||
ns: Option<Namespace>,
|
||||
tag: Option<&[u8]>,
|
||||
namespaces: &HashMap<Namespace, &[u8]>,
|
||||
writer: &mut quick_xml::Writer<W>,
|
||||
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
|
||||
) -> std::io::Result<()> {
|
||||
XmlSerialize::serialize(
|
||||
&val.map(|status| format!("HTTP/1.1 {}", status)),
|
||||
@@ -111,11 +111,10 @@ impl<T1: XmlSerialize, T2: XmlSerialize> axum::response::IntoResponse
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
use axum::body::Body;
|
||||
|
||||
let mut output: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into();
|
||||
let mut writer = quick_xml::Writer::new_with_indent(&mut output, b' ', 4);
|
||||
if let Err(err) = self.serialize_root(&mut writer) {
|
||||
return crate::Error::from(err).into_response();
|
||||
}
|
||||
let output = match self.serialize_to_string() {
|
||||
Ok(out) => out,
|
||||
Err(err) => return crate::Error::from(err).into_response(),
|
||||
};
|
||||
|
||||
let mut resp = axum::response::Response::builder().status(StatusCode::MULTI_STATUS);
|
||||
let hdrs = resp.headers_mut().unwrap();
|
||||
|
||||
@@ -23,20 +23,17 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_serialize_resourcetype() {
|
||||
let mut buf = Vec::new();
|
||||
let mut writer = quick_xml::Writer::new(&mut buf);
|
||||
Document {
|
||||
let out = Document {
|
||||
resourcetype: Resourcetype(&[
|
||||
ResourcetypeInner(Some(crate::namespace::NS_DAV), "displayname"),
|
||||
ResourcetypeInner(Some(crate::namespace::NS_CALENDARSERVER), "calendar-color"),
|
||||
]),
|
||||
}
|
||||
.serialize_root(&mut writer)
|
||||
.serialize_to_string()
|
||||
.unwrap();
|
||||
let out = String::from_utf8(buf).unwrap();
|
||||
assert_eq!(
|
||||
out,
|
||||
"<document><resourcetype><displayname xmlns=\"DAV:\"/><calendar-color xmlns=\"http://calendarserver.org/ns/\"/></resourcetype></document>"
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<document><resourcetype><displayname xmlns=\"DAV:\"/><calendar-color xmlns=\"http://calendarserver.org/ns/\"/></resourcetype></document>"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use rustical_xml::{ValueDeserialize, ValueSerialize, XmlDeserialize};
|
||||
use rustical_xml::{ValueDeserialize, ValueSerialize, XmlDeserialize, XmlRootTag};
|
||||
|
||||
use super::PropfindType;
|
||||
|
||||
@@ -32,11 +32,35 @@ impl ValueSerialize for SyncLevel {
|
||||
}
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc5323#section-5.17
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
pub struct LimitElement {
|
||||
#[xml(ns = "crate::namespace::NS_DAV")]
|
||||
pub nresults: NresultsElement,
|
||||
}
|
||||
|
||||
impl From<u64> for LimitElement {
|
||||
fn from(value: u64) -> Self {
|
||||
Self {
|
||||
nresults: NresultsElement(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LimitElement> for u64 {
|
||||
fn from(value: LimitElement) -> Self {
|
||||
value.nresults.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
pub struct NresultsElement(#[xml(ty = "text")] u64);
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, XmlRootTag)]
|
||||
// <!ELEMENT sync-collection (sync-token, sync-level, limit?, prop)>
|
||||
// <!-- DAV:limit defined in RFC 5323, Section 5.17 -->
|
||||
// <!-- DAV:prop defined in RFC 4918, Section 14.18 -->
|
||||
#[xml(ns = "crate::namespace::NS_DAV")]
|
||||
#[xml(ns = "crate::namespace::NS_DAV", root = b"sync-collection")]
|
||||
pub struct SyncCollectionRequest<PN: XmlDeserialize> {
|
||||
#[xml(ns = "crate::namespace::NS_DAV")]
|
||||
pub sync_token: String,
|
||||
@@ -45,5 +69,48 @@ pub struct SyncCollectionRequest<PN: XmlDeserialize> {
|
||||
#[xml(ns = "crate::namespace::NS_DAV", ty = "untagged")]
|
||||
pub prop: PropfindType<PN>,
|
||||
#[xml(ns = "crate::namespace::NS_DAV")]
|
||||
pub limit: Option<u64>,
|
||||
pub limit: Option<LimitElement>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::xml::{
|
||||
PropElement, PropfindType,
|
||||
sync_collection::{SyncCollectionRequest, SyncLevel},
|
||||
};
|
||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlDocument};
|
||||
|
||||
const SYNC_COLLECTION_REQUEST: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<sync-collection xmlns="DAV:">
|
||||
<sync-token />
|
||||
<sync-level>1</sync-level>
|
||||
<limit>
|
||||
<nresults>100</nresults>
|
||||
</limit>
|
||||
<prop>
|
||||
<getetag />
|
||||
</prop>
|
||||
</sync-collection>
|
||||
"#;
|
||||
|
||||
#[derive(XmlDeserialize, PropName, EnumVariants, PartialEq)]
|
||||
#[xml(unit_variants_ident = "TestPropName")]
|
||||
enum TestProp {
|
||||
Getetag(String),
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_sync_collection_request() {
|
||||
let request =
|
||||
SyncCollectionRequest::<TestPropName>::parse_str(SYNC_COLLECTION_REQUEST).unwrap();
|
||||
assert_eq!(
|
||||
request,
|
||||
SyncCollectionRequest {
|
||||
sync_token: "".to_owned(),
|
||||
sync_level: SyncLevel::One,
|
||||
prop: PropfindType::Prop(PropElement(vec![TestPropName::Getetag], vec![])),
|
||||
limit: Some(100.into())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,12 @@ use std::collections::HashMap;
|
||||
pub struct TagList(Vec<(Option<NamespaceOwned>, String)>);
|
||||
|
||||
impl XmlSerialize for TagList {
|
||||
fn serialize<W: std::io::Write>(
|
||||
fn serialize(
|
||||
&self,
|
||||
ns: Option<Namespace>,
|
||||
tag: Option<&[u8]>,
|
||||
namespaces: &HashMap<Namespace, &[u8]>,
|
||||
writer: &mut quick_xml::Writer<W>,
|
||||
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
|
||||
) -> std::io::Result<()> {
|
||||
let prefix = ns
|
||||
.map(|ns| namespaces.get(&ns))
|
||||
@@ -57,7 +57,6 @@ impl XmlSerialize for TagList {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(refining_impl_trait)]
|
||||
fn attributes<'a>(&self) -> Option<Vec<quick_xml::events::attributes::Attribute<'a>>> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -99,13 +99,13 @@ impl<S: SubscriptionStore> DavPushController<S> {
|
||||
content_update,
|
||||
};
|
||||
|
||||
let mut output: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into();
|
||||
let mut writer = quick_xml::Writer::new_with_indent(&mut output, b' ', 4);
|
||||
if let Err(err) = push_message.serialize_root(&mut writer) {
|
||||
error!("Could not serialize push message: {}", err);
|
||||
return;
|
||||
}
|
||||
let payload = String::from_utf8(output).unwrap();
|
||||
let payload = match push_message.serialize_to_string() {
|
||||
Ok(payload) => payload,
|
||||
Err(err) => {
|
||||
error!("Could not serialize push message: {}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for subsciption in subscriptions {
|
||||
if let Some(allowed_push_servers) = &self.allowed_push_servers {
|
||||
|
||||
@@ -19,6 +19,8 @@ export class CreateAddressbookForm extends LitElement {
|
||||
@property()
|
||||
user: String = ''
|
||||
@property()
|
||||
principal: String = ''
|
||||
@property()
|
||||
addr_id: String = ''
|
||||
@property()
|
||||
displayname: String = ''
|
||||
@@ -34,6 +36,11 @@ export class CreateAddressbookForm extends LitElement {
|
||||
<dialog ${ref(this.dialog)}>
|
||||
<h3>Create addressbook</h3>
|
||||
<form @submit=${this.submit} ${ref(this.form)}>
|
||||
<label>
|
||||
principal (for group addressbooks)
|
||||
<input type="text" name="principal" value=${this.user} @change=${e => this.principal = e.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
id
|
||||
<input type="text" name="id" @change=${e => this.addr_id = e.target.value} />
|
||||
@@ -68,7 +75,7 @@ export class CreateAddressbookForm extends LitElement {
|
||||
return
|
||||
}
|
||||
// TODO: Escape user input: There's not really a security risk here but would be nicer
|
||||
await this.client.createDirectory(`/principal/${this.user}/${this.addr_id}`, {
|
||||
await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.addr_id}`, {
|
||||
data: `
|
||||
<mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
|
||||
<set>
|
||||
|
||||
@@ -18,6 +18,8 @@ export class CreateCalendarForm extends LitElement {
|
||||
@property()
|
||||
user: String = ''
|
||||
@property()
|
||||
principal: String = ''
|
||||
@property()
|
||||
cal_id: String = ''
|
||||
@property()
|
||||
displayname: String = ''
|
||||
@@ -40,6 +42,11 @@ export class CreateCalendarForm extends LitElement {
|
||||
<dialog ${ref(this.dialog)}>
|
||||
<h3>Create calendar</h3>
|
||||
<form @submit=${this.submit} ${ref(this.form)}>
|
||||
<label>
|
||||
principal (for group calendar)
|
||||
<input type="text" name="principal" value=${this.user} @change=${e => this.principal = e.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
id
|
||||
<input type="text" name="id" @change=${e => this.cal_id = e.target.value} />
|
||||
@@ -70,6 +77,7 @@ export class CreateCalendarForm extends LitElement {
|
||||
Support ${comp}
|
||||
<input type="checkbox" value=${comp} @change=${e => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} />
|
||||
</label>
|
||||
<br>
|
||||
`)}
|
||||
<br>
|
||||
<button type="submit">Create</button>
|
||||
@@ -94,7 +102,7 @@ export class CreateCalendarForm extends LitElement {
|
||||
alert("No calendar components selected")
|
||||
return
|
||||
}
|
||||
await this.client.createDirectory(`/principal/${this.user}/${this.cal_id}`, {
|
||||
await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.cal_id}`, {
|
||||
data: `
|
||||
<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>
|
||||
|
||||
@@ -17,6 +17,7 @@ let CreateAddressbookForm = class extends i {
|
||||
super();
|
||||
this.client = an("/carddav");
|
||||
this.user = "";
|
||||
this.principal = "";
|
||||
this.addr_id = "";
|
||||
this.displayname = "";
|
||||
this.description = "";
|
||||
@@ -32,6 +33,11 @@ let CreateAddressbookForm = class extends i {
|
||||
<dialog ${n(this.dialog)}>
|
||||
<h3>Create addressbook</h3>
|
||||
<form @submit=${this.submit} ${n(this.form)}>
|
||||
<label>
|
||||
principal (for group addressbooks)
|
||||
<input type="text" name="principal" value=${this.user} @change=${(e2) => this.principal = e2.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
id
|
||||
<input type="text" name="id" @change=${(e2) => this.addr_id = e2.target.value} />
|
||||
@@ -68,7 +74,7 @@ let CreateAddressbookForm = class extends i {
|
||||
alert("Empty displayname");
|
||||
return;
|
||||
}
|
||||
await this.client.createDirectory(`/principal/${this.user}/${this.addr_id}`, {
|
||||
await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.addr_id}`, {
|
||||
data: `
|
||||
<mkcol xmlns="DAV:" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
|
||||
<set>
|
||||
@@ -87,6 +93,9 @@ let CreateAddressbookForm = class extends i {
|
||||
__decorateClass([
|
||||
n$1()
|
||||
], CreateAddressbookForm.prototype, "user", 2);
|
||||
__decorateClass([
|
||||
n$1()
|
||||
], CreateAddressbookForm.prototype, "principal", 2);
|
||||
__decorateClass([
|
||||
n$1()
|
||||
], CreateAddressbookForm.prototype, "addr_id", 2);
|
||||
|
||||
@@ -17,6 +17,7 @@ let CreateCalendarForm = class extends i {
|
||||
super();
|
||||
this.client = an("/caldav");
|
||||
this.user = "";
|
||||
this.principal = "";
|
||||
this.cal_id = "";
|
||||
this.displayname = "";
|
||||
this.description = "";
|
||||
@@ -35,6 +36,11 @@ let CreateCalendarForm = class extends i {
|
||||
<dialog ${n(this.dialog)}>
|
||||
<h3>Create calendar</h3>
|
||||
<form @submit=${this.submit} ${n(this.form)}>
|
||||
<label>
|
||||
principal (for group calendar)
|
||||
<input type="text" name="principal" value=${this.user} @change=${(e2) => this.principal = e2.target.value} />
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
id
|
||||
<input type="text" name="id" @change=${(e2) => this.cal_id = e2.target.value} />
|
||||
@@ -65,6 +71,7 @@ let CreateCalendarForm = class extends i {
|
||||
Support ${comp}
|
||||
<input type="checkbox" value=${comp} @change=${(e2) => e2.target.checked ? this.components.add(e2.target.value) : this.components.delete(e2.target.value)} />
|
||||
</label>
|
||||
<br>
|
||||
`)}
|
||||
<br>
|
||||
<button type="submit">Create</button>
|
||||
@@ -92,7 +99,7 @@ let CreateCalendarForm = class extends i {
|
||||
alert("No calendar components selected");
|
||||
return;
|
||||
}
|
||||
await this.client.createDirectory(`/principal/${this.user}/${this.cal_id}`, {
|
||||
await this.client.createDirectory(`/principal/${this.principal || this.user}/${this.cal_id}`, {
|
||||
data: `
|
||||
<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>
|
||||
@@ -116,6 +123,9 @@ let CreateCalendarForm = class extends i {
|
||||
__decorateClass([
|
||||
n$1()
|
||||
], CreateCalendarForm.prototype, "user", 2);
|
||||
__decorateClass([
|
||||
n$1()
|
||||
], CreateCalendarForm.prototype, "principal", 2);
|
||||
__decorateClass([
|
||||
n$1()
|
||||
], CreateCalendarForm.prototype, "cal_id", 2);
|
||||
|
||||
@@ -1,21 +1,41 @@
|
||||
:root {
|
||||
--background-color: #FFF;
|
||||
--background-darker: #EEE;
|
||||
--text-on-background-color: #111;
|
||||
--primary-color: #2F2FE1;
|
||||
--primary-color-dark: color-mix(in srgb, var(--primary-color), #000000 80%);
|
||||
--text-on-primary-color: #FFF;
|
||||
/* --color-red: #FE2060; */
|
||||
/* --color-red: #EE1D59; */
|
||||
--color-red: #E31B39;
|
||||
--dilute-color: black;
|
||||
--border-color: black;
|
||||
}
|
||||
|
||||
html {
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background-color: #222;
|
||||
--background-darker: #292929;
|
||||
--text-on-background-color: #CACACA;
|
||||
--primary-color: color-mix(in srgb, #2F2FE1, white 15%);
|
||||
--primary-color-dark: color-mix(in srgb, var(--primary-color), #000000 80%);
|
||||
--text-on-primary-color: #FFF;
|
||||
/* --color-red: #FE2060; */
|
||||
--color-red: #EE1D59;
|
||||
--dilute-color: white;
|
||||
--border-color: color-mix(in srgb, var(--background-color), var(--dilute-color) 15%);
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
dialog {
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-on-background-color);
|
||||
}
|
||||
|
||||
body {
|
||||
/* position: relative; */
|
||||
font-family: sans-serif;
|
||||
font-family: 'Noto Sans', Helvetica, Arial, sans-serif;
|
||||
margin: 0 auto;
|
||||
max-width: 1200px;
|
||||
min-height: 100%;
|
||||
@@ -29,32 +49,65 @@ body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text-on-background-color);
|
||||
}
|
||||
|
||||
header {
|
||||
background: var(--background-darker);
|
||||
height: 60px;
|
||||
min-height: 60px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
padding: 4px 12px;
|
||||
|
||||
border: 2px solid black;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
margin: 12px;
|
||||
box-shadow: 4px 2px 12px -5px black;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
a.logo {
|
||||
font-size: 2em;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--background-darker), var(--dilute-color) 5%);
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
margin: 4px 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--background-darker), var(--dilute-color) 2%);
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--background-darker), var(--dilute-color) 20%);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: color-mix(in srgb, var(--background-darker), var(--dilute-color) 15%);
|
||||
}
|
||||
|
||||
svg.icon {
|
||||
width: 1.3em;
|
||||
vertical-align: bottom;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.logout_form {
|
||||
display: contents;
|
||||
|
||||
button {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +122,7 @@ button,
|
||||
.button {
|
||||
border: none;
|
||||
background: var(--primary-color);
|
||||
padding: 10px 16px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
color: var(--text-on-primary-color);
|
||||
font-size: 0.9em;
|
||||
@@ -97,7 +150,7 @@ input[type="password"] {
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid black;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: 4px 2px 12px -8px black;
|
||||
border-collapse: collapse;
|
||||
@@ -108,7 +161,7 @@ section {
|
||||
}
|
||||
|
||||
table {
|
||||
border: 1px solid black;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: 4px 2px 12px -6px black;
|
||||
border-collapse: collapse;
|
||||
@@ -118,7 +171,7 @@ table {
|
||||
td,
|
||||
th {
|
||||
padding: 8px;
|
||||
border: 1px solid black;
|
||||
border: 1px solid var(--border-color);
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
@@ -126,12 +179,8 @@ table {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
/* tr:nth-of-type(2n+1) { */
|
||||
/* background: var(--background-darker); */
|
||||
/* } */
|
||||
|
||||
tr:hover {
|
||||
background: #DDD;
|
||||
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 10%);
|
||||
}
|
||||
|
||||
tr:first-child th {
|
||||
@@ -151,87 +200,92 @@ table {
|
||||
}
|
||||
}
|
||||
|
||||
#page-user {
|
||||
ul {
|
||||
padding-left: 0;
|
||||
ul.collection-list {
|
||||
padding-left: 0;
|
||||
|
||||
li.collection-list-item {
|
||||
list-style: none;
|
||||
display: contents;
|
||||
li.collection-list-item {
|
||||
list-style: none;
|
||||
display: contents;
|
||||
|
||||
a {
|
||||
background: #EEE;
|
||||
display: grid;
|
||||
min-height: 80px;
|
||||
grid-template-areas:
|
||||
". . color-chip"
|
||||
"title comps color-chip"
|
||||
"description description color-chip"
|
||||
"subscription-url subscription-url color-chip"
|
||||
"actions actions color-chip"
|
||||
". . color-chip";
|
||||
grid-template-rows: 12px auto auto auto auto 12px;
|
||||
grid-template-columns: min-content auto 80px;
|
||||
row-gap: 4px;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
padding-left: 12px;
|
||||
a {
|
||||
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 5%);
|
||||
display: grid;
|
||||
min-height: 80px;
|
||||
height: fit-content;
|
||||
grid-template-areas:
|
||||
". . color-chip"
|
||||
"title comps color-chip"
|
||||
"description description color-chip"
|
||||
"subscription-url subscription-url color-chip"
|
||||
"metadata metadata color-chip"
|
||||
"actions actions color-chip"
|
||||
". . color-chip";
|
||||
grid-template-rows: 12px auto auto auto auto auto 12px;
|
||||
grid-template-columns: min-content auto 80px;
|
||||
row-gap: 4px;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
padding-left: 12px;
|
||||
|
||||
border: 2px solid black;
|
||||
border-radius: 12px;
|
||||
margin: 12px;
|
||||
box-shadow: 4px 2px 12px -6px black;
|
||||
overflow: hidden;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
margin: 12px 0;
|
||||
box-shadow: 4px 2px 12px -6px black;
|
||||
overflow: hidden;
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
grid-area: title;
|
||||
margin-right: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.title {
|
||||
font-weight: bold;
|
||||
grid-area: title;
|
||||
margin-right: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
span {
|
||||
margin: 8px initial;
|
||||
}
|
||||
|
||||
.comps {
|
||||
grid-area: comps;
|
||||
|
||||
span {
|
||||
margin: 8px initial;
|
||||
margin: 0 2px;
|
||||
background: var(--primary-color);
|
||||
color: var(--text-on-primary-color);
|
||||
font-size: .8em;
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.comps {
|
||||
grid-area: comps;
|
||||
.description {
|
||||
grid-area: description;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
span {
|
||||
margin: 0 2px;
|
||||
background: var(--primary-color);
|
||||
color: var(--text-on-primary-color);
|
||||
font-size: .8em;
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
.metadata {
|
||||
grid-area: metadata;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.description {
|
||||
grid-area: description;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.subscription-url {
|
||||
grid-area: subscription-url;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.subscription-url {
|
||||
grid-area: subscription-url;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.color-chip {
|
||||
background: var(--color);
|
||||
grid-area: color-chip;
|
||||
}
|
||||
|
||||
.color-chip {
|
||||
background: var(--color);
|
||||
grid-area: color-chip;
|
||||
}
|
||||
.actions {
|
||||
grid-area: actions;
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
grid-area: actions;
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #DDD;
|
||||
}
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -242,7 +296,7 @@ textarea {
|
||||
}
|
||||
|
||||
dialog {
|
||||
border: 1px solid black;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
}
|
||||
@@ -252,6 +306,20 @@ footer {
|
||||
justify-content: center;
|
||||
margin-top: 32px;
|
||||
gap: 24px;
|
||||
/* position: absolute; */
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 10%);
|
||||
border: 2px solid var(--border-color);
|
||||
padding: 6px 6px;
|
||||
color: var(--text-on-background-color);
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
svg.icon {
|
||||
stroke-width: 2px;
|
||||
color: var(--text-on-background-color);
|
||||
stroke: var(--text-on-background-color);
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
<section>
|
||||
<h2>Addressbooks</h2>
|
||||
<ul>
|
||||
{% for addressbook in addressbooks %}
|
||||
<li class="collection-list-item">
|
||||
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}">
|
||||
<span class="title">
|
||||
{%- if addressbook.principal != user.id -%}{{ addressbook.principal }}/{%- endif -%}
|
||||
{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}
|
||||
</span>
|
||||
<span class="description">
|
||||
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
|
||||
</span>
|
||||
<div class="actions">
|
||||
<form action="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}" target="_blank"
|
||||
method="GET">
|
||||
<button type="submit">Download</button>
|
||||
</form>
|
||||
<delete-button trash
|
||||
href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
You do not have any addressbooks yet
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{%if !deleted_addressbooks.is_empty() %}
|
||||
<h3>Deleted Addressbooks</h3>
|
||||
<ul>
|
||||
{% for addressbook in deleted_addressbooks %}
|
||||
<li class="collection-list-item">
|
||||
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}">
|
||||
<span class="title">
|
||||
{%- if addressbook.principal != user.id -%}{{ addressbook.principal }}/{%- endif -%}
|
||||
{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}
|
||||
</span>
|
||||
<span class="description">
|
||||
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
|
||||
</span>
|
||||
<div class="actions">
|
||||
<form action="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}/restore"
|
||||
method="POST" class="restore-form">
|
||||
<button type="submit">Restore</button>
|
||||
</form>
|
||||
<delete-button href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<create-addressbook-form user="{{ user.id }}"></create-addressbook-form>
|
||||
|
||||
</section>
|
||||
@@ -1,72 +0,0 @@
|
||||
<section>
|
||||
<h2>Calendars</h2>
|
||||
<ul>
|
||||
{% for calendar in calendars %}
|
||||
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
|
||||
<li class="collection-list-item" style="--color: {{ color }}">
|
||||
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}">
|
||||
<span class="title">
|
||||
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
|
||||
{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
|
||||
</span>
|
||||
<div class="comps">
|
||||
{% for comp in calendar.components %}
|
||||
<span>{{ comp }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span class="description">
|
||||
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
|
||||
</span>
|
||||
{% if let Some(subscription_url) = calendar.subscription_url %}
|
||||
<span class="subscription-url">{{ subscription_url }}</span>
|
||||
{% endif %}
|
||||
<div class="actions">
|
||||
<form action="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}" target="_blank" method="GET">
|
||||
<button type="submit">Download</button>
|
||||
</form>
|
||||
{% if !calendar.id.starts_with("_birthdays_") %}
|
||||
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="color-chip"></div>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
You do not have any calendars yet
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{%if !deleted_calendars.is_empty() %}
|
||||
<h3>Deleted Calendars</h3>
|
||||
<ul>
|
||||
{% for calendar in deleted_calendars %}
|
||||
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
|
||||
<li class="collection-list-item" style="--color: {{ color }}">
|
||||
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}">
|
||||
<span class="title">
|
||||
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
|
||||
{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
|
||||
</span>
|
||||
<div class="comps">
|
||||
{% for comp in calendar.components %}
|
||||
<span>{{ comp }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span class="description">
|
||||
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
|
||||
</span>
|
||||
<div class="actions">
|
||||
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST"
|
||||
class="restore-form">
|
||||
<button type="submit">Restore</button>
|
||||
</form>
|
||||
<delete-button href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
|
||||
</div>
|
||||
<div class="color-chip"></div>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<create-calendar-form user="{{ user.id }}"></create-calendar-form>
|
||||
|
||||
</section>
|
||||
@@ -1,58 +0,0 @@
|
||||
<section>
|
||||
<h2>Profile</h2>
|
||||
|
||||
{% let groups = user.memberships_without_self() %}
|
||||
{% if groups.len() > 0 %}
|
||||
<h3>Groups</h3>
|
||||
<ul>
|
||||
{% for group in groups %}
|
||||
<li>{{ group }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<h3>App tokens</h3>
|
||||
<table id="app-tokens">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Created at</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for app_token in app_tokens %}
|
||||
<tr>
|
||||
<td>{{ app_token.name }}</td>
|
||||
<td>
|
||||
{% if let Some(created_at) = app_token.created_at %}
|
||||
{{ chrono_humanize::HumanTime::from(created_at.to_owned()) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<form action="/frontend/user/{{ user.id }}/app_token/{{ app_token.id }}/delete" method="POST">
|
||||
<button type="submit" class="delete">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="generate">
|
||||
<td>
|
||||
<form action="/frontend/user/{{ user.id }}/app_token" method="POST" id="form_generate_app_token">
|
||||
<label class="font_bold" for="generate_app_token_name">App name</label>
|
||||
<input type="text" name="name" id="generate_app_token_name" />
|
||||
</form>
|
||||
</td>
|
||||
<td></td>
|
||||
<td>
|
||||
<button type="submit" form="form_generate_app_token">Generate</button>
|
||||
{% if is_apple %}
|
||||
<button type="submit" form="form_generate_app_token" name="apple" value="true">Apple Configuration Profile
|
||||
(contains token)</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% if let Some(hostname) = davx5_hostname %}
|
||||
<a
|
||||
href="intent://{{ hostname | urlencode }}#Intent;action=android.intent.action.VIEW;component=at.bitfire.davdroid.ui.setup.LoginActivity;scheme=davx5;package=at.bitfire.davdroid;S.loginFlow=1;end">Configure
|
||||
in DAVx5</a>
|
||||
{% endif %}
|
||||
</section>
|
||||
@@ -0,0 +1,60 @@
|
||||
<h2>{{user.id }}'s Addressbooks</h2>
|
||||
<ul class="collection-list">
|
||||
{% for (meta, addressbook) in addressbooks %}
|
||||
<li class="collection-list-item">
|
||||
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}">
|
||||
<span class="title">
|
||||
{%- if addressbook.principal != user.id -%}{{ addressbook.principal }}/{%- endif -%}
|
||||
{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}
|
||||
</span>
|
||||
<span class="description">
|
||||
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
|
||||
</span>
|
||||
<div class="actions">
|
||||
<form action="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}" target="_blank"
|
||||
method="GET">
|
||||
<button type="submit">Download</button>
|
||||
</form>
|
||||
<delete-button trash
|
||||
href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
||||
</div>
|
||||
<div class="metadata">
|
||||
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
You do not have any addressbooks yet
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{%if !deleted_addressbooks.is_empty() %}
|
||||
<h3>Deleted Addressbooks</h3>
|
||||
<ul class="collection-list">
|
||||
{% for (meta, addressbook) in deleted_addressbooks %}
|
||||
<li class="collection-list-item">
|
||||
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}">
|
||||
<span class="title">
|
||||
{%- if addressbook.principal != user.id -%}{{ addressbook.principal }}/{%- endif -%}
|
||||
{{ addressbook.displayname.to_owned().unwrap_or(addressbook.id.to_owned()) }}
|
||||
</span>
|
||||
<span class="description">
|
||||
{% if let Some(description) = addressbook.description %}{{ description }}{% endif %}
|
||||
</span>
|
||||
<div class="actions">
|
||||
<form action="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}/restore"
|
||||
method="POST" class="restore-form">
|
||||
<button type="submit">Restore</button>
|
||||
</form>
|
||||
<delete-button href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
||||
</div>
|
||||
<div class="metadata">
|
||||
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<create-addressbook-form user="{{ user.id }}"></create-addressbook-form>
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<h2>{{ user.id }}'s Calendars</h2>
|
||||
<ul class="collection-list">
|
||||
{% for (meta, calendar) in calendars %}
|
||||
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
|
||||
<li class="collection-list-item" style="--color: {{ color }}">
|
||||
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}">
|
||||
<span class="title">
|
||||
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
|
||||
{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
|
||||
</span>
|
||||
<div class="comps">
|
||||
{% for comp in calendar.components %}
|
||||
<span>{{ comp }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span class="description">
|
||||
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
|
||||
</span>
|
||||
{% if let Some(subscription_url) = calendar.subscription_url %}
|
||||
<span class="subscription-url">{{ subscription_url }}</span>
|
||||
{% endif %}
|
||||
<div class="actions">
|
||||
<form action="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}" target="_blank" method="GET">
|
||||
<button type="submit">Download</button>
|
||||
</form>
|
||||
{% if !calendar.id.starts_with("_birthdays_") %}
|
||||
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="metadata">
|
||||
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
|
||||
</div>
|
||||
<div class="color-chip"></div>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
You do not have any calendars yet
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{%if !deleted_calendars.is_empty() %}
|
||||
<h3>Deleted Calendars</h3>
|
||||
<ul class="collection-list">
|
||||
{% for (meta, calendar) in deleted_calendars %}
|
||||
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %}
|
||||
<li class="collection-list-item" style="--color: {{ color }}">
|
||||
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}">
|
||||
<span class="title">
|
||||
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
|
||||
{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
|
||||
</span>
|
||||
<div class="comps">
|
||||
{% for comp in calendar.components %}
|
||||
<span>{{ comp }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span class="description">
|
||||
{% if let Some(description) = calendar.description %}{{ description }}{% endif %}
|
||||
</span>
|
||||
<div class="actions">
|
||||
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST"
|
||||
class="restore-form">
|
||||
<button type="submit">Restore</button>
|
||||
</form>
|
||||
<delete-button href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
|
||||
</div>
|
||||
<div class="metadata">
|
||||
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
|
||||
</div>
|
||||
<div class="color-chip"></div>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<create-calendar-form user="{{ user.id }}"></create-calendar-form>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<h2>{{ user.id }}'s Profile</h2>
|
||||
|
||||
{% let groups = user.memberships_without_self() %}
|
||||
{% if groups.len() > 0 %}
|
||||
<h3>Groups</h3>
|
||||
<ul>
|
||||
{% for group in groups %}
|
||||
<li>{{ group }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<h3>App tokens</h3>
|
||||
<table id="app-tokens">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Created at</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for app_token in app_tokens %}
|
||||
<tr>
|
||||
<td>{{ app_token.name }}</td>
|
||||
<td>
|
||||
{% if let Some(created_at) = app_token.created_at %}
|
||||
{{ chrono_humanize::HumanTime::from(created_at.to_owned()) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<form action="/frontend/user/{{ user.id }}/app_token/{{ app_token.id }}/delete" method="POST">
|
||||
<button type="submit" class="delete">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="generate">
|
||||
<td>
|
||||
<form action="/frontend/user/{{ user.id }}/app_token" method="POST" id="form_generate_app_token">
|
||||
<label class="font_bold" for="generate_app_token_name">App name</label>
|
||||
<input type="text" name="name" id="generate_app_token_name" />
|
||||
</form>
|
||||
</td>
|
||||
<td></td>
|
||||
<td>
|
||||
<button type="submit" form="form_generate_app_token">Generate</button>
|
||||
{% if is_apple %}
|
||||
<button type="submit" form="form_generate_app_token" name="apple" value="true">Apple Configuration Profile
|
||||
(contains token)</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% if let Some(hostname) = davx5_hostname %}
|
||||
<a
|
||||
href="intent://{{ hostname | urlencode }}#Intent;action=android.intent.action.VIEW;component=at.bitfire.davdroid.ui.setup.LoginActivity;scheme=davx5;package=at.bitfire.davdroid;S.loginFlow=1;end">Configure
|
||||
in DAVx5</a>
|
||||
{% endif %}
|
||||
8
crates/frontend/public/templates/icons/calendar.svg
Normal file
8
crates/frontend/public/templates/icons/calendar.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<!-- Adapted from https://iconoir.com/ -->
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon">
|
||||
<path d="M15 4V2M15 4V6M15 4H10.5M3 10V19C3 20.1046 3.89543 21 5 21H19C20.1046 21 21 20.1046 21 19V10H3Z" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M3 10V6C3 4.89543 3.89543 4 5 4H7" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M7 2V6" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M21 10V6C21 4.89543 20.1046 4 19 4H18.5" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
8
crates/frontend/public/templates/icons/group.svg
Normal file
8
crates/frontend/public/templates/icons/group.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<!-- Adapted from https://iconoir.com/ -->
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon">
|
||||
<path d="M1 20V19C1 15.134 4.13401 12 8 12V12C11.866 12 15 15.134 15 19V20" stroke-linecap="round"></path>
|
||||
<path d="M13 14V14C13 11.2386 15.2386 9 18 9V9C20.7614 9 23 11.2386 23 14V14.5" stroke-linecap="round"></path>
|
||||
<path d="M8 12C10.2091 12 12 10.2091 12 8C12 5.79086 10.2091 4 8 4C5.79086 4 4 5.79086 4 8C4 10.2091 5.79086 12 8 12Z" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M18 9C19.6569 9 21 7.65685 21 6C21 4.34315 19.6569 3 18 3C16.3431 3 15 4.34315 15 6C15 7.65685 16.3431 9 18 9Z" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
6
crates/frontend/public/templates/icons/user.svg
Normal file
6
crates/frontend/public/templates/icons/user.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<!-- Adapted from https://iconoir.com/ -->
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon">
|
||||
<path d="M5 20V19C5 15.134 8.13401 12 12 12V12C15.866 12 19 15.134 19 19V20" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M12 12C14.2091 12 16 10.2091 16 8C16 5.79086 14.2091 4 12 4C9.79086 4 8 5.79086 8 8C8 10.2091 9.79086 12 12 12Z" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
@@ -12,7 +12,8 @@
|
||||
<body>
|
||||
{% block header %}
|
||||
<header>
|
||||
<a href="/frontend/user">RustiCal</a>
|
||||
<a class="logo" href="/frontend/user">RustiCal</a>
|
||||
{% block header_center %}{% endblock %}
|
||||
<form method="POST" action="/frontend/logout" class="logout_form">
|
||||
<button type="submit">Log out</button>
|
||||
</form>
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
<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 %}
|
||||
|
||||
{% block content %}
|
||||
<div id="page-user">
|
||||
|
||||
<h1>Welcome {{ user.id }}!</h1>
|
||||
|
||||
{% include "components/profile_section.html" %}
|
||||
{% include "components/calendars_section.html" %}
|
||||
{% include "components/addressbooks_section.html" %}
|
||||
|
||||
{% block header_center %}
|
||||
<nav class="header-center">
|
||||
<a href="/frontend/user/{{ user.id }}" {% if S::name() == "profile" %}class="active"{% endif %}>{% include "icons/user.svg" %}Profile</a>
|
||||
<a href="/frontend/user/{{ user.id }}/calendar" {% if S::name() == "calendars" %}class="active"{% endif %}>{% include "icons/calendar.svg" %}Calendars</a>
|
||||
<a href="/frontend/user/{{ user.id }}/addressbook" {% if S::name() == "addressbooks" %}class="active"{% endif %}>{% include "icons/group.svg" %}Addressbooks</a>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ section|safe }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use axum::{
|
||||
};
|
||||
use headers::{ContentType, HeaderMapExt};
|
||||
use http::{Method, StatusCode};
|
||||
use routes::{addressbooks::route_addressbooks, calendars::route_calendars};
|
||||
use rustical_oidc::{OidcConfig, OidcServiceConfig, route_get_oidc_callback, route_post_oidc};
|
||||
use rustical_store::{
|
||||
AddressbookStore, CalendarStore,
|
||||
@@ -20,6 +21,7 @@ mod assets;
|
||||
mod config;
|
||||
pub mod nextcloud_login;
|
||||
mod oidc_user_store;
|
||||
pub(crate) mod pages;
|
||||
mod routes;
|
||||
|
||||
pub use config::FrontendConfig;
|
||||
@@ -56,6 +58,7 @@ pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: Addres
|
||||
post(route_delete_app_token::<AP>),
|
||||
)
|
||||
// Calendar
|
||||
.route("/user/{user}/calendar", get(route_calendars::<CS>))
|
||||
.route(
|
||||
"/user/{user}/calendar/{calendar}",
|
||||
get(route_calendar::<CS>),
|
||||
@@ -65,6 +68,7 @@ pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: Addres
|
||||
post(route_calendar_restore::<CS>),
|
||||
)
|
||||
// Addressbook
|
||||
.route("/user/{user}/addressbook", get(route_addressbooks::<AS>))
|
||||
.route(
|
||||
"/user/{user}/addressbook/{addressbook}",
|
||||
get(route_addressbook::<AS>),
|
||||
|
||||
1
crates/frontend/src/pages/mod.rs
Normal file
1
crates/frontend/src/pages/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod user;
|
||||
14
crates/frontend/src/pages/user.rs
Normal file
14
crates/frontend/src/pages/user.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use askama::Template;
|
||||
use askama_web::WebTemplate;
|
||||
use rustical_store::auth::Principal;
|
||||
|
||||
pub trait Section: Template {
|
||||
fn name() -> &'static str;
|
||||
}
|
||||
|
||||
#[derive(Template, WebTemplate)]
|
||||
#[template(path = "pages/user.html")]
|
||||
pub struct UserPage<S: Section> {
|
||||
pub user: Principal,
|
||||
pub section: S,
|
||||
}
|
||||
75
crates/frontend/src/routes/addressbooks.rs
Normal file
75
crates/frontend/src/routes/addressbooks.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use askama::Template;
|
||||
use askama_web::WebTemplate;
|
||||
use axum::{Extension, extract::Path, response::IntoResponse};
|
||||
use http::StatusCode;
|
||||
use rustical_store::{Addressbook, AddressbookStore, CollectionMetadata, auth::Principal};
|
||||
|
||||
use crate::pages::user::{Section, UserPage};
|
||||
|
||||
impl Section for AddressbooksSection {
|
||||
fn name() -> &'static str {
|
||||
"addressbooks"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Template, WebTemplate)]
|
||||
#[template(path = "components/sections/addressbooks_section.html")]
|
||||
pub struct AddressbooksSection {
|
||||
pub user: Principal,
|
||||
pub addressbooks: Vec<(CollectionMetadata, Addressbook)>,
|
||||
pub deleted_addressbooks: Vec<(CollectionMetadata, Addressbook)>,
|
||||
}
|
||||
|
||||
pub async fn route_addressbooks<AS: AddressbookStore>(
|
||||
Path(user_id): Path<String>,
|
||||
Extension(addr_store): Extension<Arc<AS>>,
|
||||
user: Principal,
|
||||
) -> impl IntoResponse {
|
||||
if user_id != user.id {
|
||||
return StatusCode::UNAUTHORIZED.into_response();
|
||||
}
|
||||
|
||||
let mut addressbooks = vec![];
|
||||
for group in user.memberships() {
|
||||
addressbooks.extend(addr_store.get_addressbooks(group).await.unwrap());
|
||||
}
|
||||
|
||||
let mut deleted_addressbooks = vec![];
|
||||
for group in user.memberships() {
|
||||
deleted_addressbooks.extend(addr_store.get_deleted_addressbooks(group).await.unwrap());
|
||||
}
|
||||
|
||||
let mut addressbook_infos = vec![];
|
||||
for addressbook in addressbooks {
|
||||
addressbook_infos.push((
|
||||
addr_store
|
||||
.addressbook_metadata(&addressbook.principal, &addressbook.id)
|
||||
.await
|
||||
.unwrap(),
|
||||
addressbook,
|
||||
));
|
||||
}
|
||||
|
||||
let mut deleted_addressbook_infos = vec![];
|
||||
for addressbook in deleted_addressbooks {
|
||||
deleted_addressbook_infos.push((
|
||||
addr_store
|
||||
.addressbook_metadata(&addressbook.principal, &addressbook.id)
|
||||
.await
|
||||
.unwrap(),
|
||||
addressbook,
|
||||
));
|
||||
}
|
||||
|
||||
UserPage {
|
||||
section: AddressbooksSection {
|
||||
user: user.clone(),
|
||||
addressbooks: addressbook_infos,
|
||||
deleted_addressbooks: deleted_addressbook_infos,
|
||||
},
|
||||
user,
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
74
crates/frontend/src/routes/calendars.rs
Normal file
74
crates/frontend/src/routes/calendars.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::pages::user::{Section, UserPage};
|
||||
use askama::Template;
|
||||
use askama_web::WebTemplate;
|
||||
use axum::{Extension, extract::Path, response::IntoResponse};
|
||||
use http::StatusCode;
|
||||
use rustical_store::{Calendar, CalendarStore, CollectionMetadata, auth::Principal};
|
||||
|
||||
impl Section for CalendarsSection {
|
||||
fn name() -> &'static str {
|
||||
"calendars"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Template, WebTemplate)]
|
||||
#[template(path = "components/sections/calendars_section.html")]
|
||||
pub struct CalendarsSection {
|
||||
pub user: Principal,
|
||||
pub calendars: Vec<(CollectionMetadata, Calendar)>,
|
||||
pub deleted_calendars: Vec<(CollectionMetadata, Calendar)>,
|
||||
}
|
||||
|
||||
pub async fn route_calendars<CS: CalendarStore>(
|
||||
Path(user_id): Path<String>,
|
||||
Extension(cal_store): Extension<Arc<CS>>,
|
||||
user: Principal,
|
||||
) -> impl IntoResponse {
|
||||
if user_id != user.id {
|
||||
return StatusCode::UNAUTHORIZED.into_response();
|
||||
}
|
||||
|
||||
let mut calendars = vec![];
|
||||
for group in user.memberships() {
|
||||
calendars.extend(cal_store.get_calendars(group).await.unwrap());
|
||||
}
|
||||
|
||||
let mut calendar_infos = vec![];
|
||||
for calendar in calendars {
|
||||
calendar_infos.push((
|
||||
cal_store
|
||||
.calendar_metadata(&calendar.principal, &calendar.id)
|
||||
.await
|
||||
.unwrap(),
|
||||
calendar,
|
||||
));
|
||||
}
|
||||
|
||||
let mut deleted_calendars = vec![];
|
||||
for group in user.memberships() {
|
||||
deleted_calendars.extend(cal_store.get_deleted_calendars(group).await.unwrap());
|
||||
}
|
||||
|
||||
let mut deleted_calendar_infos = vec![];
|
||||
for calendar in deleted_calendars {
|
||||
deleted_calendar_infos.push((
|
||||
cal_store
|
||||
.calendar_metadata(&calendar.principal, &calendar.id)
|
||||
.await
|
||||
.unwrap(),
|
||||
calendar,
|
||||
));
|
||||
}
|
||||
|
||||
UserPage {
|
||||
section: CalendarsSection {
|
||||
user: user.clone(),
|
||||
calendars: calendar_infos,
|
||||
deleted_calendars: deleted_calendar_infos,
|
||||
},
|
||||
user,
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
pub mod addressbook;
|
||||
pub mod addressbooks;
|
||||
pub mod app_token;
|
||||
pub mod calendar;
|
||||
pub mod calendars;
|
||||
pub mod login;
|
||||
pub mod user;
|
||||
|
||||
@@ -11,19 +11,23 @@ use axum_extra::{TypedHeader, extract::Host};
|
||||
use headers::UserAgent;
|
||||
use http::StatusCode;
|
||||
use rustical_store::{
|
||||
Addressbook, AddressbookStore, Calendar, CalendarStore,
|
||||
AddressbookStore, CalendarStore,
|
||||
auth::{AppToken, AuthenticationProvider, Principal},
|
||||
};
|
||||
|
||||
use crate::pages::user::{Section, UserPage};
|
||||
|
||||
impl Section for ProfileSection {
|
||||
fn name() -> &'static str {
|
||||
"profile"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Template, WebTemplate)]
|
||||
#[template(path = "pages/user.html")]
|
||||
pub struct UserPage {
|
||||
#[template(path = "components/sections/profile_section.html")]
|
||||
pub struct ProfileSection {
|
||||
pub user: Principal,
|
||||
pub app_tokens: Vec<AppToken>,
|
||||
pub calendars: Vec<Calendar>,
|
||||
pub deleted_calendars: Vec<Calendar>,
|
||||
pub addressbooks: Vec<Addressbook>,
|
||||
pub deleted_addressbooks: Vec<Addressbook>,
|
||||
pub is_apple: bool,
|
||||
pub davx5_hostname: Option<String>,
|
||||
}
|
||||
@@ -69,14 +73,13 @@ pub async fn route_user_named<
|
||||
let davx5_hostname = user_agent.as_str().contains("Android").then_some(host);
|
||||
|
||||
UserPage {
|
||||
app_tokens: auth_provider.get_app_tokens(&user.id).await.unwrap(),
|
||||
calendars,
|
||||
deleted_calendars,
|
||||
addressbooks,
|
||||
deleted_addressbooks,
|
||||
section: ProfileSection {
|
||||
user: user.clone(),
|
||||
app_tokens: auth_provider.get_app_tokens(&user.id).await.unwrap(),
|
||||
is_apple,
|
||||
davx5_hostname,
|
||||
},
|
||||
user,
|
||||
is_apple,
|
||||
davx5_hostname,
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{Error, addressbook::Addressbook};
|
||||
use crate::{CollectionMetadata, Error, addressbook::Addressbook};
|
||||
use async_trait::async_trait;
|
||||
use rustical_ical::AddressObject;
|
||||
|
||||
@@ -35,6 +35,12 @@ pub trait AddressbookStore: Send + Sync + 'static {
|
||||
synctoken: i64,
|
||||
) -> Result<(Vec<AddressObject>, Vec<String>, i64), Error>;
|
||||
|
||||
async fn addressbook_metadata(
|
||||
&self,
|
||||
principal: &str,
|
||||
addressbook_id: &str,
|
||||
) -> Result<CollectionMetadata, Error>;
|
||||
|
||||
async fn get_objects(
|
||||
&self,
|
||||
principal: &str,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{Calendar, error::Error};
|
||||
use crate::{Calendar, CollectionMetadata, error::Error};
|
||||
use async_trait::async_trait;
|
||||
use chrono::NaiveDate;
|
||||
use rustical_ical::CalendarObject;
|
||||
@@ -53,6 +53,12 @@ pub trait CalendarStore: Send + Sync + 'static {
|
||||
self.get_objects(principal, cal_id).await
|
||||
}
|
||||
|
||||
async fn calendar_metadata(
|
||||
&self,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
) -> Result<CollectionMetadata, Error>;
|
||||
|
||||
async fn get_objects(
|
||||
&self,
|
||||
principal: &str,
|
||||
|
||||
@@ -135,6 +135,20 @@ impl<CS: CalendarStore, BS: CalendarStore> CalendarStore for CombinedCalendarSto
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn calendar_metadata(
|
||||
&self,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
) -> Result<crate::CollectionMetadata, Error> {
|
||||
if cal_id.starts_with(BIRTHDAYS_PREFIX) {
|
||||
self.birthday_store
|
||||
.calendar_metadata(principal, cal_id)
|
||||
.await
|
||||
} else {
|
||||
self.cal_store.calendar_metadata(principal, cal_id).await
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
async fn get_objects(
|
||||
&self,
|
||||
|
||||
@@ -16,7 +16,7 @@ fn birthday_calendar(addressbook: Addressbook) -> Calendar {
|
||||
id: format!("{}{}", BIRTHDAYS_PREFIX, addressbook.id),
|
||||
displayname: addressbook
|
||||
.displayname
|
||||
.map(|name| format!("{} birthdays", name)),
|
||||
.map(|name| format!("{name} birthdays")),
|
||||
order: 0,
|
||||
description: None,
|
||||
color: None,
|
||||
@@ -104,6 +104,17 @@ impl<AS: AddressbookStore> CalendarStore for ContactBirthdayStore<AS> {
|
||||
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,
|
||||
|
||||
@@ -37,3 +37,11 @@ pub struct CollectionOperation {
|
||||
pub topic: String,
|
||||
pub data: CollectionOperationInfo,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct CollectionMetadata {
|
||||
pub len: usize,
|
||||
pub deleted_len: usize,
|
||||
pub size: u64,
|
||||
pub deleted_size: u64,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const SYNC_NAMESPACE: &str = "github.com/lennart-k/rustical/ns/";
|
||||
|
||||
pub fn format_synctoken(synctoken: i64) -> String {
|
||||
format!("{}{}", SYNC_NAMESPACE, synctoken)
|
||||
format!("{SYNC_NAMESPACE}{synctoken}")
|
||||
}
|
||||
|
||||
pub fn parse_synctoken(synctoken: &str) -> Option<i64> {
|
||||
|
||||
@@ -8,7 +8,10 @@ license.workspace = true
|
||||
publish = false
|
||||
|
||||
[features]
|
||||
test = []
|
||||
test = ["dep:rstest"]
|
||||
|
||||
[dev-dependencies]
|
||||
rstest.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tokio.workspace = true
|
||||
@@ -25,3 +28,4 @@ password-hash.workspace = true
|
||||
uuid.workspace = true
|
||||
pbkdf2.workspace = true
|
||||
rustical_ical.workspace = true
|
||||
rstest = { workspace = true, optional = true }
|
||||
|
||||
@@ -3,8 +3,8 @@ use async_trait::async_trait;
|
||||
use derive_more::derive::Constructor;
|
||||
use rustical_ical::AddressObject;
|
||||
use rustical_store::{
|
||||
Addressbook, AddressbookStore, CollectionOperation, CollectionOperationInfo, Error,
|
||||
synctoken::format_synctoken,
|
||||
Addressbook, AddressbookStore, CollectionMetadata, CollectionOperation,
|
||||
CollectionOperationInfo, Error, synctoken::format_synctoken,
|
||||
};
|
||||
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
@@ -223,6 +223,28 @@ impl SqliteAddressbookStore {
|
||||
Ok((objects, deleted_objects, new_synctoken))
|
||||
}
|
||||
|
||||
async fn _list_objects<'e, E: Executor<'e, Database = Sqlite>>(
|
||||
executor: E,
|
||||
principal: &str,
|
||||
addressbook_id: &str,
|
||||
) -> Result<Vec<(u64, bool)>, rustical_store::Error> {
|
||||
struct ObjectEntry {
|
||||
length: u64,
|
||||
deleted: bool,
|
||||
}
|
||||
Ok(sqlx::query_as!(
|
||||
ObjectEntry,
|
||||
"SELECT length(vcf) AS 'length!: u64', deleted_at AS 'deleted!: bool' FROM addressobjects WHERE principal = ? AND addressbook_id = ?",
|
||||
principal,
|
||||
addressbook_id
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await.map_err(crate::Error::from)?
|
||||
.into_iter()
|
||||
.map(|row| (row.length, row.deleted))
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn _get_objects<'e, E: Executor<'e, Database = Sqlite>>(
|
||||
executor: E,
|
||||
principal: &str,
|
||||
@@ -442,6 +464,29 @@ impl AddressbookStore for SqliteAddressbookStore {
|
||||
Self::_sync_changes(&self.db, principal, addressbook_id, synctoken).await
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn addressbook_metadata(
|
||||
&self,
|
||||
principal: &str,
|
||||
addressbook_id: &str,
|
||||
) -> Result<CollectionMetadata, rustical_store::Error> {
|
||||
let mut sizes = vec![];
|
||||
let mut deleted_sizes = vec![];
|
||||
for (size, deleted) in Self::_list_objects(&self.db, principal, addressbook_id).await? {
|
||||
if deleted {
|
||||
deleted_sizes.push(size)
|
||||
} else {
|
||||
sizes.push(size)
|
||||
}
|
||||
}
|
||||
Ok(CollectionMetadata {
|
||||
len: sizes.len(),
|
||||
deleted_len: deleted_sizes.len(),
|
||||
size: sizes.iter().sum(),
|
||||
deleted_size: deleted_sizes.iter().sum(),
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn get_objects(
|
||||
&self,
|
||||
|
||||
@@ -5,7 +5,7 @@ use derive_more::derive::Constructor;
|
||||
use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectType};
|
||||
use rustical_store::calendar_store::CalendarQuery;
|
||||
use rustical_store::synctoken::format_synctoken;
|
||||
use rustical_store::{Calendar, CalendarStore, Error};
|
||||
use rustical_store::{Calendar, CalendarStore, CollectionMetadata, Error};
|
||||
use rustical_store::{CollectionOperation, CollectionOperationInfo};
|
||||
use sqlx::types::chrono::NaiveDateTime;
|
||||
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
||||
@@ -242,6 +242,28 @@ impl SqliteCalendarStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn _list_objects<'e, E: Executor<'e, Database = Sqlite>>(
|
||||
executor: E,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
) -> Result<Vec<(u64, bool)>, rustical_store::Error> {
|
||||
struct ObjectEntry {
|
||||
length: u64,
|
||||
deleted: bool,
|
||||
}
|
||||
Ok(sqlx::query_as!(
|
||||
ObjectEntry,
|
||||
"SELECT length(ics) AS 'length!: u64', deleted_at AS 'deleted!: bool' FROM calendarobjects WHERE principal = ? AND cal_id = ?",
|
||||
principal,
|
||||
cal_id
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await.map_err(crate::Error::from)?
|
||||
.into_iter()
|
||||
.map(|row| (row.length, row.deleted))
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn _get_objects<'e, E: Executor<'e, Database = Sqlite>>(
|
||||
executor: E,
|
||||
principal: &str,
|
||||
@@ -552,6 +574,28 @@ impl CalendarStore for SqliteCalendarStore {
|
||||
Self::_calendar_query(&self.db, principal, cal_id, query).await
|
||||
}
|
||||
|
||||
async fn calendar_metadata(
|
||||
&self,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
) -> Result<CollectionMetadata, Error> {
|
||||
let mut sizes = vec![];
|
||||
let mut deleted_sizes = vec![];
|
||||
for (size, deleted) in Self::_list_objects(&self.db, principal, cal_id).await? {
|
||||
if deleted {
|
||||
deleted_sizes.push(size)
|
||||
} else {
|
||||
sizes.push(size)
|
||||
}
|
||||
}
|
||||
Ok(CollectionMetadata {
|
||||
len: sizes.len(),
|
||||
deleted_len: deleted_sizes.len(),
|
||||
size: sizes.iter().sum(),
|
||||
deleted_size: deleted_sizes.iter().sum(),
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn get_objects(
|
||||
&self,
|
||||
|
||||
@@ -114,9 +114,11 @@ impl AuthenticationProvider for SqlitePrincipalStore {
|
||||
let password = user.password.map(Secret::into_inner);
|
||||
sqlx::query!(
|
||||
r#"
|
||||
REPLACE INTO principals
|
||||
(id, displayname, principal_type, password_hash)
|
||||
VALUES (?, ?, ?, ?)
|
||||
INSERT INTO principals
|
||||
(id, displayname, principal_type, password_hash) VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
(displayname, principal_type, password_hash)
|
||||
= (excluded.displayname, excluded.principal_type, excluded.password_hash)
|
||||
"#,
|
||||
user.id,
|
||||
user.displayname,
|
||||
|
||||
@@ -2,41 +2,54 @@ use crate::{
|
||||
SqliteStore, addressbook_store::SqliteAddressbookStore, calendar_store::SqliteCalendarStore,
|
||||
principal_store::SqlitePrincipalStore,
|
||||
};
|
||||
use rustical_store::{
|
||||
AddressbookStore, CalendarStore, CollectionOperation, SubscriptionStore,
|
||||
auth::AuthenticationProvider,
|
||||
};
|
||||
use rustical_store::auth::{AuthenticationProvider, Principal, PrincipalType};
|
||||
use sqlx::SqlitePool;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
pub async fn get_test_stores() -> (
|
||||
Arc<impl AddressbookStore>,
|
||||
Arc<impl CalendarStore>,
|
||||
Arc<impl SubscriptionStore>,
|
||||
Arc<impl AuthenticationProvider>,
|
||||
Receiver<CollectionOperation>,
|
||||
) {
|
||||
let db = SqlitePool::connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::migrate!("./migrations").run(&db).await.unwrap();
|
||||
// let db = create_db_pool("sqlite::memory:", true).await.unwrap();
|
||||
// Channel to watch for changes (for DAV Push)
|
||||
let (send, recv) = tokio::sync::mpsc::channel(1000);
|
||||
static DB: OnceCell<SqlitePool> = OnceCell::const_new();
|
||||
|
||||
let addressbook_store = Arc::new(SqliteAddressbookStore::new(db.clone(), send.clone()));
|
||||
let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send));
|
||||
let subscription_store = Arc::new(SqliteStore::new(db.clone()));
|
||||
let principal_store = Arc::new(SqlitePrincipalStore::new(db.clone()));
|
||||
(
|
||||
addressbook_store,
|
||||
cal_store,
|
||||
subscription_store,
|
||||
principal_store,
|
||||
recv,
|
||||
)
|
||||
async fn get_test_db() -> SqlitePool {
|
||||
DB.get_or_init(async || {
|
||||
let db = SqlitePool::connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::migrate!("./migrations").run(&db).await.unwrap();
|
||||
|
||||
// Populate with test data
|
||||
let principal_store = SqlitePrincipalStore::new(db.clone());
|
||||
principal_store
|
||||
.insert_principal(
|
||||
Principal {
|
||||
id: "user".to_owned(),
|
||||
displayname: None,
|
||||
memberships: vec![],
|
||||
password: None,
|
||||
principal_type: PrincipalType::Individual,
|
||||
},
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db
|
||||
})
|
||||
.await
|
||||
.clone()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_store() {
|
||||
get_test_stores().await;
|
||||
#[rstest::fixture]
|
||||
pub async fn get_test_addressbook_store() -> SqliteAddressbookStore {
|
||||
let (send, _recv) = tokio::sync::mpsc::channel(1000);
|
||||
SqliteAddressbookStore::new(get_test_db().await, send)
|
||||
}
|
||||
#[rstest::fixture]
|
||||
pub async fn get_test_calendar_store() -> SqliteCalendarStore {
|
||||
let (send, _recv) = tokio::sync::mpsc::channel(1000);
|
||||
SqliteCalendarStore::new(get_test_db().await, send)
|
||||
}
|
||||
#[rstest::fixture]
|
||||
pub async fn get_test_subscription_store() -> SqliteStore {
|
||||
SqliteStore::new(get_test_db().await)
|
||||
}
|
||||
#[rstest::fixture]
|
||||
pub async fn get_test_principal_store() -> SqlitePrincipalStore {
|
||||
SqlitePrincipalStore::new(get_test_db().await)
|
||||
}
|
||||
|
||||
@@ -13,12 +13,12 @@ impl Enum {
|
||||
|
||||
quote! {
|
||||
impl #impl_generics ::rustical_xml::XmlSerialize for #ident #type_generics #where_clause {
|
||||
fn serialize<W: ::std::io::Write>(
|
||||
fn serialize(
|
||||
&self,
|
||||
ns: Option<::quick_xml::name::Namespace>,
|
||||
tag: Option<&[u8]>,
|
||||
namespaces: &::std::collections::HashMap<::quick_xml::name::Namespace, &[u8]>,
|
||||
writer: &mut ::quick_xml::Writer<W>
|
||||
writer: &mut ::quick_xml::Writer<&mut Vec<u8>>
|
||||
) -> ::std::io::Result<()> {
|
||||
use ::quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
|
||||
|
||||
|
||||
@@ -88,12 +88,12 @@ impl NamedStruct {
|
||||
|
||||
quote! {
|
||||
impl #impl_generics ::rustical_xml::XmlSerialize for #ident #type_generics #where_clause {
|
||||
fn serialize<W: ::std::io::Write>(
|
||||
fn serialize(
|
||||
&self,
|
||||
ns: Option<::quick_xml::name::Namespace>,
|
||||
tag: Option<&[u8]>,
|
||||
namespaces: &::std::collections::HashMap<::quick_xml::name::Namespace, &[u8]>,
|
||||
writer: &mut ::quick_xml::Writer<W>
|
||||
writer: &mut ::quick_xml::Writer<&mut Vec<u8>>
|
||||
) -> ::std::io::Result<()> {
|
||||
use ::quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
|
||||
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
use crate::XmlRootTag;
|
||||
use quick_xml::{
|
||||
events::{attributes::Attribute, BytesStart, Event},
|
||||
events::{BytesStart, Event, attributes::Attribute},
|
||||
name::{Namespace, QName},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
pub use xml_derive::XmlSerialize;
|
||||
|
||||
pub trait XmlSerialize {
|
||||
fn serialize<W: std::io::Write>(
|
||||
fn serialize(
|
||||
&self,
|
||||
ns: Option<Namespace>,
|
||||
tag: Option<&[u8]>,
|
||||
namespaces: &HashMap<Namespace, &[u8]>,
|
||||
writer: &mut quick_xml::Writer<W>,
|
||||
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
|
||||
) -> std::io::Result<()>;
|
||||
|
||||
fn attributes<'a>(&self) -> Option<impl IntoIterator<Item: Into<Attribute<'a>>>>;
|
||||
fn attributes<'a>(&self) -> Option<Vec<Attribute<'a>>>;
|
||||
}
|
||||
|
||||
impl<T: XmlSerialize> XmlSerialize for Option<T> {
|
||||
fn serialize<W: std::io::Write>(
|
||||
fn serialize(
|
||||
&self,
|
||||
ns: Option<Namespace>,
|
||||
tag: Option<&[u8]>,
|
||||
namespaces: &HashMap<Namespace, &[u8]>,
|
||||
writer: &mut quick_xml::Writer<W>,
|
||||
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
|
||||
) -> std::io::Result<()> {
|
||||
if let Some(some) = self {
|
||||
some.serialize(ns, tag, namespaces, writer)
|
||||
@@ -33,36 +33,36 @@ impl<T: XmlSerialize> XmlSerialize for Option<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(refining_impl_trait)]
|
||||
fn attributes<'a>(&self) -> Option<Vec<Attribute<'a>>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub trait XmlSerializeRoot {
|
||||
fn serialize_root<W: std::io::Write>(
|
||||
&self,
|
||||
writer: &mut quick_xml::Writer<W>,
|
||||
) -> std::io::Result<()>;
|
||||
fn serialize_root(&self, writer: &mut quick_xml::Writer<&mut Vec<u8>>) -> std::io::Result<()>;
|
||||
|
||||
fn serialize_to_string(&self) -> std::io::Result<String> {
|
||||
let mut buf: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into();
|
||||
let mut writer = quick_xml::Writer::new(&mut buf);
|
||||
self.serialize_root(&mut writer)?;
|
||||
Ok(String::from_utf8_lossy(&buf).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: XmlSerialize + XmlRootTag> XmlSerializeRoot for T {
|
||||
fn serialize_root<W: std::io::Write>(
|
||||
&self,
|
||||
writer: &mut quick_xml::Writer<W>,
|
||||
) -> std::io::Result<()> {
|
||||
fn serialize_root(&self, writer: &mut quick_xml::Writer<&mut Vec<u8>>) -> std::io::Result<()> {
|
||||
let namespaces = Self::root_ns_prefixes();
|
||||
self.serialize(Self::root_ns(), Some(Self::root_tag()), &namespaces, writer)
|
||||
}
|
||||
}
|
||||
|
||||
impl XmlSerialize for () {
|
||||
fn serialize<W: std::io::Write>(
|
||||
fn serialize(
|
||||
&self,
|
||||
ns: Option<Namespace>,
|
||||
tag: Option<&[u8]>,
|
||||
namespaces: &HashMap<Namespace, &[u8]>,
|
||||
writer: &mut quick_xml::Writer<W>,
|
||||
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
|
||||
) -> std::io::Result<()> {
|
||||
let prefix = ns
|
||||
.map(|ns| namespaces.get(&ns))
|
||||
@@ -89,7 +89,6 @@ impl XmlSerialize for () {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(refining_impl_trait)]
|
||||
fn attributes<'a>(&self) -> Option<Vec<quick_xml::events::attributes::Attribute<'a>>> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -104,12 +104,12 @@ impl<T: ValueDeserialize> XmlDeserialize for T {
|
||||
}
|
||||
|
||||
impl<T: ValueSerialize> XmlSerialize for T {
|
||||
fn serialize<W: std::io::Write>(
|
||||
fn serialize(
|
||||
&self,
|
||||
ns: Option<Namespace>,
|
||||
tag: Option<&[u8]>,
|
||||
namespaces: &HashMap<Namespace, &[u8]>,
|
||||
writer: &mut quick_xml::Writer<W>,
|
||||
writer: &mut quick_xml::Writer<&mut Vec<u8>>,
|
||||
) -> std::io::Result<()> {
|
||||
let prefix = ns
|
||||
.map(|ns| namespaces.get(&ns))
|
||||
@@ -140,7 +140,6 @@ impl<T: ValueSerialize> XmlSerialize for T {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(refining_impl_trait)]
|
||||
fn attributes<'a>(&self) -> Option<Vec<quick_xml::events::attributes::Attribute<'a>>> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -198,6 +198,7 @@ fn test_struct_generics() {
|
||||
#[derive(XmlDeserialize, XmlRootTag)]
|
||||
#[xml(root = b"document")]
|
||||
struct Document<T: XmlDeserialize> {
|
||||
#[allow(dead_code)]
|
||||
child: T,
|
||||
}
|
||||
|
||||
@@ -218,6 +219,7 @@ fn test_struct_unparsed() {
|
||||
#[derive(XmlDeserialize, XmlRootTag)]
|
||||
#[xml(root = b"document")]
|
||||
struct Document {
|
||||
#[allow(dead_code)]
|
||||
child: Unparsed,
|
||||
}
|
||||
|
||||
|
||||
@@ -23,12 +23,16 @@ enum ExtensionProp {
|
||||
enum CalendarProp {
|
||||
// WebDAV (RFC 2518)
|
||||
#[xml(ns = "NS_DAV")]
|
||||
#[allow(dead_code)]
|
||||
Displayname(Option<String>),
|
||||
#[xml(ns = "NS_DAV")]
|
||||
#[allow(dead_code)]
|
||||
Getcontenttype(&'static str),
|
||||
|
||||
#[xml(ns = "NS_DAV", rename = b"principal-URL")]
|
||||
#[allow(dead_code)]
|
||||
PrincipalUrl,
|
||||
#[allow(dead_code)]
|
||||
Topic,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,29 +4,34 @@ use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag};
|
||||
fn test_propertyupdate() {
|
||||
#[derive(XmlDeserialize)]
|
||||
struct SetPropertyElement<T: XmlDeserialize> {
|
||||
#[allow(dead_code)]
|
||||
prop: T,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize)]
|
||||
struct TagName {
|
||||
#[xml(ty = "tag_name")]
|
||||
#[allow(dead_code)]
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize)]
|
||||
struct PropertyElement {
|
||||
#[xml(ty = "untagged")]
|
||||
#[allow(dead_code)]
|
||||
property: TagName,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize)]
|
||||
struct RemovePropertyElement {
|
||||
#[allow(dead_code)]
|
||||
prop: PropertyElement,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize)]
|
||||
enum Operation<T: XmlDeserialize> {
|
||||
Set(SetPropertyElement<T>),
|
||||
#[allow(dead_code)]
|
||||
Remove(RemovePropertyElement),
|
||||
}
|
||||
|
||||
@@ -34,10 +39,11 @@ fn test_propertyupdate() {
|
||||
#[xml(root = b"propertyupdate")]
|
||||
struct PropertyupdateElement<T: XmlDeserialize> {
|
||||
#[xml(ty = "untagged", flatten)]
|
||||
#[allow(dead_code)]
|
||||
operations: Vec<Operation<T>>,
|
||||
}
|
||||
|
||||
let doc = PropertyupdateElement::<Unparsed>::parse_str(
|
||||
PropertyupdateElement::<Unparsed>::parse_str(
|
||||
r#"
|
||||
<propertyupdate>
|
||||
<set>
|
||||
|
||||
@@ -11,17 +11,17 @@ fn test_struct_value_tagged() {
|
||||
#[derive(Debug, XmlSerialize, PartialEq)]
|
||||
enum Prop {
|
||||
Test(String),
|
||||
Hello(usize),
|
||||
Unit,
|
||||
// Hello(usize),
|
||||
// Unit,
|
||||
}
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut writer = quick_xml::Writer::new(&mut buf);
|
||||
Document {
|
||||
let out = Document {
|
||||
prop: Prop::Test("asd".to_owned()),
|
||||
}
|
||||
.serialize_root(&mut writer)
|
||||
.serialize_to_string()
|
||||
.unwrap();
|
||||
let out = String::from_utf8(buf).unwrap();
|
||||
assert_eq!(out, "<propfind><prop><test>asd</test></prop></propfind>");
|
||||
assert_eq!(
|
||||
out,
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<propfind><prop><test>asd</test></prop></propfind>"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
use std::{
|
||||
borrow::{Borrow, Cow},
|
||||
collections::HashMap,
|
||||
};
|
||||
|
||||
use quick_xml::Writer;
|
||||
use quick_xml::name::Namespace;
|
||||
use rustical_xml::{XmlDocument, XmlRootTag, XmlSerialize, XmlSerializeRoot};
|
||||
use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot};
|
||||
use std::collections::HashMap;
|
||||
use xml_derive::XmlDeserialize;
|
||||
|
||||
#[test]
|
||||
@@ -22,16 +18,13 @@ fn test_struct_document() {
|
||||
text: String,
|
||||
}
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut writer = quick_xml::Writer::new(&mut buf);
|
||||
Document {
|
||||
child: Child {
|
||||
text: "asd".to_owned(),
|
||||
},
|
||||
}
|
||||
.serialize_root(&mut writer)
|
||||
.serialize_to_string()
|
||||
.unwrap();
|
||||
let out = String::from_utf8(buf).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -51,17 +44,14 @@ fn test_struct_untagged_attr() {
|
||||
text: String,
|
||||
}
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut writer = quick_xml::Writer::new(&mut buf);
|
||||
Document {
|
||||
name: "okay".to_owned(),
|
||||
child: Child {
|
||||
text: "asd".to_owned(),
|
||||
},
|
||||
}
|
||||
.serialize_root(&mut writer)
|
||||
.serialize_to_string()
|
||||
.unwrap();
|
||||
let out = String::from_utf8(buf).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -73,16 +63,16 @@ fn test_struct_value_tagged() {
|
||||
num: usize,
|
||||
}
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut writer = quick_xml::Writer::new(&mut buf);
|
||||
Document {
|
||||
let out = Document {
|
||||
href: "okay".to_owned(),
|
||||
num: 123,
|
||||
}
|
||||
.serialize_root(&mut writer)
|
||||
.serialize_to_string()
|
||||
.unwrap();
|
||||
let out = String::from_utf8(buf).unwrap();
|
||||
assert_eq!(out, "<document><href>okay</href><num>123</num></document>");
|
||||
assert_eq!(
|
||||
out,
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<document><href>okay</href><num>123</num></document>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -94,15 +84,15 @@ fn test_struct_value_untagged() {
|
||||
href: String,
|
||||
}
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut writer = quick_xml::Writer::new(&mut buf);
|
||||
Document {
|
||||
let out = Document {
|
||||
href: "okays".to_owned(),
|
||||
}
|
||||
.serialize_root(&mut writer)
|
||||
.serialize_to_string()
|
||||
.unwrap();
|
||||
let out = String::from_utf8(buf).unwrap();
|
||||
assert_eq!(out, "<document>okays</document>");
|
||||
assert_eq!(
|
||||
out,
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<document>okays</document>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -114,17 +104,14 @@ fn test_struct_vec() {
|
||||
href: Vec<String>,
|
||||
}
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut writer = quick_xml::Writer::new(&mut buf);
|
||||
Document {
|
||||
let out = Document {
|
||||
href: vec!["okay".to_owned(), "wow".to_owned()],
|
||||
}
|
||||
.serialize_root(&mut writer)
|
||||
.serialize_to_string()
|
||||
.unwrap();
|
||||
let out = String::from_utf8(buf).unwrap();
|
||||
assert_eq!(
|
||||
out,
|
||||
"<document><href>okay</href><href>wow</href></document>"
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<document><href>okay</href><href>wow</href></document>"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -137,25 +124,25 @@ fn test_struct_serialize_with() {
|
||||
href: String,
|
||||
}
|
||||
|
||||
fn serialize_href<W: ::std::io::Write>(
|
||||
fn serialize_href(
|
||||
val: &str,
|
||||
ns: Option<Namespace>,
|
||||
tag: Option<&[u8]>,
|
||||
namespaces: &HashMap<Namespace, &[u8]>,
|
||||
writer: &mut Writer<W>,
|
||||
writer: &mut Writer<&mut Vec<u8>>,
|
||||
) -> std::io::Result<()> {
|
||||
val.to_uppercase().serialize(ns, tag, namespaces, writer)
|
||||
}
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut writer = quick_xml::Writer::new(&mut buf);
|
||||
Document {
|
||||
let out = Document {
|
||||
href: "okay".to_owned(),
|
||||
}
|
||||
.serialize_root(&mut writer)
|
||||
.serialize_to_string()
|
||||
.unwrap();
|
||||
let out = String::from_utf8(buf).unwrap();
|
||||
assert_eq!(out, "<document><href>OKAY</href></document>");
|
||||
assert_eq!(
|
||||
out,
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<document><href>OKAY</href></document>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -173,8 +160,6 @@ fn test_struct_tag_list() {
|
||||
name: String,
|
||||
}
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut writer = quick_xml::Writer::new(&mut buf);
|
||||
Document {
|
||||
tags: vec![
|
||||
Tag {
|
||||
@@ -188,10 +173,8 @@ fn test_struct_tag_list() {
|
||||
},
|
||||
],
|
||||
}
|
||||
.serialize_root(&mut writer)
|
||||
.serialize_to_string()
|
||||
.unwrap();
|
||||
let out = String::from_utf8(buf).unwrap();
|
||||
dbg!(out);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -205,15 +188,11 @@ fn test_struct_ns() {
|
||||
child: String,
|
||||
}
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut writer = quick_xml::Writer::new(&mut buf);
|
||||
Document {
|
||||
child: "hello!".to_string(),
|
||||
}
|
||||
.serialize_root(&mut writer)
|
||||
.serialize_to_string()
|
||||
.unwrap();
|
||||
let out = String::from_utf8(buf).unwrap();
|
||||
dbg!(out);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -227,16 +206,11 @@ fn test_struct_tuple() {
|
||||
#[derive(Debug, XmlSerialize, PartialEq, Default)]
|
||||
struct Child(#[xml(ty = "tag_name")] String, #[xml(ty = "text")] String);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut writer = quick_xml::Writer::new(&mut buf);
|
||||
|
||||
Document {
|
||||
child: Child("child".to_owned(), "Hello!".to_owned()),
|
||||
}
|
||||
.serialize_root(&mut writer)
|
||||
.serialize_to_string()
|
||||
.unwrap();
|
||||
let out = String::from_utf8(buf).unwrap();
|
||||
dbg!(out);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -247,11 +221,7 @@ fn test_tuple_struct() {
|
||||
#[xml(root = b"document")]
|
||||
struct Document(#[xml(ns = "NS", rename = b"okay")] String);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut writer = quick_xml::Writer::new(&mut buf);
|
||||
Document("hello!".to_string())
|
||||
.serialize_root(&mut writer)
|
||||
.serialize_to_string()
|
||||
.unwrap();
|
||||
let out = String::from_utf8(buf).unwrap();
|
||||
dbg!(out);
|
||||
}
|
||||
|
||||
@@ -27,3 +27,4 @@ a CalDAV/CardDAV server
|
||||
- Evolution
|
||||
- Apple Calendar
|
||||
- Home Assistant integration
|
||||
- Thunderbird
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
use argon2::password_hash::SaltString;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use password_hash::{PasswordHasher, rand_core::OsRng};
|
||||
use pbkdf2::Params;
|
||||
use rustical_frontend::FrontendConfig;
|
||||
|
||||
use crate::config::{
|
||||
Config, DataStoreConfig, DavPushConfig, HttpConfig, SqliteDataStoreConfig, TracingConfig,
|
||||
};
|
||||
use clap::Parser;
|
||||
use rustical_frontend::FrontendConfig;
|
||||
|
||||
mod membership;
|
||||
pub mod principals;
|
||||
@@ -33,49 +29,3 @@ pub fn cmd_gen_config(_args: GenConfigArgs) -> anyhow::Result<()> {
|
||||
println!("{generated_config}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, ValueEnum)]
|
||||
enum PwhashAlgorithm {
|
||||
#[value(help = "Use this for your password")]
|
||||
Argon2,
|
||||
#[value(help = "Significantly faster algorithm, use for app tokens")]
|
||||
Pbkdf2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct PwhashArgs {
|
||||
#[arg(long, short = 'a')]
|
||||
algorithm: PwhashAlgorithm,
|
||||
#[arg(
|
||||
long,
|
||||
short = 'r',
|
||||
help = "ONLY for pbkdf2: Number of rounds to calculate",
|
||||
default_value_t = 100
|
||||
)]
|
||||
rounds: u32,
|
||||
}
|
||||
|
||||
pub fn cmd_pwhash(args: PwhashArgs) -> anyhow::Result<()> {
|
||||
println!("Enter your password:");
|
||||
let password = rpassword::read_password()?;
|
||||
let salt = SaltString::generate(OsRng);
|
||||
let password_hash = match args.algorithm {
|
||||
PwhashAlgorithm::Argon2 => argon2::Argon2::default()
|
||||
.hash_password(password.as_bytes(), &salt)
|
||||
.unwrap(),
|
||||
PwhashAlgorithm::Pbkdf2 => pbkdf2::Pbkdf2
|
||||
.hash_password_customized(
|
||||
password.as_bytes(),
|
||||
None,
|
||||
None,
|
||||
Params {
|
||||
rounds: args.rounds,
|
||||
..Default::default()
|
||||
},
|
||||
&salt,
|
||||
)
|
||||
.unwrap(),
|
||||
};
|
||||
println!("{password_hash}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ use app::make_app;
|
||||
use axum::ServiceExt;
|
||||
use axum::extract::Request;
|
||||
use clap::{Parser, Subcommand};
|
||||
use commands::cmd_gen_config;
|
||||
use commands::principals::{PrincipalsArgs, cmd_principals};
|
||||
use commands::{cmd_gen_config, cmd_pwhash};
|
||||
use config::{DataStoreConfig, SqliteDataStoreConfig};
|
||||
use figment::Figment;
|
||||
use figment::providers::{Env, Format, Toml};
|
||||
@@ -43,7 +43,6 @@ struct Args {
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Command {
|
||||
GenConfig(commands::GenConfigArgs),
|
||||
Pwhash(commands::PwhashArgs),
|
||||
Principals(PrincipalsArgs),
|
||||
}
|
||||
|
||||
@@ -84,7 +83,6 @@ async fn main() -> Result<()> {
|
||||
|
||||
match args.command {
|
||||
Some(Command::GenConfig(gen_config_args)) => cmd_gen_config(gen_config_args)?,
|
||||
Some(Command::Pwhash(pwhash_args)) => cmd_pwhash(pwhash_args)?,
|
||||
Some(Command::Principals(principals_args)) => cmd_principals(principals_args).await?,
|
||||
None => {
|
||||
let config: Config = Figment::new()
|
||||
|
||||
Reference in New Issue
Block a user